如何使用q.js的Promise处理多个异步操作

23
注意:这个问题也在 Q.js 邮件列表上发布

我有一个涉及多个异步操作的情况,所接受的答案指出,使用 Promise、例如 q.js 库将更加有益。

我已经被说服改写我的代码来使用 Promise,但由于代码相当长,我已经剪裁了不相关的部分,并将关键部分导出到单独的 repo 中。

该 repo 在这里,最重要的文件是这个

要求是,我希望在遍历所有拖放文件后 pageSizes 不为空。

问题是,getSizeSettingsFromPage 函数内的 FileAPI 操作导致 getSizeSettingsFromPage 是异步的。

因此,我无法像这样放置 checkWhenReady();

function traverseFiles() {
  for (var i=0, l=pages.length; i<l; i++) {
    getSizeSettingsFromPage(pages[i], calculateRatio);   
  }
  checkWhenReady(); // this always returns 0.
}

这个方法可以使用,但并不是最理想的。我更喜欢在所有 pages 都成功进行了函数 calculateRatio 后,只调用一次 checkWhenReady 方法。

function calculateRatio(width, height, filename) {
  // .... code 
  pageSizes.add(filename, object);
  checkWhenReady(); // this works but it is not ideal. I prefer to call this method AFTER all the `pages` have undergone calculateRatio
  // ..... more code...
}

我该如何重构代码以利用 Q.js 中的 Promises?

2个回答

45

我建议使用Q.js使这个工作正常,以下是我的建议。关键是,每当您想要异步执行某些操作时,应返回一个Promise,并且一旦任务完成,您应该解决该Promise。这允许调用该函数的用户等待任务完成,然后执行其他操作。

与以前一样,我已经用 // *** 注释了我的更改内容。如果您有任何进一步的问题,请告诉我。

        function traverseFiles() {
            // *** Create an array to hold our promises
            var promises = [ ];
            for (var i=0, l=pages.length; i<l; i++) {
                // *** Store the promise returned by getSizeSettingsFromPage in a variable
                promise = getSizeSettingsFromPage(pages[i]);
                promise.then(function(values) {
                    var width = values[0],
                        height = values[1],
                        filename = values[2];
                    // *** When the promise is resolved, call calculateRatio
                    calculateRatio(width, height, filename);
                });
                // *** Add the promise returned by getSizeSettingsFromPage to the array
                promises.push(promise);
            }
            // *** Call checkWhenReady after all promises have been resolved
            Q.all(promises).then(checkWhenReady);
        }

        function getSizeSettingsFromPage(file) {
            // *** Create a Deferred
            var deferred = Q.defer();
            reader = new FileReader();
            reader.onload = function(evt) {
                var image = new Image();
                image.onload = function(evt) {
                    var width = this.width;
                    var height = this.height;
                    var filename = file.name;
                    // *** Resolve the Deferred
                    deferred.resolve([ width, height, filename ]);
                };
                image.src = evt.target.result;
            };
            reader.readAsDataURL(file);
            // *** Return a Promise
            return deferred.promise;
        }

编辑

defer 创建一个包含两部分的Deferred, 一个是promise, 另一个是resolve函数。 promise 是由getSizeSettingsFromPage返回的。基本上,返回一个promise是一个函数说“我稍后会回来”这种方式。一旦函数完成任务(在此例中,即image.onload事件已触发),则使用resolve函数来解析promise。这表示等待promise的任何内容都已完成任务。

以下是一个更简单的示例:

function addAsync(a, b) {
    var deferred = Q.defer();
    // Wait 2 seconds and then add a + b
    setTimeout(function() {
        deferred.resolve(a + b);
    }, 2000);
    return deferred.promise;
}

addAsync(3, 4).then(function(result) {
    console.log(result);
});
// logs 7 after 2 seconds

addAsync函数会在相加之前等待2秒钟,它返回一个异步的promise(deferred.promise),并在等待2秒后解决该promise(deferred.resolve)。可以在promise上调用then方法,并传递一个回调函数以在promise被解决后执行。该回调函数将传入promise的解决值。

