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

【小ネタ】JavaScriptのPromiseに関する実行順序を確認する

はじめに

Promiseオブジェクト生成時に渡す関数(executor)っていつ実行されるんだっけ、とか、executor完了後に「.then()」「.error()」でコールバック関数を登録した場合ってどうなるんだっけ、という点が個人的に曖昧だったので整理しました。
関連情報、および確認用のコードをまとめます。

Promise自体の説明については、以下のドキュメントが丁寧で分かりやすいと思います。

JavaScript Promiseの本
https://azu.github.io/promises-book/

公式はこちら。

Promise - JavaScript | MDN
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

Promiseの基本形/用語

Promiseに関わる要素のうち、明示的な呼称が無いものが存在します。
本稿では各要素について下記のように呼称することとします。

Promiseの基本形
// Promiseオブジェクト生成関数を定義する
const createPromise = () => {
  return new Promise((resolve, reject) => { // 本稿ではこの関数を executor と呼ぶ。
    // executor は2つの引数を持ち、その内容はいずれも関数。
    // 本稿ではそれぞれそのまま resolve(), reject() と呼ぶ。
    // 
    // executor 内の処理が正常終了した場合は第1引数である resolve() を、異常終了した場合は第2引数である reject() を実行するよう実装する。

    try {
      // なにか処理を実行する
      // ...
      // ...

      // executor 内の処理が正常終了したときに以下のように実行
      // ここで渡した引数は、Promiseオブジェクトの「.then()」に渡す関数内から、第1引数として参照できるようになる
      resolve('This executor is resolved.');
    } catch (e) {
      // executor 内の処理が異常終了したときに以下のように実行
      // ここで渡した引数は、Promiseオブジェクトの「.catch()」に渡す関数内から、第1引数として参照できるようになる
      reject('This executor is rejected.');
    }
  });
};


// Promiseオブジェクトを生成し、実行する
const promiseObject = createPromise();

// executor 実行完了後の処理をハンドリングする
promiseObject
  .then((resolvedValue) => { // 本稿ではこの関数を onFullFilled関数 と呼ぶ。
    // executor の処理が正常終了している場合、この関数内の処理が実行される。
    // この関数の第1引数(この例ではresolvedValue)には、executor 内で実行された resolve() に渡された値が渡される。
    // つまりこの例では 'This executor is resolved.' が入っている。

  })
  .catch((rejectedValue) => { // 本稿ではこの関数を onRejected関数 と呼ぶ。
    // executor の処理が異常終了している場合、この関数内の処理が実行される。
    // この関数の第1引数(この例ではrejectedValue)には、executor 内で実行された reject() に渡された値が渡される。
    // つまりこの例では 'This executor is rejected.' が入っている。

  });

上記コードから用語を抜き出すと下記のようになります。

用語 説明/内容 備考
Promiseオブジェクト生成関数 上記コードの createPromise() 関数。
executor Promise生成時に第1引数として渡す関数。 MDNドキュメント上も executor と呼ばれている。
resolve() executorの第1引数。通常、executorが正常終了した場合に実行されるよう実装する。 MDNドキュメント上は resolutionFunc と呼ばれている。
reject() executorの第2引数。通常、executorが異常終了した場合に実行されるよう実装する。 MDNドキュメント上は rejectionFunc と呼ばれている。
Promiseオブジェクト 上記コードの promiseObject が該当。
onFullFilled関数 Promiseオブジェクトの .then() メソッドの第1引数として渡される関数。 executorが正常終了している場合、つまりresolve()の実行が完了したのちに実行される処理が渡される。
onRejected関数 Promiseオブジェクトの .catch() メソッドの第1引数として渡される関数。 executorが異常終了している場合、つまりreject()の実行が完了したのちに実行される処理が渡される。

確認したかったこと

  • Promise生成時に渡す関数(executor)はいつ実行されるか
    • Promiseオブジェクトを生成した時点で実行される
  • Promiseの「.then()」「.error()」メソッドによる、onFullFilled関数/onRejected関数の登録が完了する前に、executorの実行が完了した場合、どうなるか
    • executorは特に問題なくそのまま完了し、PromiseオブジェクトはSettled(FullFilled / Rejectedのいずれか)の状態になる。
    • Promiseオブジェクトには、executor内のコールバック関数(つまりresolve() / reject())に渡された値が保持される。
    • 「.then()」「.error()」メソッドにてonFullfilled関数/onRejected関数が登録されたのち、登録されたコールバック関数は非同期で即座に実行される。
    • onFullFilled関数/onRejected関数は、Promiseオブジェクトに紐づけられた値を参照しながら、特に問題なく処理を実行し完了する。

確認した事項

Promise生成時に渡す関数(executor)はいつ実行されるか

executorはPromiseオブジェクト生成中に実行されるとのことです。
ということは、Promiseオブジェクトの生成が完了したときには、すでにexecutorは実行を開始しているということです。

Constructor Syntax
var promiseObj = new Promise(executor);
Arguments
executor
A function to be executed by the constructor, during the process of constructing the promiseObj. The executor is custom code that ties an outcome to a promise. You, the programmer, write the executor. The signature of this function is expected to be:

