Help us understand the problem. What is going on with this article?

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か所に書くことが出来る
toshihirock
こちらは個人の意見で会社とは関係ありません。お約束です。
http://toshihirock.blogspot.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした