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

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

4

6 回答 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!
});
于 2013-01-23T21:21:12.683 回答
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 框架都实现了自定义事件和这种事件处理。

于 2013-01-29T19:04:04.807 回答
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 对象的一些用例

于 2013-01-18T21:52:52.730 回答
2

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

Web Worker 浏览器兼容性

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

另一个值得注意的重要事情是,您需要用于加载文件的任何 JS 库都需要直接复制到 worker 将执行的 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 结合使用的优缺点。

Stack Overflow - Web Worker 和同步请求

于 2013-01-23T17:27:36.943 回答
1

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

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

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

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 的需求语法在同一个函数中混合并行和串行执行)。

于 2013-01-23T18:08:57.890 回答
1

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

干杯!

于 2013-01-29T16:30:09.870 回答