0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Promiseをシミュレートして理解する

Posted at

Promiseとは

MDN曰く「プロミス (Promise) は、非同期処理の最終的な完了もしくは失敗を表すオブジェクトです。
正直、何言ってるのか理解できなかったです。
でも、ChatGPTさんと問答してテストコードを弄りまくった結果、なんとか理解したと思います。
Promiseの解説記事は巷に溢れていますが、使い方が主で、 Promise とは、いったいどんな物であるかという解説はほとんど見かけません。そこで、ChatGPTさんに「Promiseオブジェクトの実態はどんなものですか?」という質問をしたところPromiseをシミュレートしたコードを表示してくれました。
問答とテストを繰り返し最終的にできたコードは以下のようなものです。

MyPromise.js

class MyPromise {
    constructor(executor) {
        this.state = "pending";
        this.value = undefined;
        this.handlers = [];
        this.catchHandlers = [];

        const resolve = (value) => {
            if (this.state !== "pending") return;
            this.state = "fulfilled";
            this.value = value;
            this.handlers.forEach(handler => queueMicrotask(() => handler(value)));
        };

        const reject = (error) => {
            if (this.state !== "pending") return;
            this.state = "rejected";
            this.value = error;
            if (this.catchHandlers.length > 0) {
                this.catchHandlers.forEach(handler => queueMicrotask(() => handler(error)));
            } else {
                // もし catch がない場合でも、次の then で処理できるようにする
                this.handlers.forEach(handler => queueMicrotask(() => handler(undefined, error)));
            }
        };

        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }

    then(onFulfilled, onRejected) {
        return new MyPromise((resolve, reject) => {
            const handler = (value, error) => {
                if (error) {
                    if (onRejected) {
                        try {
                            const result = onRejected(error);
                            resolve(result);
                        } catch (e) {
                            reject(e);
                        }
                    } else {
                        reject(error); // エラーハンドリングされていない場合は次の catch に渡す
                    }
                    return;
                }

                try {
                    const result = onFulfilled(value);
                    resolve(result);
                } catch (e) {
                    reject(e);
                }
            };

            this.handlers.push(handler);

            if (this.state === "fulfilled") {
                queueMicrotask(() => handler(this.value));
            } else if (this.state === "rejected") {
                queueMicrotask(() => handler(undefined, this.value));
            }
        });
    }

    catch(onRejected) {
        return new MyPromise((resolve, reject) => {
            const handler = (error) => {
                try {
                    const result = onRejected(error);
                    resolve(result);
                } catch (e) {
                    reject(e);
                }
            };

            this.catchHandlers.push(handler);

            if (this.state === "rejected") {
                queueMicrotask(() => handler(this.value));
            }
        });
    }
}

とてもよくできたコードでPromiseの基本的な動作を問題なくシミュレートしてくれます。でも、このままだと理解が難しいので、簡略化したコードを次に示します。

class MyPromise {
    constructor(executor) {
        this.state = "pending";
        this.value = undefined;
        this.handlers = [];

        const resolve = (value) => {
            if (this.state !== "pending") return;
            
            this.state = "fulfilled";
            this.value = value;            
            this.handlers.forEach((handler) => {
                queueMicrotask(()=>{handler(value)})
                });
        };

        const reject = (error) => {
            if (this.state !== "pending") return;
            this.state = "rejected";
            this.value = error;
            this.handlers.forEach((handler) => {
                queueMicrotask(()=>{handler(value)})
                });
        };

        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }

    then(onFulfilled) {
        return new MyPromise((resolve_p) => {
            this.handlers.push((value_p) => {
                const result = onFulfilled(value_p);
                resolve_p(result);
            });
        });
    }
}

rejectの部分のコードは正しくないので、rejectという関数が定義されていると思ってください。

promiseはPromiseクラスのインスタンス

Promiseに基本的な使い方は

new MyPromise((resolve)=>{
    setTimeout(()=>{
        resolve('タイマー終了');
    },1000);
}).then((value)=>{
    console.log(value);
});

と使いますので、PromiseクラスにnewしてPromiseのインスタンスを作ることで利用します。
すなわち、promiseはインスタンスオブジェクトなわけです。
オブジェクトなのに、なんらかの処理を実行しているというのはなんとなく釈然としないものがありますが、それは次の仕組みによります。

promiseはnewで即座に実行される

promiseを初めてみた時、new Promiseとインスタンスを作るだけで処理が実行されるのが不思議でした。でも、MyPromiseのコードを見れば明白です。
MyPromiseのコンストラクタはexecutorという関数を引数にとり、コンストラクタの最後で、その関数を実行します。(tryのところ)
executorは、Promiseをnewする時に渡した関数です。上記の例であれば