在您的情况中,我们有一个promise数组,并且需要等待所有promise完成后再执行某个函数,因此我们使用了Q.all。这是一个例子:

function addAsync(a, b) {
    var deferred = Q.defer();
    // Wait 2 seconds and then add a + b
    setTimeout(function() {
        deferred.resolve(a + b);
    }, 2000);
    return deferred.promise;
}

Q.all([
    addAsync(1, 1),
    addAsync(2, 2),
    addAsync(3, 3)
]).spread(function(result1, result2, result3) {
    console.log(result1, result2, result3);
});
// logs "2 4 6" after approximately 2 seconds

1
我已编辑了我的答案,试图解释 defer 的原因,以及其他一些事情。干杯 :) - Nathan Wall
1
谢谢kimisia。我刚刚更新了原始代码,使用由getSizeSettingsFromPage返回的Promise去调用calculateRatio而无需传递whenReady参数。只要您在使用Promise,为什么不全都用呢:-)。Promises消除了那些回调函数的需要。 - Nathan Wall
只是想补充一下,我回到了我的原始代码并重构它以包含 Q.js Promises 代码。结果很好。谢谢。 - Kim Stacks
哦,谢谢你,Nathan!说真的,我对回调函数没有直观的理解,所以带有“then”关键字的Promise让我更容易阅读代码。 - Kim Stacks
Q.all([ addAsync(1, 1), addAsync(2, 2), addAsync(3, 3) ]).spread(function(result1, result2, result3) { console.log(result1, result2, result3); }); - clevertension
显示剩余5条评论

2

看起来您应该使用Q.all函数创建一个主Promise,对应于所有getSizeSettings Promise都已完成的情况。

https://github.com/kriskowal/q#combination

var ps = [];
for (var i=0, l=pages.length; i<l; i++) {
   ps[i] = getSizeSettingsFromPage(pages[i], calculateRatio);   
}

Q.all(ps).then(function(){ callWhenReady() })

大多数 Promise 库应该提供类似的方法来进行这种同步。如果您遇到一个没有提供此功能的库,您可以将每个单独的 Promise 钩子连接到一个回调函数,当其被调用时增加共享计数器。当计数器达到 n 时,您就知道已经解决了所有 Promise,因此可以让增量器回调函数也调用“真正”的回调函数。

//If you did not have Q.all available
//Or had to code this without a promise library

var to_go = pages.length;
for (var i=0, l=pages.length; i<l; i++) {
   getSizeSettingsFromPage(pages[i], calculateRatio)
   .then(function(){
       to_go--;
       if(to_go == 0){
           callWhenReady()
       }
   });
}

请注意,目前在这些情况下,异步调用可以并行运行。如果您需要它们顺序运行,则通常唯一的方法是将for循环重写为递归函数。
var go = function(i){
    if(i>=pages.length){
        return call_next_step()
    }else{
        return do_ith_calculation(i)
        .then(function(){
            return go(i+1)
        })
    }
};
go(0);

我尝试了https://github.com/simkimsia/learn-promises-javascript/commit/69db89b95bfd87437c30c76f763199dc53530fad,使用了你的第二个解决方案。但是当我拖放文件时,代码就崩溃了。我在控制台上看不出任何问题。 - Kim Stacks
我也尝试了Q.all https://github.com/simkimsia/learn-promises-javascript/commit/5084edc25a68b378be1496292f645812776dd7ce 代码可以成功运行,但返回的结果仍然是0。 - Kim Stacks
我没有尝试递归函数,因为我对它没有直观的理解。 - Kim Stacks
谢谢回答,missingno。我通过Nathan的回答解决了问题!干杯 :) - Kim Stacks
@kimsia:递归循环只在您想要将异步内容链接在彼此之后时才需要。第二个解决方案是Q.all在底层实现的方式 - 只有在没有Q.all可用并且需要自己编码类似内容时才需要它。 - hugomg

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