Edited at

Promiseについて0から勉強してみた

More than 3 years have passed since last update.

ES6を使う機会がありそうで、Promiseについて全然知らなかったので、実際に書きながら勉強してみたときのメモ。

なお、以下を参考にさせて頂きました。

0から勉強する時でもとても分かりやすかったです。

JavaScript Promiseの本


環境


  • Node.js v4.2.2


Promiseとは


  • 非同期処理を操作できる

  • 非同期処理の成功時(resolve)、失敗時(reject)の処理を明示的に書くことが出来る

  • 非同期処理を平行・直列に実行させることが出来る


とりあえずやってみる

とりあえず、参考にさせて頂いたサイトのコードを書いてみました。


promise-workflow.js

function asyncFunction() {

// Promiseオブジェクトを返却する.処理成功時にはresolveが呼ばれる
return new Promise(function (resolve, reject) {
setTimeout(function () {
// 成功
resolve('Async Hello world');
}, 16);
});
}

asyncFunction().then(function (value) {
// 非同期処理成功
console.log(value); // => 'Async Hello world'
}).catch(function (error) {
// 非同期処理失敗。呼ばれない
console.log(error);
});


上記では非同期で実行されるsetTimeoutメソッドをPromiseを使って実行しています。

resolveメソッドを呼びだす事で非同期処理の成功を示し、thenメソッドの引数の処理を実行しています。

以下はにreject関数を呼び出すことで非同期処理の失敗を示し、catchメソッドの引数の処理を実行しています。


promise-workflow.js

function asyncFunction() {

return new Promise(function (resolve, reject) {
setTimeout(function () {
// 失敗
reject(new Error('Error'));
});
});
}

asyncFunction().then(function (value) {
console.log(value);
}).catch(function (error) {
// 呼ばれる
console.log(error); // => 'Error'
});



メソッドチェーンについて理解する


chain.js

function taskA () {

console.log("TaskA");
}

function taskB () {
console.log("TaskB");
}

function onRejected(error) {
console.log("error = " + error);
}

var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejectted);


上記を実行するとtaskA,taskBと順番に標準出力が表示されます。


  • var promiseのresolve()が実行され

  • then(taskA)が実行され

  • then(taskB)が実行された

という流れだと思います。

なお、上記のように記載した場合、taskAもしくはtaskBのいずれかでエラーが発生した場合にonRejectedメソッドが呼び出されます。

エラーが発生したとは具体的に以下のいずれかの場合となります。


  • 例外(thrown new Error)が発生した

  • rejectが呼び出された


chain.js

function taskA () {

throw new Error('Error');
console.log("TaskA");
}

function taskB () {
console.log("TaskB");
}

function onRejectted(error) {
console.log("error = " + error);
}

var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejectted);


上記実行するとerror = Error: Errorのみ標準出力されます。

taskBの関数については呼び出されません。


非同期処理を並列で行う

一つの非同期処理をPromiseにしても嬉しくないので、並行で非同期処理を行う方法を確認します。

(taskAが終わってtaskBをやって。。。というような逐次処理は次の章で)


Promise.all()

Promise.all()メソッドはPromiseオブジェクトの配列を受け取り、全てのPromiseオブジェクトがresolveされたタイミングでthenが呼び出されます。

書いて確認してみましょう。


promise-all.js

var taskA = new Promise(function(resolve, reject) {

setTimeout(function () {
console.log('taskA');
resolve();
}, 16);
});

var taskB = new Promise(function(resolve, reject) {
setTimeout(function () {
console.log('taskB');
resolve();
}, 10);
});

var before = new Date();
Promise.all([taskA, taskB]).then(function () {
var after = new Date();
var result = after.getTime() - before.getTime();
console.log(result);
});


上記の結果は以下のようになります。

taskB

taskA
16

上記より以下であることが分かります。


  • taskAとtaskBは並行で実行されている(直列で実行されている場合には差分の秒数として26が表示されるはず)

  • taskAとtaskBが終わってからthenが呼び出されている

ちなみにrejectを返却するとどうなるのかやってみます。


hoge.js

var taskA = new Promise(function(resolve, reject) {

setTimeout(function () {
console.log('taskA');
resolve();
}, 16);
});

var taskB = new Promise(function(resolve, reject) {
setTimeout(function () {
console.log('taskB');
reject();
}, 10);
});

var before = new Date();
Promise.all([taskA, taskB]).then(function () {
var after = new Date();
var result = after.getTime() - before.getTime();
console.log(result);
}).catch(function () {
console.log('error');
});


結果は以下です。

taskB

error
taskA

