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

时间:2013-01-18 21:14:25

标签: javascript asynchronous loose-coupling waterfall

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

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 方式深入研究无穷无尽的回调链?是否有一种经过验证的范例可以在保持代码松散耦合的同时干净地处理这些问题?

5 个答案:

答案 0 :(得分:6)

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

你不需要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 :(得分: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();

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

答案 2 :(得分: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);
        }
      });
    }
  }
});

我还发现了另一个decent post,它为Deferred对象提供了一些用例

答案 3 :(得分: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。)

答案 4 :(得分:1)

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

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

  4. 干杯!