23

注意:这个问题也在此处的 Q.js 邮件列表中交叉发布。


我遇到了多个异步操作的情况,我接受的答案指出,使用诸如 q.js 之类的库来使用 Promises 会更有益。

我确信要重构我的代码以使用 Promises,但由于代码很长,我已经修剪了不相关的部分并将关键部分导出到单独的 repo 中。

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

要求是我希望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?

4

2 回答 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,它包含两部分,apromiseresolve函数。由promise返回getSizeSettingsFromPage。基本上,返回一个承诺是一个函数说“我稍后会回复你”的一种方式。一旦函数完成它的任务(在这种情况下,一旦image.onload事件被触发),该resolve函数将用于解决承诺。这表明任何等待任务已完成的承诺。

这是一个更简单的例子:

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.promse) 并在 2 秒等待 ( ) 后解决该 Promise deferred.resolve。该then方法可以在 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
于 2012-11-28T06:02:54.620 回答
2

看起来您应该使用该Q.all函数创建一个对应于所有 getSizeSettings 承诺何时实现的主承诺。

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时,您知道您已经解决了所有承诺,因此您也可以让增量回调调用“真实”回调。

//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);
于 2012-11-28T05:00:48.133 回答