Promise - JavaScript | MDN
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#Constructor_Syntax

onFullFilled関数/onRejected関数がいつ実行されるか

「.then()」「.error()」で登録されたコールバック関数は、登録直後に非同期で実行される。

JavaScript Promiseの本 - 2.3. コラム: Promiseは常に非同期?
https://azu.github.io/promises-book/#promise-is-always-async

onFullFilled関数/onRejected関数の登録前に、executorの実行が完了した場合、どうなるか

端的に説明した記述が見当たらなかったので、次のサンプルコードで確認しました。
結論としては、executor内でresolve()若しくはreject()を実行した際の引数を、onFullFilled関数/onRejected関数内から参照することができ、問題なく処理が完了します。

確認用サンプルコード

コード

以下のようなサンプルコードで確認しました。

確認用サンプルコード
// Promise オブジェクトを返却する関数 createPromise を作成する
const createPromise = () => {
  return new Promise((resolve, reject) => {
    // executor 実行時の時刻を出力する。
    console.log(
      "2. At the top of the executor() of a Promise object. at " + Date.now()
    );

    // 後続の処理を実行し、最後にresolve()を実行する。
    // 一般的なユースケースとあわせるため、100ミリ秒後に後続の処理が完了することとした。
    // ただし、Promise.then() / .error() によるコールバック関数登録前に完了する。
    setTimeout(() => {
      console.log(
        "5. At the function called delayed within the executor(). at " + Date.now()
      );

      // executor内の関数でresolve()が実行された時刻が分かるよう、UNIXエポックミリ秒を引数に渡す。
      resolve({message: "The executor has finished.", unixtime: Date.now(),});

      // reject()実行時の動作確認用。
      // reject({message: "The executor has failed.", unixtime: Date.now(),});

    }, 100);
  });
};

// createPromise 関数を実行し、Promiseオブジェクトを生成する。
console.log("1. Create a Promise object. at " + Date.now());
let promiseObject = createPromise();
console.log("3. A Promise object has been created. at " + Date.now());

// Promise.then() / .error() によりコールバック関数を登録する。
// コールバック関数の登録は、400ミリ秒後に完了する
setTimeout(() => {
  console.log("6. Registering the onFullFilled and onRejected callback functions has been started. at " + Date.now());
  promiseObject
    .then((resolvedValue) => {  // onFullFilled関数
      console.log(
        "7. The onFullFilled function has finished. " + JSON.stringify(resolvedValue) + " at " + Date.now()
      );
    })
    .catch((rejectedError) => {  // onRejected関数
      console.log(
        "7. The onRejected function has finished. " + JSON.stringify(rejectedError) + " at " + Date.now()
      );
    });
}, 400);

console.log("4. At the end of this program. at " + Date.now());

実行結果

実行結果は下記のとおりです。
処理開始から102ミリ秒後にexecutor関数が完了しています。(ここで言う「完了」とは、resolve()若しくはreject()が実行されたことを指します。)
executor内で最終的にresolve()が実行されたのは103ミリ秒経過後であることが、7行目のログから分かります。

その後、処理開始から402ミリ秒後、「.then()」及び「.error()」メソッドによるonFullFilled関数/onRejected関数の登録が行われています。
その後、登録されたonFullFilled関数も実行され、これは処理開始から402ミリ秒後であることが分かります。

1. Create a Promise object. at 1589122444638                              // Promiseオブジェクト生成開始
2. At the top of the executor() of a Promise object. at 1589122444639     // executor実行開始
3. A Promise object has been created. at 1589122444639                    // Promiseオブジェクト生成完了
4. At the end of this program. at 1589122444639                           // コードの最終行に到達
5. At the function called delayed within the executor(). at 1589122444740 // executor内の処理がすべて完了。開始から102ミリ秒経過後
6. Registering the onFullFilled and onRejected callback functions has been started. at 1589122445040 // onFullFilled関数/onRejected関数の登録開始。開始から402ミリ秒経過後
7. The onFullFilled function has finished. {'message':'The executor has finished.','unixtime':1589122444741} at 1589122445040 // onFullFilled関数の実行完了。実行開始から402ミリ秒後に完了。executor内のresolve()を実行したのは開始から103ミリ秒経過後であることが併せてわかる。
tmiki
Tech Fun株式会社( http://www.techfun.co.jp/ )所属のエンジニア。10年ぐらいインフラエンジニア、ここ数年はアプリエンジニア。 好きな言語はDartとゲール語とサンスクリット語、好きなAWSサービスはIAM/STSです。 本サイトで投稿する内容は個人の見解に基づくものであり、所属組織の意向に関わるものではありません。
techfun
Tech FunはITの力で世界を豊かにする総合サービス企業です。 IT研修スクール「TechFun.jp(https://techfun.jp/)」、eラーニングプラットフォーム「StudySmile(https://studysmile.com/)」のほか、ミャンマーオフショア開発、スマートフォンアプリ開発、Webシステム開発、SIサービスを展開しています。
https://www.techfun.co.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
ユーザーは見つかりませんでした