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

11

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

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

如何处理多个相互依赖且/或分层的异步调用,而不深入到无尽的回调链中,以经典的“还原细长意大利面”方式?是否有一种被证明的范例可以干净地处理这些内容,同时保持代码松散耦合?


这两个异步函数的第二个参数是回调函数吗? - numbers1311407
你想并行加载所有文件吗?并分别显示数据/警告错误吗? - Bergi
1
请您能否就现有答案提供评论,说明它们为什么不可接受。反馈能帮助我们帮助您。 - Ben Felda
@BenFelda 我会进一步调查现有的答案,但当我开始悬赏时,只有jQuery答案可用。 - Confluence
@Bergi 这只是一个例子,我想要的是一种干净的方式来组织有大量异步调用的代码。 - Confluence
我想你可能想要使用事件。它是一种非常适用于松耦合异步代码的模型。 - Hank
6个回答

6

延迟对象是解决这个问题的最佳方法。它们准确地捕获了您(和大量异步代码)想要的内容:“去做这件可能很昂贵的事情,不要在此期间打扰我,然后在回来时执行此操作。”

而且您不需要使用jQuery来使用它们。一位有进取心的个人已经将Deferred移植到underscore中,并声称您甚至不需要使用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');
});

设置有点啰嗦,但一旦编写了处理延迟对象状态并将其放在某个函数中的代码,您就不必再担心了,您可以非常轻松地玩弄实际事件流。例如:
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!
});

需要注意的是,这将在几乎相同的时间安排每个loadFile(),从而导致并行下载...这取决于情况可能不是所期望的。 - Ja͢ck
延迟对象的美妙之处在于,您可以按时间或进度分阶段执行它们,甚至可以串行运行它们,而不必触及执行下载的代码或处理数据的代码。 - Eevee
但是要创建Promise对象,通常也需要执行Ajax调用,这就是我的意思。 - Ja͢ck

3

事件

使用事件可能是一个好主意,它可以避免创建代码树并解耦你的代码。

我已经使用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();

自定义事件和这种事件处理方式在几乎所有流行的 JavaScript 框架中都有实现。


2
听起来你需要使用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);
        }
      });
    }
  }
});

我还发现另一篇不错的文章,介绍了Deferred对象的几个使用场景。


请注意 - 您必须修改 "fetchFiles" 和 "loadFile",以便它们返回 jQuery Deferred 对象。例如,$.ajax就是这样做的。 - wes
你可以使用 donefail 分别处理成功和失败,而不是检查传递给 then 的参数。 - Eevee

2
如果你不想使用jQuery,那么你可以使用Web Worker与同步请求相结合。Web Worker在除了Internet Explorer 10之前的所有主要浏览器中都得到支持。
基本上,如果你不完全确定Web Worker是什么,可以将其视为浏览器在不影响主线程的情况下执行专业JavaScript的一种方式(注意:在单核CPU上,两个线程将交替运行。幸运的是,现在大多数计算机都配备有双核CPU)。通常,Web Worker用于复杂的计算或某些强烈的处理任务。只需记住,Web Worker中的任何代码都不能引用DOM,也不能引用未传递给它的任何全局数据结构。实际上,Web Worker独立于主线程运行。工人执行的任何代码都应该与您的JavaScript代码库分开,放在自己的JS文件中。此外,如果Web Worker需要特定的数据才能正常工作,则需要在启动它们时将该数据传递给它们。
另外一个值得注意的重要事项是,您需要使用任何JS库来加载文件,这些库都需要直接复制到工作线程将执行的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 Workers 结合使用同步请求的优缺点。

Stack Overflow - Web Workers and Synchronous Requests


1

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 的语法混合并行和串行执行)。

1

我在开发的一个Web应用程序中遇到了这个问题,以下是我如何解决它的方法(没有使用任何库)。

步骤1:编写了一个非常轻量级的发布订阅实现。没有什么花哨的东西。包括订阅,取消订阅,发布和日志记录。所有内容(带注释)共计93行JavaScript代码。gzip压缩前为2.7kb。

步骤2:通过让发布订阅实现来完成重活,将你尝试完成的过程解耦。以下是一个示例:

// 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. 上面的代码示例没有解决回退或运行适当的检查。我相信它需要一些工具才能达到生产标准。只是想向你展示它是多么容易,并且你的解决方案可以独立于库。

干杯!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接