如何使用q.js承诺可以处理多个异步操作



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


我遇到过多个异步操作的情况,我接受的答案指出,使用像q.js这样的库使用Promises会更有益。

我确信重构我的代码以使用Promises,但由于代码很长,我已经修剪了不相关的部分,并将关键部分导出到一个单独的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

下面是我对如何使用Q.js的建议。关键是,任何时候你想异步地做某事,你都应该返回一个承诺,一旦任务完成,你就应该解决这个承诺。这允许函数的调用方侦听要完成的任务,然后执行其他操作。

和以前一样,我已经用// ***对我的更改进行了评论。如果你还有其他问题,请告诉我。

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,它包含两个部分:promiseresolve函数。promisegetSizeSettingsFromPage返回。基本上,返回promise是函数说"我稍后再给你回复"的一种方式。一旦函数完成了它的任务(在这种情况下,一旦image.onload事件触发),resolve函数就用于解析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.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

看起来应该使用Q.all函数来创建一个主promise,该主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库都应该提供类似的方法来进行这种同步。如果你遇到一个不这样做的,你可以做的是将每个单独的承诺挂接到一个回调,该回调在调用时增加共享计数器。当你的计数器达到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);

最新更新