メインコンテンツへスキップ

複数のPromiseを制御するためのメソッドを実装してみた

· loading · loading ·
kiitosu
著者
kiitosu
画像処理やデバイスドライバ、データ基盤構築からWebバックエンドまで、多様な領域に携わってきました。地図解析や地図アプリケーションの仕組みにも経験があり、幅広い技術を活かした開発に取り組んでいます。休日は草野球とランニングを楽しんでいます。
目次

はじめに
#

以前から進めている自作Promise実装(MyPromise)に、複数のPromiseをまとめて制御するためのメソッド群を追加しました。

なお、これまでのPromiseに関する記事はこちらです。

Promiseを「制御」するためのメソッド
#

インスタンスメソッド(thencatch)が「個々のPromiseの結果を受け取る」ためのものであるのに対し、今回追加したメソッド群は**「複数のPromiseをどうオーケストレーションし、制御するか」**という目的のために存在します。

具体的には、以下のような制御を実現します。

  • 並行処理の待ち合わせ: allallSettled のように、複数の非同期処理が完了するタイミングを同期させる。
  • 最速の応答を採用: raceany のように、複数の処理の中から条件に合うものを抽出する。
  • 状態の即時確定: resolvereject のように、非同期処理のライフサイクルを外部から明示的に開始・確定させる。

なぜ「静的メソッド」として実装するのか
#

これらはインスタンスメソッドではなく static メソッド(静的メソッド)として定義しています。これには明確な技術的理由があります。

1. Promiseを生成するための「工場(ファクトリ)」としての役割
#

特に resolvereject がこれに当たります。 もしこれらがインスタンスメソッドだった場合、「Promiseを作るために、まず別のPromiseインスタンスを(無意味でも)用意して、そのメソッドを呼ぶ」という本末転倒な手順が必要になってしまいます。

静的メソッドにすることで、インスタンスがまだ存在しない状態から、特定の状態を持つPromiseを即座に生成することができます。これはデザインパターンの「ファクトリメソッド」の考え方です。

2. 複数のPromiseを「外側」から俯瞰して制御する
#

allrace は、複数の Promise インスタンスを配列として受け取ります。 特定の「ある一つの Promise」が自分自身のことを処理するのではなく、複数の Promise を外側からオーケストレーション(調整)する役割であるため、個々のインスタンスに紐付かないクラス側のメソッドとして定義するのが自然です。

3. JavaScript標準仕様(ES6以降)への準拠
#

今回実装したメソッドは、すべてES6以降のJavaScriptで標準化されている Promise クラスのインターフェースに基づいています。標準仕様が「静的メソッド」としてこれらを定義しているため、自作実装でもそれに倣うことで、他の開発者にとっても予測可能で使いやすいインターフェースになります。

今回実装したのは以下のメソッドです。

  • MyPromise.resolve(value)
  • MyPromise.reject(reason)
  • MyPromise.all(promises)
  • MyPromise.race(promises)
  • MyPromise.allSettled(promises)
  • MyPromise.any(promises)

それぞれの役割と、自作する際のポイントをまとめます。

各メソッドの実装解説
#

1. MyPromise.resolve / reject
#

これらは一番基本となるメソッドです。

static resolve(value) {
    // valueがMyPromiseならそのまま返却
    if (value instanceof MyPromise) return value;
    // それ以外はvalueで解決した新しいMyPromiseを返す
    return new MyPromise((resolve) => resolve(value))
}

static reject(reason) {
    // rejectは常に新しいPromiseを返す
    return new MyPromise((_, reject) => reject(reason))
}

resolve のポイントは、引数が既に MyPromise のインスタンスである場合にそれをそのまま返す点です。これにより、二重にPromiseで包んでしまうのを防いでいます。

2. MyPromise.all
#

複数のPromiseがすべて成功するのを待つメソッドです。

static all(promises) {
    return new MyPromise((resolve, reject) => {
        const results = [];
        let count = 0;

        if (promises.length === 0) {
            return resolve(results);
        }

        for (let i = 0; i < promises.length; i++) {
            MyPromise.resolve(promises[i]).then(
                (value) => {
                    results[i] = value; // インデックスで順序を保持
                    count++;
                    if (count === promises.length) {
                        resolve(results);
                    }
                },
                (reason) => {
                    reject(reason); // 1つでも失敗したら即座にreject
                }
            );
        }
    });
}

実装の肝は、結果を格納する results 配列にインデックスを指定して代入することで、Promiseが解決された順序ではなく、渡された順序を維持することです。

3. MyPromise.race
#

「競争」の名の通り、一番早く解決(または拒否)されたものの結果を返します。

static race(promises) {
    return new MyPromise((resolve, reject) => {
        for (let i = 0; i < promises.length; i++) {
            MyPromise.resolve(promises[i]).then(resolve, reject);
        }
    });
}

実装は非常にシンプルで、渡されたすべてのPromiseに対して then(resolve, reject) を登録するだけです。最初に決着がついたものが、新しく作ったPromiseの結果となります。

4. MyPromise.allSettled
#

すべてのPromiseが**完了(成功か失敗かを問わず)**するのを待ちます。

static allSettled(promises) {
    return new MyPromise((resolve) => {
        const results = [];
        let count = 0;

        if (promises.length === 0) return resolve(results);

        for (let i = 0; i < promises.length; i++) {
            MyPromise.resolve(promises[i]).then(
                (value) => {
                    results[i] = { status: 'fulfilled', value };
                    count++;
                    if (count === promises.length) resolve(results);
                },
                (reason) => {
                    results[i] = { status: 'rejected', reason };
                    count++;
                    if (count === promises.length) resolve(results);
                }
            );
        }
    });
}

all と似ていますが、失敗しても reject せずに結果を保持し続けるのが特徴です。

5. MyPromise.any
#

どれか1つでも成功すればその結果を返し、すべて失敗した場合のみエラー(AggregateError)を返します。

static any(promises) {
    return new MyPromise((resolve, reject) => {
        const reasons = [];
        let count = 0;

        if (promises.length === 0) {
            return reject(new AggregateError([], 'All promises were rejected'));
        }

        for (let i = 0; i < promises.length; i++) {
            MyPromise.resolve(promises[i]).then(
                (value) => resolve(value), // 1つでも成功すればOK
                (reason) => {
                    reasons[i] = reason;
                    count++;
                    if (count === promises.length) {
                        reject(new AggregateError(reasons, "All promises were rejected"));
                    }
                }
            );
        }
    });
}

race との違いは、1つが失敗しても諦めずに他の成功を待つ点です。

おわりに
#

これらの静的メソッドを実装することで、自作の MyPromise もかなり実用的な機能を備えてきました。 特に allany の実装を通して、Promiseの状態管理や非同期処理の集約の仕組みを深く理解することができました。

次は、ブラウザやNode.js環境での挙動の違いや、さらなる仕様への準拠を進めていきたいと思います。

Reply by Email

関連記事

Promiseを実装してみた
· loading · loading
Promiseの仕様
· loading · loading
Redis使ってみた
· loading · loading