【问题标题】:Handling interdependent and/or layered asynchronous calls处理相互依赖和/或分层的异步调用
【发布时间】:2013-01-18 21:14:25
【问题描述】:

例如,假设我想从某个地方获取文件列表,然后加载这些文件的内容,最后将它们显示给用户。在同步模型中,它会是这样的(伪代码):

var file_list = fetchFiles(source);

if (!file_list) {
    display('failed to fetch list');

} else {
        for (file in file_list) { // iteration, not enumeration
        var data = loadFile(file);

        if (!data) {
            display('failed to load: ' + file);
        } else {
            display(data);
        }
    }
}

这为用户提供了不错的反馈,如果我认为有必要,我可以将代码片段移动到函数中。生活很简单。

现在,粉碎我的梦想:fetchFiles()loadFile() 实际上是异步的。简单的方法是将它们转换为同步函数。但是,如果浏览器锁定等待调用完成,这就不好了。

如何以经典的 reductio ad spaghettum 方式处理多个相互依赖和/或分层的异步调用,而无需深入研究无尽的回调链?是否有经过验证的范例可以干净地处理这些问题,同时保持代码松散耦合?

【问题讨论】:

  • 这两个异步函数是否将回调作为其第二个参数?
  • 是否要并行加载所有文件?并为每个单独显示数据/警报错误?
  • 您能否对现有答案发表评论,说明为什么它们不可接受。反馈可以帮助我们帮助您。
  • @BenFelda 我将进一步调查现有的答案,但是当我开始赏金时,只有 jQuery 答案可用。
  • @Bergi 这实际上只是一个例子,我想要的是一种简洁的方式来构建具有大量异步调用的代码。

标签: javascript asynchronous loose-coupling waterfall


【解决方案1】:

Deferreds 确实是这里的方法。它们准确地捕获了您(以及大量异步代码)想要的东西:“走开,做这件可能很昂贵的事情,同时不要打扰我,然后等你回来再做。”

而且你不需要 jQuery 来使用它们。一个有进取心的人有ported Deferred to underscore,并声称你甚至不需要下划线就可以使用它。

所以您的代码可以如下所示:

function fetchFiles(source) {
    var dfd = _.Deferred();

    // do some kind of thing that takes a long time
    doExpensiveThingOne({
        source: source,
        complete: function(files) {
            // this informs the Deferred that it succeeded, and passes
            // `files` to all its success ("done") handlers
            dfd.resolve(files);

            // if you know how to capture an error condition, you can also
            // indicate that with dfd.reject(...)
        }
    });

    return dfd;
}

function loadFile(file) {
    // same thing!
    var dfd = _.Deferred();

    doExpensiveThingTwo({
        file: file,
        complete: function(data) {
            dfd.resolve(data);
        }
    });

    return dfd;
}

// and now glue it together
_.when(fetchFiles(source))
.done(function(files) {
    for (var file in files) {
        _.when(loadFile(file))
        .done(function(data) {
            display(data);
        })
        .fail(function() {
            display('failed to load: ' + file);
        });
    }
})
.fail(function() {
    display('failed to fetch list');
});

设置有点冗长,但是一旦您编写了处理 Deferred 状态的代码并将其填充到某个函数中,您就不必再担心它了,您就可以玩转实际流程了事件很容易。例如:

var file_dfds = [];
for (var file in files) {
    file_dfds.push(loadFile(file));
}

_.when(file_dfds)
.done(function(datas) {
    // this will only run if and when ALL the files have successfully
    // loaded!
});

【讨论】:

  • +1。尽管应该注意的是,这将安排每个loadFile() 几乎同时进行,从而导致并行下载......根据情况可能不需要。
  • 延迟的美妙之处在于您可以按时间或进度错开它们,甚至可以连续运行它们,而无需触及执行下载的代码或处理数据的代码。
  • 但是要创建 promise 对象,您通常还需要执行 Ajax 调用,这就是我的意思。
【解决方案2】:

活动

也许使用事件是个好主意。它可以防止您创建代码树并解耦您的代码。

我使用bean 作为事件的框架。

示例伪代码

// async request for files
function fetchFiles(source) {

    IO.get(..., function (data, status) {
        if(data) {
            bean.fire(window, 'fetched_files', data);
        } else {
            bean.fire(window, 'fetched_files_fail', data, status);
        } 
    });

}

