JavaScript
Node.js
promise

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

More than 1 year has 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か所に書くことが出来る