上記より以下であることが分かります。


  • 配列に指定されたいずれかのPromiseでrejectが呼び出されたタイミングでcatchメソッドが呼びされ、実行される

  • catchメソッドが呼びされても終了していないPromiseの処理は継続される

  • 全てのPromiseの処理が終わってもthenメソッドは呼び出されない

Promiseのいずれかでもエラーになった時点で他のPromiseの処理を待たずに終了させたい場合にはprocess.exit(1)など使えば良さそうですね。


Promise.race()

Promise.all()は全てのPromiseが呼び出された後にthenメソッドが呼び出されました。(catchではなく)

Promise.race()は一つでもresolve, rejectが呼び出されたら、thenもしくはcatchが呼びされます。

確認します。


hoge.js

var taskA = new Promise(function(resolve, reject) {

setTimeout(function () {
console.log('taskA');
resolve();
}, 16);
});

var taskB = new Promise(function(resolve, reject) {
setTimeout(function () {
console.log('taskB');
resolve();
}, 1);
});

var before = new Date();
Promise.race([taskA, taskB]).then(function () {
var after = new Date();
var result = after.getTime() - before.getTime();
console.log(result);
}).catch(function () {
console.log('error');
});


結果

taskB

5
taskA

1という表示を期待したのですが、msなので多少は仕方なさそうですが、以下の結果が分かりました。


  • いずれかのPromiseのresolveが呼びされたタイミングでthenメソッドが実行される

  • thenメソッドが呼びされた後もまだ終了していないPromiseの処理は継続

こちらもrejectが呼び出された場合について確認します。


hoge.js

var taskA = new Promise(function(resolve, reject) {

setTimeout(function () {
console.log('taskA');
resolve();
}, 16);
});

var taskB = new Promise(function(resolve, reject) {
setTimeout(function () {
console.log('taskB');
reject();
}, 1);
});

var before = new Date();
Promise.race([taskA, taskB]).then(function () {
var after = new Date();
var result = after.getTime() - before.getTime();
console.log(result);
}).catch(function () {
console.log('error');
});


結果は以下。

Promise.all()と同じ挙動ですね。

taskB

error
taskA

参考ページにも書いてありましたが、Promiseには処理を途中でキャンセルというのはないみたいですね。


非同期処理を逐次処理で実行する

Promise.all()及びPromise.race()は並行で非同期処理を制御できるメソッドでした。

次に逐次処理(直列処理)について確認します。

まずは、参考サイトを参考にしてシンプルに書いてみます。

ECMAScript6のアロー関数とPromiseまとめ - JavaScript


hoge.js

Promise.resolve()

.then(function () {
return new Promise(function(resolve, reject) {
setTimeout(function () {
console.log('taskA');
resolve('taskA death');
}, 16);
});
})
.then(function(value) {
return new Promise(function(resolve, reject) {
setTimeout(function () {
console.log(value);
console.log('taskB');
resolve('taskB death');
}, 1);
});
})
.then(function (value) {
console.log('then');
console.log(value);
}).catch(function (error) {
console.log(error);
});

シンプルですね。

下記参照させて頂いたページを参考に並列処理の際に利用した関数を逐次処理させます。こちらは関数に分かれているのでテストもしやすそうです。

4.8. Promiseによる逐次処理


hoge.js

function sequenceTasks(tasks) {

function recordValue(results, value) {
results.push(value);
return results;
}
var pushValue = recordValue.bind(null, []);
return tasks.reduce(function (promise, task) {
return promise.then(task).then(pushValue);
}, Promise.resolve());
}

var promises = {
doTaskA: function() {
return taskA().then();
},
doTaskB: function() {
return taskB(value).then();
}
};

function taskA() {
return new Promise(function(resolve, reject) {
setTimeout(function () {
console.log('taskA');
resolve('taskA death');
}, 16);
});
}

function taskB(value) {
return new Promise(function(resolve, reject) {
setTimeout(function () {
console.log(value);
console.log('taskB');
resolve('taskB death');
}, 1);
});
}

function main() {
return sequenceTasks([promises.doTaskA, promises.doTaskB]);
}

main().then(function(value) {
console.log('then');
console.log(value);
// taskAもしくはtaskBでエラーの場合に呼び出される
}).catch(function(error) {
console.log(error);
});


sequenceTasksメソッドを使うとPromiseでの実行結果がそれぞれpushされるので最終的に後から結果の取得を行うこともできます。

taskA

[ 'taskA death' ]
taskB
then
[ 'taskA death', 'taskB death' ]

以下が確認できました。


  • 逐次(直列)で非同期処理を行える

  • 1つ目の非同期処理の結果を2つ目の非同期処理に渡すことが出来る

  • taskAもしくはtaskBでエラーがあった場合の処理を1か所に書くことが出来る