// handler for when we get data
function onFetchedFiles (event, files) {
    for (file in files) { 
        var data = loadFile(file);

        if (!data) {
            display('failed to load: ' + file);
        } else {
            display(data);
        }
    }
}

// handler for failures
function onFetchedFilesFail (event, status) {
    display('Failed to fetch list. Reason: ' + status);
}

// subscribe the window to these events
bean.on(window, 'fetched_files', onFetchedFiles);
bean.on(window, 'fetched_files_fail', onFetchedFilesFail);

fetchFiles();

几乎所有流行的 JS 框架都实现了自定义事件和这种事件处理。

【讨论】:

    【解决方案3】:

    听起来你需要jQuery Deferred。以下是一些未经测试的代码,可能有助于为您指明正确的方向:

    $.when(fetchFiles(source)).then(function(file_list) { 
      if (!file_list) {
        display('failed to fetch list');
      } else {
        for (file in file_list) {
          $.when(loadFile(file)).then(function(data){
            if (!data) {
              display('failed to load: ' + file);
            } else {
              display(data);
            }
          });
        }
      }
    });
    

    我还找到了另一个 decent post,它提供了 Deferred 对象的一些用例

    【讨论】:

    • 请注意 - 您必须修改“fetchFiles”和“loadFile”,以便它们返回一个 jQuery Deferred 对象。例如,$.ajax 就是这样做的
    • 您可以使用donefail分别处理成功和失败,而不是检查传递给then的参数
    【解决方案4】:

    如果您不想使用 jQuery,您可以使用结合同步请求的网络工作者。除了 10 之前的任何 Internet Explorer 版本外,所有主流浏览器都支持 Web Worker。

    Web Worker browser compatability

    基本上,如果您不能完全确定 Web Worker 是什么,请将其视为浏览器在不影响主线程的情况下在单独的线程上执行专门的 JavaScript 的一种方式(警告:在单核上CPU,两个线程将交替运行。幸运的是,现在大多数计算机都配备了双核 CPU)。通常,网络工作者是为复杂的计算或一些密集的处理任务而保留的。请记住,Web Worker 中的任何代码都不能引用 DOM,也不能引用任何尚未传递给它的全局数据结构。本质上,网络工作者独立于主线程运行。 Worker 执行的任何代码都应与 JavaScript 代码库的其余部分分开,放在它自己的 JS 文件中。此外,如果网络工作者需要特定数据才能正常工作,您需要在启动时将这些数据传递给它们。

    另一个值得注意的重要事情是,您需要用于加载文件的任何 JS 库都需要直接复制到 worker 将执行的 JavaScript 文件中。这意味着这些库应该首先被缩小(如果它们还没有被缩小),然后复制并粘贴到文件的顶部。

    无论如何,我决定编写一个基本模板来向您展示如何处理这个问题。看看下面。随时提出问题/批评/等。

    在您希望在主线程上继续执行的 JS 文件上,您需要类似下面的代码来调用工作线程。

    function startWorker(dataObj)
    {
        var message = {},
            worker;
    
          try
          {
            worker = new Worker('workers/getFileData.js');
          } 
          catch(error) 
          {
            // Throw error
          }
    
        message.data = dataObj;
    
        // all data is communicated to the worker in JSON format
        message = JSON.stringify(message);
    
        // This is the function that will handle all data returned by the worker
        worker.onMessage = function(e)
        {
            display(JSON.parse(e.data));
        }
    
        worker.postMessage(message);
    }
    

    然后,在一个用于工作人员的单独文件中(如您在上面的代码中所见,我将文件命名为getFileData.js),编写如下内容...

    function fetchFiles(source)
    {
        // Put your code here
        // Keep in mind that any requests made should be synchronous as this should not
        // impact the main thread
    }
    
    function loadFile(file)
    {
        // Put your code here
        // Keep in mind that any requests made should be synchronous as this should not
        // impact the main thread
    }
    
    onmessage = function(e)
    {
        var response = [],
            data = JSON.parse(e.data),
            file_list = fetchFiles(data.source),
            file, fileData;
    
        if (!file_list) 
        {
            response.push('failed to fetch list');
        }
        else 
        {
            for (file in file_list) 
            { // iteration, not enumeration
                fileData = loadFile(file);
    
                if (!fileData) 
                {
                    response.push('failed to load: ' + file);
                } 
                else 
                {
                    response.push(fileData);
                }
            }
        }
    
        response = JSON.stringify(response);
    
        postMessage(response);
    
        close();
    }
    

    PS:另外,我挖了另一个线程,它可以更好地帮助您了解将同步请求与 Web Worker 结合使用的优缺点。

    Stack Overflow - Web Workers and Synchronous Requests

    【讨论】:

      【解决方案5】:

      async 是一个流行的异步流控制库,经常与 node.js 一起使用。我从未在浏览器中亲自使用过它,但显然它也可以在那里工作。

      此示例将(理论上)运行您的两个函数,返回所有文件名及其加载状态的对象。 async.map 并行运行,而waterfall 是一个系列,将每一步的结果传递到下一步。

      我在这里假设您的两个异步函数接受回调。如果他们不这样做,我需要更多关于他们打算如何使用的信息(他们会在完成时触发事件吗?等等)。

      async.waterfall([
        function (done) {
          fetchFiles(source, function(list) {
            if (!list) done('failed to fetch file list');
            else done(null, list);
          });
          // alternatively you could simply fetchFiles(source, done) here, and handle
          // the null result in the next function.
        },
      
        function (file_list, done) {
          var loadHandler = function (memo, file, cb) {
            loadFile(file, function(data) {
              if (!data) {
                display('failed to load: ' + file);
              } else {
                display(data);
              }
              // if any of the callbacks to `map` returned an error, it would halt 
              // execution and pass that error to the final callback.  So we don't pass
              // an error here, but rather a tuple of the file and load result.
              cb(null, [file, !!data]);
            });
          };
          async.map(file_list, loadHandler, done);
        }
      ], function(err, result) {
        if (err) return display(err);
        // All files loaded! (or failed to load)
        // result would be an array of tuples like [[file, bool file loaded?], ...]
      });
      

      waterfall 接受一个函数数组并按顺序执行它们,将每个函数的结果作为参数传递给下一个,以及一个回调函数作为最后一个参数,您可以使用错误或函数的结果数据。

      您当然可以在这两者之间或周围添加任意数量的不同异步回调,而根本无需更改代码的结构。 waterfall 实际上只是 10 种不同的流控制结构中的一种,因此您有很多选择(尽管我几乎总是最终使用 auto,它允许您通过 Makefile 之类的在同一个函数中混合并行和串行执行要求语法)。

      【讨论】:

        【解决方案6】:

        我正在开发的一个 web 应用程序遇到了这个问题,这就是我解决它的方法(没有库)。

        第 1 步:写了very lightweight pubsub implementation。没有什么花哨。订阅、取消订阅、发布和记录。一切(使用 cmets)加起来 93 行 Javascript。 gzip 前 2.7kb。

        第 2 步:通过让 pubsub 实现完成繁重的工作来解耦您尝试完成的过程。这是一个例子:

        // listen for when files have been fetched and set up what to do when it comes in
        pubsub.notification.subscribe(
            "processFetchedResults", // notification to subscribe to
            "fetchedFilesProcesser", // subscriber
        
            /* what to do when files have been fetched */ 
            function(params) {
        
                var file_list = params.notificationParams.file_list;
        
                for (file in file_list) { // iteration, not enumeration
                var data = loadFile(file);
        
                if (!data) {
                    display('failed to load: ' + file);
                } else {
                    display(data);
                }
            }
        );    
        
        // trigger fetch files 
        function fetchFiles(source) {
        
           // ajax call to source
           // on response code 200 publish "processFetchedResults"
           // set publish parameters as ajax call response
           pubsub.notification.publish("processFetchedResults", ajaxResponse, "fetchFilesFunction");
        }
        

        当然,这在设置中非常冗长,而在幕后的魔力却很少。 以下是一些技术细节:

        1. 我正在使用setTimeout 来处理触发订阅。这样,它们以非阻塞方式运行。

        2. 调用与处理有效地分离。您可以为通知"processFetchedResults" 编写不同的订阅,并在响应通过后执行多项操作(例如记录和处理),同时将它们保存在非常独立、微小且易于管理的代码块中。

        3. 上面的代码示例没有解决回退问题或运行适当的检查。我确信它需要一些工具才能达到生产标准。只是想向您展示它的可能性以及您的解决方案如何独立于库。

        干杯!

        【讨论】:

          猜你喜欢
          • 2015-12-13
          • 1970-01-01
          • 2016-05-12
          • 2017-10-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-09-08
          相关资源
          最近更新 更多