注:この質問は、Q.jsメーリングリストにも here でクロスポストされています。
複数の非同期操作がある状況があり、 回答を受け入れました は、q.jsなどのライブラリを使用してPromiseを使用する方が有益であると指摘しました。
Promiseを使用するようにコードをリファクタリングすることは確信していますが、コードが非常に長いため、関係のない部分をトリミングし、重要な部分を別のリポジトリにエクスポートしました。
リポジトリは here で、最も重要なファイルは 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
がこの関数computeRatioを正常に完了した後、checkWhenReadyを1回だけ呼び出すことを好みます。
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でPromiseを使用するためにコードをリファクタリングするにはどうすればよいですか?
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
は、2つの部分、promise
とresolve
関数を含むDeferredを作成します。 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つの数値を追加しますが、それらを追加する前に2秒待機します。非同期であるため、promise(deferred.promse
)2秒待ってから約束を解決します(deferred.resolve
)。 then
メソッドはpromiseで呼び出され、promiseが解決された後に実行されるコールバック関数を渡すことができます。コールバック関数は、promiseの解決値で渡されます。
あなたの場合、約束の配列があり、関数を実行する前にそれらのallが完了するのを待つ必要があったので、 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
関数を使用して、すべての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ライブラリは、この種の同期を行うための同様の方法を提供する必要があります。あなたができることを見つけられない場合、コールされたときに共有カウンターをインクリメントするコールバックに個々のプロミスをフックすることです。カウンターが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);