问题 处理相互依赖和/或分层的异步调用


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

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() 实际上是异步的。简单的方法是将它们转换为同步函数。但是,如果浏览器锁定等待呼叫完成,这并不好。

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


11822
2018-01-18 21:14


起源

这两个异步函数是否将回调作为第二个参数? - numbers1311407
你想并行加载所有文件吗?并分别显示数据/警报错误? - Bergi
您能否就现有答案发表评论,说明为何不接受这些答案。反馈可以帮助我们。 - Ben Felda
@BenFelda我将进一步研究现有的答案,但是当我开始赏金时,只有jQuery答案可用。 - Confluence
@Bergi这只是一个例子,我想要的是用很多异步调用来构造代码的简洁方法。 - Confluence


答案:


延期真的是去这里的方式。它们准确地捕获了你(和许多异步代码)想要的东西:“走开去做这个可能很昂贵的东西,在此期间不要打扰我,然后在你回来时这样做。”

而且你不需要jQuery来使用它们。一个有进取心的人有 移植延迟到下划线并声称你甚至不需要使用下划线来使用它。

所以你的代码看起来像这样:

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!
});

6
2018-01-23 21:21



+1。虽然应该注意这将安排每个 loadFile() 几乎在同一时间,导致并行下载...根据情况可能不需要。 - Ja͢ck
延迟的优点在于,您可以按时间或进度错开它们,甚至可以连续运行它们,而无需触及执行下载的代码或处理数据的代码。 - Eevee
但是要创建promise对象,你通常也会执行Ajax调用就是我的意思。 - Ja͢ck


活动

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

我用过  作为事件的框架。

示例伪代码

// 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
2018-01-29 19:04





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

$.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);
        }
      });
    }
  }
});

我还发现了另一个 体面的职位 它给出了Deferred对象的一些用例


2
2018-01-18 21:52



一个注意事项 - 你必须修改“fetchFiles”和“loadFile”,以便它们返回一个jQuery Deferred对象。例如,$ .ajax就是这样做的 - wes
您可以使用 done 和 fail 单独处理成功和失败,而不是检查传递给的参数 then - Eevee


如果您不想使用jQuery,那么您可以使用的是Web worker和同步请求。除了10之前的任何Internet Explorer版本之外,每个主要浏览器都支持Web worker。

Web Worker浏览器兼容性

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

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

无论如何,我决定写一个基本模板来向你展示如何处理这个问题。请在下面查看。随意提问/批评/等。

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

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一起使用同步请求的优缺点。

堆栈溢出 - Web工作者和同步请求


2
2018-01-23 17:27





异步 是一个常用的异步流控制库,通常与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(如需求语法)在同一函数中混合并行和系列执行。


1
2018-01-23 18:08





我有一个我正在研究的webapp这个问题,这就是我如何解决它(没有库)。

步骤1:写了一个 非常轻量级的pubsub实现。没有什么花哨。订阅,取消订阅,发布和记录。一切(带注释)总共增加了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. 上面的代码示例不解决回退或运行正确的检查。我相信它需要一些工具才能达到生产标准。只是想向您展示它的可能性以及您的解决方案与库无关的方式。

干杯!


1
2018-01-29 16:30