(resolve)=>{
    setTimeout(()=>{
        resolve('タイマー終了');
    },1000);

です。
ですので、new Promiseをするとコンストラクタが実行され、渡された関数が即座に実行されるので、new Promiseだけで実行が開始されるのです。
渡された関数はこの例ではsetTimeoutを使った非同期の関数ですが、通常の同期的な関数でも、即座にresolveするような関数でも構いません。
promiseの非同期的な動作は、ここではなく、後述するようにresolveの処理の中で行われます。

executorの引数は関数

executorを実行する時の引数はresolverejectです。
この2つの関数はPromiseクラスのコンストラクタの中で宣言されています。
上記の例であれば、executor

(resolve)=>{
    setTimeout(()=>{
        resolve('タイマー終了');
    },1000);

ですので、タイマーが1秒経過に実行されるコールバック関数

()=>{
        resolve('タイマー終了');
    }

の中のresolve()はpromiseインスタンスの中のresolveメソッドです。
理由は、resolve()は、外側のexcuterの引数なので、レキシカルスコープ により内側のコールバック関数内のresolve()は、この引数のresolveになります。

const resolve = (value) => {
            if (this.state !== "pending") return;
            
            this.state = "fulfilled";
            this.value = value;            
            this.handlers.forEach((handler) => {
                queueMicrotask(()=>{handler(value)})
                });
        };

resolve()は基本的には、excuterの最後に実行されるメソッドです。

resolveはthenで登録された関数を実行する

resolve()の中身をみていきましょう。

resolve()の中のthisは、new Promiseでできたインスタンスオブジェクトですね。
最初の処理は、promiseの状態(state)をチェックして保留中(pending)であれば成功(fulfilled)にします。
保留中(pending)以外の状態では状態は変化しないようにreturnして、続く処理はパスされます。

次は、resolveの引数のvalueをプロパティに保存します。excuterからの返り値は、このようにreturnではなくresolveを実行することで返します。

次に、一番大事なことをします。

this.handlers.forEach((handler) => {
    queueMicrotask(()=>{handler(value)})
});

hanlers配列に登録されたハンドラー(関数)を先ほど記録したvalueを引数に実行するようにしたアロー関数を作って、マイクロタスクキューに登録します。
マイクロタスクキューに登録されたこの関数は、このresolveの直後に実行が開始されます。(マイクロタスクキューに他のタスクが登録されていなければ)
マイクロタスクキューに関数が登録されるので、resolve実行のあとは一旦実行が途切れて、強制的に非同期処理となります。

このhanlers配列に登録されたハンドラー(関数)は、実はthenによって登録されたコールバック関数を実行するための関数です。(コールバック関数そのものではありません。)

promiseが実行され、解決された際にthenの後のコードが実行されるのは、この仕組みによります。

thenはPromiseクラスのメソッド

プロミスの記述は

new Promise(...)
.then(...)
.then(...)
.catch(...)

という感じに、一見するとif 〜 else 〜のような制御構文のように見えますが、実態はPromiseクラスのメソッドチェーンです。
new Promiseやthenメソッドが新しいpromiseを返すので、このようなメソッドチェーンが成立します。次のように書き換えると構造がわかりやすくなります。

const p1 = new Promise(...);
const p2 = p1.then(...);
const p3 = p2.then(...);
const P4 = p3.catch(...);

thenメソッドは

then(onFulfilled) {
        return new MyPromise((resolve_p) => {
            this.handlers.push((value_p) => {
                const result = onFulfilled(value_p);
                resolve_p(result);
            });
        });
    }

onFulFilledという関数を引数にとるメソッドです。

thenは次に実行する関数を登録する

onFulFilled関数は、

new MyPromise((resolve)=>{
    setTimeout(()=>{
        resolve('タイマー終了');
    },1000);
}).then((value)=>{
    console.log(value);
});

このようなコードであれば、

(value)=>{console.log(value);}

です。
promiseが解決後に実行される関数です。
thenメソッドは、このonFulFilled関数を

(value_p) => {
    const result = onFulfilled(value_p);
    resolve_p(result);
}

onFulFilled関数実行後にresolve_p関数を実行する関数に包んで、this.handlers配列に登録します。

this.handlers.push(
+    (value_p) => {
+        const result = onFulfilled(value_p);
+        resolve_p(result);
+    }
);

this.handlers配列は、先に説明したresultメソッドの中で、マイクロタスクキューに登録する関数を収めたものですので、キューに登録するのは、このonFulFilled関数を包んだ関数ということになります。
この配列のハンドラーを登録する処理は

(resolve_p) => {
+    this.handlers.push(
+        (value_p) => {
+            const result = onFulfilled(value_p);
+            resolve_p(result);
+        }
+    );
}

もう一つの関数に包みます。
マイクロタスクキューに登録した関数の中のresolve_p関数は、この包んだ関数の引数です。

return new MyPromise(
+    (resolve_p) => {
+        this.handlers.push(
+            (value_p) => {
+                const result = onFulfilled(value_p);
+                resolve_p(result);
+            }
+        );
+    }
);

thenメソッドは、この関数をexecutorとした新しいpromiseインスタンスを生成してthenメソッドの返り値として戻します。
executorはpromiseインスタンスの生成と同時に実行されますので、結果として、thenの後に実行される処理がpromiseに中の次に実行すべきリスト(handlers配列)に登録されることになります。

thenメソッドチェーン(promiseチェーン)

then(onFulfilled) {
    return new MyPromise(
        (resolve_p) => {
            this.handlers.push(
                (value_p) => {
                    const result = onFulfilled(value_p);
                    resolve_p(result);
                }
            );
        }
    );
}

thenメソッドはこのようなコードになってます。
handlers配列はthis.handlersとなってます。
この時のthisは、レキシカルスコープですので、thisで新しく作られたpromiseインスタンスではなく、thenが実行されている最初の(ベースとなる)promiseインスタンスです。
一方、promiseが解決された時に実行されるonFulfilled関数を実行したで実行されるresolve_p(result)関数は、thenで新しく生成されたpromiseインスタンスのresove関数です。
今のpromiseのthenの関数の実行 → 新しいpromiseのresoveの実行 と進むため、

new Promise(fx1)
.then(fx2)
.then(fx3)
.then(fx4)

というメソッドチェーンを説明のために分解して

const p1 = new Promise(fx1);
const p2 = p1.then(fx2);
const p3 = p2.then(fx3);
const p4 = p3.then(fx4);

というコードで説明すると

fx1実行 → p1のresolve実行 → p1のhandlers配列の中のハンドラ(fx2)の実行 → p2のresolve実行 → p2のhandlers配列の中のハンドラ(fx3)の実行 → p3のresolve実行 → p3のhandlers配列の中のハンドラ(fx4)の実行

というように処理が次々に実行されます。これがthenメソッドチェーンが恙なく実行される理由です。

thenの処理が先行する

上記のコードの処理の順番をより詳細にみていくと、

fx1実行(非同期) → p1.then(fx2)の実行(fx2をhandlers配列に登録) → p2.then(fx3)の実行(fx3をhandlers配列に登録) → p3.then(fx4)の実行(fx4をhandlers配列に登録)
 
→ (fx1のコールバック関数の実行) → p1のresolve実行 → p1のhandlers配列の中のハンドラ(fx2)をマイクロタスクキューに登録(ここで非同期)
→ マイクロタスクキューのfx2の実行 → p2のresolve実行 → p2のhandlers配列の中のハンドラ(fx3)をマイクロタスクキューに登録(ここで非同期)
→ マイクロタスクキューのfx3の実行 → p3のresolve実行 → p3のhandlers配列の中のハンドラ(fx4)をマイクロタスクキューに登録(ここで非同期)
→ マイクロタスクキューのfx4の実行

という感じにコードの処理自体は同期的に一気に処理され、その後に登録された処理が順次実行されるようになります。

この簡略化されたMyPromiseクラスではfx1が同期的な処理であった場合は正しく処理されませんが、冒頭のMyPromiseクラスであればfx1が同期的な処理であってもthenの処理が先に同期的に処理されます。この場合は次のようになります。

fx1実行(同期) → p1のresolve実行 → p1.then(fx2)の実行(fx2をhandlers配列に登録) → p1のhandlers配列の中のハンドラ(fx2)をマイクロタスクキューに登録 → p2.then(fx3)の実行(fx3をhandlers配列に登録) → p3.then(fx4)の実行(fx4をhandlers配列に登録)
→ (ここで非同期)
 
→ マイクロタスクキューのfx2の実行 → p2のresolve実行 → p2のhandlers配列の中のハンドラ(fx3)をマイクロタスクキューに登録(ここで非同期)
→ マイクロタスクキューのfx3の実行 → p3のresolve実行 → p3のhandlers配列の中のハンドラ(fx4)をマイクロタスクキューに登録(ここで非同期)
→ マイクロタスクキューのfx4の実行

このように、最初に一連のpromiseインスタンスを生成したのちに、promiseに登録された処理を非同期にかつ順番通りに処理していくことになります。

Promiseとは

newした単体のプロミス (promise) は、確かにMDNがいう「プロミス (Promise) は、非同期処理の最終的な完了もしくは失敗を表すオブジェクト」ではありますが、thenメソッドを追加した場合には、

プロミス(promise)とは、(非)同期処理の最終的な完了もしくは失敗を表し、その結果に応じた処理を非同期に実行し、さらに続いて処理すべきプロミスへの参照を保持したオブジェクト」という感じでしょうか。

同期的な処理のように書かれたソースプログラムを、promiseチェーンという形でメモリ上にコピーして、非同期に順序通りに実行する仕組みがPromiseの本質です。

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?