25. 非同期プログラミングのためのPromise
目次
本書をサポートしてください:購入 (PDF, EPUB, MOBI) または 寄付
(広告、ブロックしないでください。)

25. 非同期プログラミングのためのPromise

本章では、Promiseによる非同期プログラミング全般、特にECMAScript 6のPromise APIについて入門します。前の章では、JavaScriptにおける非同期プログラミングの基礎を説明しています。本章で理解できない点があれば、そちらを参照してください。



25.1 概要

Promiseは、非同期計算の結果を配信するためのコールバックの代替手段です。非同期関数の作成者にはより多くの労力を要求しますが、それらの関数の使用者にはいくつかの利点を提供します。

次の関数は、Promiseを介して非同期的に結果を返します。

function asyncFunc() {
    return new Promise(
        function (resolve, reject) {
            ···
            resolve(result);
            ···
            reject(error);
        });
}

asyncFunc()は次のように呼び出します。

asyncFunc()
.then(result => { ··· })
.catch(error => { ··· });

25.1.1 then()呼び出しのチェーン化

then()は常にPromiseを返し、メソッド呼び出しをチェーン化できます。

asyncFunc1()
.then(result1 => {
    // Use result1
    return asyncFunction2(); // (A)
})
.then(result2 => { // (B)
    // Use result2
})
.catch(error => {
    // Handle errors of asyncFunc1() and asyncFunc2()
});

then()によって返されるPromise Pの解決方法は、そのコールバックが何をするかに依存します。

さらに、catch()が2つの非同期関数呼び出し(asyncFunction1()asyncFunction2())のエラーをどのように処理しているかに注目してください。つまり、キャッチされないエラーは、エラーハンドラが存在するまで渡されます。

25.1.2 非同期関数を並列実行する

then()を介して非同期関数呼び出しをチェーン化する場合、それらは一度に一つずつ順番に実行されます。

asyncFunc1()
.then(() => asyncFunc2());

そうせず、すべてをすぐに呼び出すと、基本的に並列に実行されます(Unixプロセスの用語ではフォーク)。

asyncFunc1();
asyncFunc2();

Promise.all()を使用すると、すべての結果が揃った時点で通知を受け取ることができます(Unixプロセスの用語ではジョイン)。その入力はPromiseの配列であり、その出力は結果の配列で解決される単一のPromiseです。

Promise.all([
    asyncFunc1(),
    asyncFunc2(),
])
.then(([result1, result2]) => {
    ···
})
.catch(err => {
    // Receives first rejection among the Promises
    ···
});

25.1.3 用語集:Promise

Promise APIは、結果を非同期的に配信することについてです。Promiseオブジェクト(略してPromise)はそのオブジェクトを介して配信される結果の代役およびコンテナです。

状態

状態の変化への反応

状態の変更:Promiseの状態を変更するための操作は2つあります。いずれか一方を一度呼び出した後、それ以上の呼び出しは無効になります。

25.2 はじめに:Promise

Promiseは、特定の種類の非同期プログラミング、つまり単一の結果を非同期的に返す関数(またはメソッド)を処理するのに役立つパターンです。そのような結果を受け取る一般的な方法は、コールバックを使用することです(「継続としてのコールバック」)。

asyncFunction(arg1, arg2,
    result => {
        console.log(result);
    });

Promiseはコールバックを処理するより良い方法を提供します。非同期関数はPromise、つまり最終結果のプレースホルダーおよびコンテナとして機能するオブジェクトを返します。Promiseメソッドthen()を介して登録されたコールバックは、結果を通知されます。

asyncFunction(arg1, arg2)
.then(result => {
    console.log(result);
});

継続としてのコールバックと比較して、Promiseには次の利点があります。

25.3 最初の例

Promiseの使用がどのようなものかを知るために、最初の例を見てみましょう。

Node.jsスタイルのコールバックを使用すると、ファイルの非同期読み取りは次のようになります。

fs.readFile('config.json',
    function (error, text) {
        if (error) {
            console.error('Error while reading config file');
        } else {
            try {
                const obj = JSON.parse(text);
                console.log(JSON.stringify(obj, null, 4));
            } catch (e) {
                console.error('Invalid JSON in file');
            }
        }
    });

Promiseを使用すると、同じ機能は次のように使用されます。

readFilePromisified('config.json')
.then(function (text) { // (A)
    const obj = JSON.parse(text);
    console.log(JSON.stringify(obj, null, 4));
})
.catch(function (error) { // (B)
    // File read error or JSON SyntaxError
    console.error('An error occurred', error);
});

コールバックは依然として存在しますが、結果に対して呼び出されるメソッド(then()catch())を介して提供されます。B行のエラーコールバックは2つの点で便利です。まず、エラー処理のスタイルが統一されている点(前の例のようにif (error)try-catchを使用する必要がない)。第二に、A行のコールバックとreadFilePromisified()の両方のエラーを1箇所で処理できる点です。

readFilePromisified()のコードは後述します。

25.4 Promiseを理解する3つの方法

Promiseを理解する3つの方法を見てみましょう。

次のコードは、Promiseベースの関数asyncFunc()とその呼び出しを含んでいます。

function asyncFunc() {
    return new Promise((resolve, reject) => { // (A)
        setTimeout(() => resolve('DONE'), 100); // (B)
    });
}
asyncFunc()
.then(x => console.log('Result: '+x));

// Output:
// Result: DONE

asyncFunc()はPromiseを返します。非同期計算の実際の結果'DONE'の準備が整うと、A行で始まるコールバックのパラメータであるresolve()(B行)を介して配信されます。

では、Promiseとは何でしょうか?

25.4.1 概念的に:Promiseベース関数の呼び出しはブロッキング

次のコードは、非同期関数main()からasyncFunc()を呼び出します。非同期関数はECMAScript 2017の機能です。

async function main() {
    const x = await asyncFunc(); // (A)
    console.log('Result: '+x); // (B)

    // Same as:
    // asyncFunc()
    // .then(x => console.log('Result: '+x));
}
main();

main()の本体は、何が起こっているかを概念的に、つまり非同期計算について私たちが通常どのように考えているかをうまく表現しています。つまり、asyncFunc()はブロッキング関数呼び出しです。

ECMAScript 6とジェネレータ以前は、コードを一時停止して再開することができませんでした。そのため、Promiseでは、コードが再開された後に発生するすべての処理をコールバックに記述します。そのコールバックを呼び出すことは、コードを再開することと同じです。

25.4.2 Promiseは非同期的に配信される値のコンテナ

関数がPromiseを返す場合、そのPromiseは、関数が(通常は)計算が完了したら結果を書き込む空白のようなものです。このプロセスの簡単なバージョンを配列を使用してシミュレートできます。

function asyncFunc() {
    const blank = [];
    setTimeout(() => blank.push('DONE'), 100);
    return blank;
}
const blank = asyncFunc();
// Wait until the value has been filled in
setTimeout(() => {
    const x = blank[0]; // (A)
    console.log('Result: '+x);
}, 200);

Promiseでは、最終的な値に[0](A行のように)アクセスするのではなく、then()メソッドとコールバックを使用します。

25.4.3 Promiseはイベントエミッタ

Promiseを別の見方として、イベントを発行するオブジェクトと考えることができます。

function asyncFunc() {
    const eventEmitter = { success: [] };
    setTimeout(() => { // (A)
        for (const handler of eventEmitter.success) {
            handler('DONE');
        }
    }, 100);
    return eventEmitter;
}
asyncFunc()
.success.push(x => console.log('Result: '+x)); // (B)

イベントリスナー(B行)の登録は、asyncFunc()を呼び出した後に行うことができます。これは、setTimeout()に渡されたコールバック(A行)が非同期的に(このコードの部分が終了した後)実行されるためです。

通常のイベントエミッタは、登録されるとすぐに開始される複数のイベントの配信に特化しています。

一方、Promiseは正確に1つの値の配信に特化しており、登録が遅すぎることに対する組み込みの保護が備わっています。Promiseの結果はキャッシュされ、Promiseが確定した後に登録されたイベントリスナーに渡されます。

25.5 Promiseの作成と使用

Promiseがプロデューサー側とコンシューマー側からどのように操作されるかを見てみましょう。

25.5.1 Promiseのプロデュース

プロデューサーとして、Promiseを作成し、それを介して結果を送信します。

const p = new Promise(
    function (resolve, reject) { // (A)
        ···
        if (···) {
            resolve(value); // success
        } else {
            reject(reason); // failure
        }
    });

25.5.2 Promiseの状態

Promiseを介して結果が配信されると、Promiseはその結果に固定されます。つまり、各Promiseは常に3つの(相互に排他的な)状態のいずれかになります。

Promiseは、fulfilledまたはrejectedのいずれかの場合に確定します(それが表す計算が完了しました)。Promiseは一度だけ確定し、その後は確定したままです。その後の確定の試みは効果がありません。

new Promise()のパラメータ(A行から始まる)は、executorと呼ばれます。

executor内で例外がスローされると、pはその例外でrejectedになります。

25.5.3 Promiseの消費

promiseのコンシューマとして、反応then()catch()メソッドで登録するコールバック)を介してfulfillmentまたはrejectionが通知されます。

promise
.then(value => { /* fulfillment */ })
.catch(error => { /* rejection */ });

Promiseが非同期関数(一発の結果を持つ関数)で非常に有用であるのは、Promiseが確定すると、それ以上変化しなくなるためです。さらに、Promiseが確定する前後にthen()またはcatch()を呼び出すかどうかに関係なく、競合状態は決して発生しません。

catch()は、単にthen()を呼び出すよりも便利(推奨)な代替手段であることに注意してください。つまり、次の2つの呼び出しは同等です。

promise.then(
    null,
    error => { /* rejection */ });

promise.catch(
    error => { /* rejection */ });

25.5.4 Promiseは常に非同期

Promiseライブラリは、結果がPromiseの反応に同期的に(すぐに)配信されるか、非同期的に(現在の継続、現在のコードの部分が終了した後)配信されるかを完全に制御できます。ただし、Promises/A+仕様では、後者の実行モードを常に使用する必要があります。これは、次の要件(2.2.4)でthen()メソッドについて規定されています。

onFulfilledまたはonRejectedは、実行コンテキストスタックにプラットフォームコードのみが含まれるまで呼び出されてはなりません。

つまり、コードは実行完了セマンティクス(前の章で説明)に依存でき、Promiseのチェーン化によって他のタスクの処理時間が不足することはありません。

さらに、この制約により、結果を同期的に返す場合と非同期的に返す場合がある関数を記述することが防止されます。これは、コードが予測不可能になるため、アンチパターンです。詳細については、Isaac Z. Schlueterによる「非同期のためのAPI設計」を参照してください。

25.6

Promiseを詳しく調べる前に、これまでの学習内容をいくつかの例で使用してみましょう。

25.6.1 例:fs.readFile()のPromisify

次のコードは、組み込みのNode.js関数fs.readFile()のPromiseベースのバージョンです。

import {readFile} from 'fs';

function readFilePromisified(filename) {
    return new Promise(
        function (resolve, reject) {
            readFile(filename, { encoding: 'utf8' },
                (error, data) => {
                    if (error) {
                        reject(error);
                    } else {
                        resolve(data);
                    }
                });
        });
}

readFilePromisified()は次のように使用します。

readFilePromisified(process.argv[2])
.then(text => {
    console.log(text);
})
.catch(error => {
    console.log(error);
});

25.6.2 例:XMLHttpRequestのPromisify

以下は、イベントベースのXMLHttpRequest APIを介してHTTP GETを実行するPromiseベースの関数です。

function httpGet(url) {
    return new Promise(
        function (resolve, reject) {
            const request = new XMLHttpRequest();
            request.onload = function () {
                if (this.status === 200) {
                    // Success
                    resolve(this.response);
                } else {
                    // Something went wrong (404 etc.)
                    reject(new Error(this.statusText));
                }
            };
            request.onerror = function () {
                reject(new Error(
                    'XMLHttpRequest Error: '+this.statusText));
            };
            request.open('GET', url);
            request.send();
        });
}

httpGet()は次のように使用します。

httpGet('http://example.com/file.txt')
.then(
    function (value) {
        console.log('Contents: ' + value);
    },
    function (reason) {
        console.error('Something went wrong', reason);
    });

25.6.3 例:アクティビティの遅延

setTimeout()をPromiseベースの関数delay()として実装してみましょう(Q.delay()に似ています)。

function delay(ms) {
    return new Promise(function (resolve, reject) {
        setTimeout(resolve, ms); // (A)
    });
}

// Using delay():
delay(5000).then(function () { // (B)
    console.log('5 seconds have passed!')
});

A行では、パラメータなしでresolveを呼び出していますが、これはresolve(undefined)を呼び出すことと同じです。B行でもfulfillment値は必要なく、単に無視します。通知されるだけで十分です。

25.6.4 例:Promiseのタイムアウト

function timeout(ms, promise) {
    return new Promise(function (resolve, reject) {
        promise.then(resolve);
        setTimeout(function () {
            reject(new Error('Timeout after '+ms+' ms')); // (A)
        }, ms);
    });
}

タイムアウト後の拒否(A行)はリクエストをキャンセルしませんが、Promiseがその結果でfulfilledされるのを防ぎます。

timeout()は次のように使用します。

timeout(5000, httpGet('http://example.com/file.txt'))
.then(function (value) {
    console.log('Contents: ' + value);
})
.catch(function (reason) {
    console.error('Error or timeout', reason);
});

25.7 Promiseを作成する他の方法

これで、Promiseの機能を詳しく調べることができます。まず、Promiseを作成するさらに2つの方法を探ってみましょう。

25.7.1 Promise.resolve()

Promise.resolve(x)は次のように動作します。

つまり、Promise.resolve() を使用して、任意の値(Promise、thenable、その他)をPromiseに変換できます。実際、これはPromise.all()Promise.race()によって、任意の値の配列をPromiseの配列に変換するために使用されています。

25.7.2 Promise.reject()

Promise.reject(err) は、err で拒否された Promise を返します。

const myError = new Error('Problem!');
Promise.reject(myError)
.catch(err => console.log(err === myError)); // true

25.8 Promise のチェーン

このセクションでは、Promise のチェーン処理について詳しく見ていきます。メソッド呼び出しの結果は

P.then(onFulfilled, onRejected)

新しい Promise Q です。つまり、Q に対して then() を呼び出すことで、Promise ベースの制御フローを継続できます。

25.8.1 通常の値で Q を解決する

then() が返す Promise Q を通常の値で解決する場合は、後続の then() を介してその値を取得できます。

asyncFunc()
.then(function (value1) {
    return 123;
})
.then(function (value2) {
    console.log(value2); // 123
});

25.8.2 thenable で Q を解決する

then() が返す Promise Q を、thenable R で解決することもできます。thenable とは、Promise.prototype.then() のように動作する then() メソッドを持つオブジェクトです。したがって、Promise は thenable です。R で解決する(例:onFulfilled から返す)ということは、Q の「後」に挿入されることを意味します。R の解決は、Q の onFulfilledonRejected のコールバックに転送されます。ある意味、Q は R になります。

このメカニズムの主な用途は、次の例のようにネストされた then() 呼び出しをフラット化することです。

asyncFunc1()
.then(function (value1) {
    asyncFunc2()
    .then(function (value2) {
        ···
    });
})

フラット版は次のようになります。

asyncFunc1()
.then(function (value1) {
    return asyncFunc2();
})
.then(function (value2) {
    ···
})

25.8.3 onRejected から Q を解決する

エラーハンドラーで返すものは何でも、解決値(拒否値ではない!)になります。これにより、失敗した場合に使用されるデフォルト値を指定できます。

retrieveFileName()
.catch(function () {
    // Something went wrong, use a default value
    return 'Untitled.txt';
})
.then(function (fileName) {
    ···
});

25.8.4 例外をスローして Q を拒否する

then()catch() のコールバックでスローされた例外は、拒否として次のエラーハンドラーに渡されます。

asyncFunc()
.then(function (value) {
    throw new Error();
})
.catch(function (reason) {
    // Handle error here
});

25.8.5 チェーンとエラー

エラーハンドラーを持たない then() メソッド呼び出しが1つ以上ある場合があります。その場合、エラーハンドラーがあるまでエラーが渡されます。

asyncFunc1()
.then(asyncFunc2)
.then(asyncFunc3)
.catch(function (reason) {
    // Something went wrong above
});

25.9 Promise チェーンの一般的な間違い

25.9.1 間違い:Promise チェーンの末尾を失う

次のコードでは、2つのPromiseのチェーンが構築されますが、その最初の部分のみが返されます。その結果、チェーンの末尾が失われます。

// Don’t do this
function foo() {
    const promise = asyncFunc();
    promise.then(result => {
        ···
    });

    return promise;
}

これは、チェーンの末尾を返すことで修正できます。

function foo() {
    const promise = asyncFunc();
    return promise.then(result => {
        ···
    });
}

変数promiseが必要ない場合は、このコードをさらに簡素化できます。

function foo() {
    return asyncFunc()
    .then(result => {
        ···
    });
}

25.9.2 間違い:Promise のネスト

次のコードでは、asyncFunc2() の呼び出しがネストされています。

// Don’t do this
asyncFunc1()
.then(result1 => {
    asyncFunc2()
    .then(result2 => {
        ···
    });
});

修正するには、最初の then() から2番目の Promise を返し、2番目のチェーンされた then() を介して処理します。

asyncFunc1()
.then(result1 => {
    return asyncFunc2();
})
.then(result2 => {
    ···
});

25.9.3 間違い:チェーン処理の代わりに Promise を作成する

次のコードでは、メソッド insertInto() はその結果に対して新しい Promise を作成します(A 行)。

// Don’t do this
class Model {
    insertInto(db) {
        return new Promise((resolve, reject) => { // (A)
          db.insert(this.fields) // (B)
          .then(resultCode => {
              this.notifyObservers({event: 'created', model: this});
              resolve(resultCode); // (C)
          }).catch(err => {
              reject(err); // (D)
          })
        });
    }
    ···
}

よく見ると、結果の Promise は主に、非同期メソッド呼び出し db.insert()(B 行)の解決(C 行)と拒否(D 行)を転送するために使用されていることがわかります。

修正するには、then() を使用してチェーン処理することで、Promise を作成しません。

class Model {
    insertInto(db) {
        return db.insert(this.fields) // (A)
        .then(resultCode => {
            this.notifyObservers({event: 'created', model: this});
            return resultCode; // (B)
        });
    }
    ···
}

説明

25.9.4 間違い:エラー処理に then() を使用する

原則として、catch(cb)then(null, cb) の略記です。しかし、then() の両方のパラメーターを同時に使用すると、問題が発生する可能性があります。

// Don’t do this
asyncFunc1()
.then(
    value => { // (A)
        doSomething(); // (B)
        return asyncFunc2(); // (C)
    },
    error => { // (D)
        ···
    });

拒否コールバック(D 行)は asyncFunc1() のすべての拒否を受け取りますが、解決コールバック(A 行)によって作成された拒否は受け取りません。たとえば、B 行の同期関数呼び出しで例外がスローされるか、C 行の非同期関数呼び出しで拒否が発生する可能性があります。

したがって、拒否コールバックをチェーンされた catch() に移動する方が良いでしょう。

asyncFunc1()
.then(value => {
    doSomething();
    return asyncFunc2();
})
.catch(error => {
    ···
});

25.10 エラー処理のヒント

25.10.1 運用エラーとプログラマーエラー

プログラムには、2種類のエラーがあります。

25.10.1.1 運用エラー:拒否と例外を混在させない

運用エラーの場合、各関数はエラーをシグナルするための正確に1つの方法をサポートする必要があります。Promise ベースの関数の場合、それは拒否と例外を混在させないことを意味し、例外をスローしないことを意味します。

25.10.1.2 プログラマーエラー:迅速に失敗する

プログラマーエラーの場合、例外をスローすることで、できるだけ早く失敗することが理にかなっています。

function downloadFile(url) {
    if (typeof url !== 'string') {
        throw new Error('Illegal argument: ' + url);
    }
    return new Promise(···).
}

これを行う場合は、非同期コードが例外を処理できることを確認する必要があります。例外をスローすることは、理論的には静的にチェックできる(ソースコードを分析するリンターなどによる)アサーションや同様のものには許容できると考えています。

25.10.2 Promise ベースの関数での例外の処理

then()catch() のコールバック内で例外がスローされた場合、これら2つのメソッドは例外を拒否に変換するため、問題ではありません。

しかし、同期処理を行うことで非同期関数を開始する場合、状況は異なります。

function asyncFunc() {
    doSomethingSync(); // (A)
    return doSomethingAsync()
    .then(result => {
        ···
    });
}

A 行で例外がスローされると、関数は例外をスローします。この問題には2つの解決策があります。

25.10.2.1 解決策1:拒否された Promise を返す

例外をキャッチして、拒否された Promise として返すことができます。

function asyncFunc() {
    try {
        doSomethingSync();
        return doSomethingAsync()
        .then(result => {
            ···
        });
    } catch (err) {
        return Promise.reject(err);
    }
}
25.10.2.2 解決策2:コールバック内で同期コードを実行する

Promise.resolve() を介して then() メソッド呼び出しのチェーンを開始し、コールバック内で同期コードを実行することもできます。

function asyncFunc() {
    return Promise.resolve()
    .then(() => {
        doSomethingSync();
        return doSomethingAsync();
    })
    .then(result => {
        ···
    });
}

別の方法として、Promise コンストラクターを介して Promise チェーンを開始できます。

function asyncFunc() {
    return new Promise((resolve, reject) => {
        doSomethingSync();
        resolve(doSomethingAsync());
    })
    .then(result => {
        ···
    });
}

このアプローチにより、1ティック節約できます(同期コードはすぐに実行されます)が、コードの規則性が低下します。

25.10.3 さらに読む

このセクションの情報源

25.11 Promise の合成

合成とは、既存の部分から新しいものを作成することです。Promise のシーケンシャルな合成はすでに経験済みです。2つの Promise P と Q がある場合、次のコードは P が解決された後に Q を実行する新しい Promise を生成します。

P.then(() => Q)

これは、同期コードのセミコロンに似ていることに注意してください。同期操作 f()g() のシーケンシャルな合成は次のようになります。

f(); g()

このセクションでは、Promise を合成する追加の方法について説明します。

25.11.1 計算を手動でフォークおよび結合する

2つの非同期計算、asyncFunc1()asyncFunc2() を並行して実行するとします。

// Don’t do this
asyncFunc1()
.then(result1 => {
    handleSuccess({result1});
});
.catch(handleError);

asyncFunc2()
.then(result2 => {
    handleSuccess({result2});
})
.catch(handleError);

const results = {};
function handleSuccess(props) {
    Object.assign(results, props);
    if (Object.keys(results).length === 2) {
        const {result1, result2} = results;
        ···
    }
}
let errorCounter = 0;
function handleError(err) {
    errorCounter++;
    if (errorCounter === 1) {
        // One error means that everything failed,
        // only react to first error
        ···
    }
}

2つの関数呼び出し asyncFunc1()asyncFunc2()then() チェーニングなしで行われます。その結果、どちらもすぐにほぼ並行して実行されます。実行はフォークされ、各関数呼び出しによって個別の「スレッド」が生成されます。両方のスレッドが終了すると(結果またはエラーで)、handleSuccess() または handleError() で単一のスレッドに結合されます。

このアプローチの問題は、手動でエラーが発生しやすい作業が多すぎることです。修正するには、組み込みメソッド Promise.all() を使用して、自分で行わないことです。

25.11.2 Promise.all() を使用した計算のフォークと結合

Promise.all(iterable)は、Promiseのイテラブル(thenableおよびその他の値はPromise.resolve()によってPromiseに変換されます)を受け取ります。それらのすべてがfulfilledされると、それらの値の配列でfulfilledされます。iterableが空の場合、all()によって返されるPromiseはすぐにfulfilledされます。

Promise.all([
    asyncFunc1(),
    asyncFunc2(),
])
.then(([result1, result2]) => {
    ···
})
.catch(err => {
    // Receives first rejection among the Promises
    ···
});

25.11.3 Promise.all()によるmap()

Promiseの良い点の1つは、Promiseベースの関数は結果を返すため、多くの同期ツールが引き続き機能することです。たとえば、Arrayメソッドのmap()を使用できます。

const fileUrls = [
    'http://example.com/file1.txt',
    'http://example.com/file2.txt',
];
const promisedTexts = fileUrls.map(httpGet);

promisedTextsはPromiseの配列です。前のセクションで既に説明したPromise.all()を使用して、その配列を結果の配列でfulfilledされるPromiseに変換できます。

Promise.all(promisedTexts)
.then(texts => {
    for (const text of texts) {
        console.log(text);
    }
})
.catch(reason => {
    // Receives first rejection among the Promises
});

25.11.4 Promise.race()によるタイムアウト

Promise.race(iterable)は、Promiseのイテラブル(thenableおよびその他の値はPromise.resolve()によってPromiseに変換されます)を受け取り、Promise Pを返します。入力Promiseの中で最初に解決されたものが、その解決状態を出力Promiseに渡します。iterableが空の場合、race()によって返されるPromiseは決して解決されません。

例として、タイムアウトを実装するためにPromise.race()を使用してみましょう。

Promise.race([
    httpGet('http://example.com/file.txt'),
    delay(5000).then(function () {
        throw new Error('Timed out')
    });
])
.then(function (text) { ··· })
.catch(function (reason) { ··· });

25.12 2つの有用な追加Promiseメソッド

このセクションでは、多くのPromiseライブラリが提供する、Promiseに関する2つの有用なメソッドについて説明します。これらはPromiseをさらに説明するためだけに示されており、Promise.prototypeに追加しないでください(この種の修正は、ポリフィルによってのみ行う必要があります)。

25.12.1 done()

複数のPromiseメソッド呼び出しをチェーンすると、エラーが暗黙的に破棄されるリスクがあります。たとえば

function doSomething() {
    asyncFunc()
    .then(f1)
    .catch(r1)
    .then(f2); // (A)
}

A行のthen()が拒否を生成した場合、どこでも処理されません。PromiseライブラリQは、メソッド呼び出しのチェーンの最後の要素として使用されるdone()メソッドを提供します。これは最後のthen()を置き換え、(1〜2個の引数を持つ)

function doSomething() {
    asyncFunc()
    .then(f1)
    .catch(r1)
    .done(f2);
}

または最後のthen()の後に挿入されます(引数は0個)。

function doSomething() {
    asyncFunc()
    .then(f1)
    .catch(r1)
    .then(f2)
    .done();
}

Qのドキュメントを引用すると

donethenの使用に関する黄金律は、Promiseを他の誰かに返すか、チェーンが自分自身で終わる場合は、doneを呼び出して終了させることです。catchで終了しても十分ではありません。なぜなら、catchハンドラー自体がエラーをスローする可能性があるからです。

ECMAScript 6でdone()を実装する方法は次のとおりです。

Promise.prototype.done = function (onFulfilled, onRejected) {
    this.then(onFulfilled, onRejected)
    .catch(function (reason) {
        // Throw an exception globally
        setTimeout(() => { throw reason }, 0);
    });
};

doneの機能は明らかに有用ですが、ECMAScript 6には追加されていません。最初に、エンジンが自動的にどれだけ検出できるかを調査することが目的でした。それがどの程度うまく機能するかによって、done()を導入する必要がある可能性があります。

25.12.2 finally()

エラーが発生したかどうかに関係なく、アクションを実行したい場合があります。たとえば、リソースの使用が終了した後にクリーンアップするためです。それがPromiseメソッドfinally()の用途であり、例外処理のfinally句と非常によく似ています。そのコールバックは引数を受け取りませんが、解決または拒否のいずれかが通知されます。

createResource(···)
.then(function (value1) {
    // Use resource
})
.then(function (value2) {
    // Use resource
})
.finally(function () {
    // Clean up
});

Domenic Denicolaこのようにfinally()を実装することを提案しています。

Promise.prototype.finally = function (callback) {
    const P = this.constructor;
    // We don’t invoke the callback in here,
    // because we want then() to handle its exceptions
    return this.then(
        // Callback fulfills => continue with receiver’s fulfillment or rejec\
tion
        // Callback rejects => pass on that rejection (then() has no 2nd para\
meter!)
        value  => P.resolve(callback()).then(() => value),
        reason => P.resolve(callback()).then(() => { throw reason })
    );
};

コールバックは、レシーバ(this)の解決方法を決定します。

例1Jake Archibaldによる):スピナーを非表示にするためのfinally()の使用。簡略版

showSpinner();
fetchGalleryData()
.then(data => updateGallery(data))
.catch(showNoDataError)
.finally(hideSpinner);

例2Kris Kowalによる):テストのティアダウンにfinally()を使用。

const HTTP = require("q-io/http");
const server = HTTP.Server(app);
return server.listen(0)
.then(function () {
    // run test
})
.finally(server.stop);

25.13 Node.js:コールバックベースの同期関数とPromiseの併用

PromiseライブラリQには、Node.jsスタイルの(err, result)コールバックAPIとのインターフェースのためのツール関数があります。たとえば、denodeifyはコールバックベースの関数をPromiseベースの関数に変換します。

const readFile = Q.denodeify(FS.readFile);

readFile('foo.txt', 'utf-8')
.then(function (text) {
    ···
});

denodifyは、Q.denodeify()の機能のみを提供し、ECMAScript 6 Promise APIに準拠するマイクロライブラリです。

25.14 ES6互換のPromiseライブラリ

多くのPromiseライブラリが存在します。次のものはECMAScript 6 APIに準拠しているため、現在使用でき、後でネイティブのES6に簡単に移行できます。

最小限のポリフィル

大規模なPromiseライブラリ

ES6標準ライブラリのポリフィル

25.15 次のステップ:ジェネレータによるPromiseの使用

Promiseによる非同期関数の実装は、イベントまたはコールバックによる実装よりも便利ですが、それでも理想的ではありません。

解決策は、ブロッキングコールをJavaScriptに取り込むことです。ジェネレータを使用すると、ライブラリを介してこれを行うことができます。次のコードでは、制御フローライブラリcoを使用して、2つのJSONファイルを非同期的に取得します。

co(function* () {
    try {
        const [croftStr, bondStr] = yield Promise.all([  // (A)
            getFile('http://localhost:8000/croft.json'),
            getFile('http://localhost:8000/bond.json'),
        ]);
        const croftJson = JSON.parse(croftStr);
        const bondJson = JSON.parse(bondStr);

        console.log(croftJson);
        console.log(bondJson);
    } catch (e) {
        console.log('Failure to read: ' + e);
    }
});

A行では、Promise.all()の結果が準備できるまで、yieldによって実行がブロック(待機)されます。つまり、非同期操作を実行しながら、コードは同期的に見えます。

ジェネレータに関する章で詳細を説明しています。

25.16 Promiseの詳細:簡単な実装

このセクションでは、別の角度からPromiseにアプローチします。APIの使用方法を学ぶのではなく、簡単な実装を見ていきます。この異なる角度は、Promiseを理解する上で非常に役立ちました。

Promiseの実装はDemoPromiseと呼ばれます。理解しやすくするために、APIと完全に一致するわけではありません。しかし、実際のインプリメンテーションが直面する課題について多くの洞察を与えるのに十分な近さです。

DemoPromiseは、3つのプロトタイプメソッドを持つクラスです。

つまり、resolverejectはメソッドです(コンストラクタのコールバックパラメータに渡される関数ではありません)。

25.16.1 スタンドアロンのPromise

最初の実装は、最小限の機能を持つスタンドアロンのPromiseです。

この最初の実装の使用方法を次に示します。

const dp = new DemoPromise();
dp.resolve('abc');
dp.then(function (value) {
    console.log(value); // abc
});

次の図は、最初の実装であるDemoPromiseの動作を示しています。

25.16.1.1 DemoPromise.prototype.then()

まずthen()を調べましょう。2つのケースを処理する必要があります。

then(onFulfilled, onRejected) {
    const self = this;
    const fulfilledTask = function () {
        onFulfilled(self.promiseResult);
    };
    const rejectedTask = function () {
        onRejected(self.promiseResult);
    };
    switch (this.promiseState) {
        case 'pending':
            this.fulfillReactions.push(fulfilledTask);
            this.rejectReactions.push(rejectedTask);
            break;
        case 'fulfilled':
            addToTaskQueue(fulfilledTask);
            break;
        case 'rejected':
            addToTaskQueue(rejectedTask);
            break;
    }
}

前のコードスニペットは、次のヘルパー関数を使用します。

function addToTaskQueue(task) {
    setTimeout(task, 0);
}
25.16.1.2 DemoPromise.prototype.resolve()

resolve()は次のように機能します。Promiseが既に解決されている場合、何も行いません(Promiseは一度だけ解決できることを保証します)。それ以外の場合は、Promiseの状態が'fulfilled'に変更され、結果がthis.promiseResultにキャッシュされます。次に、これまでにエンキューされたすべての履行リアクションがトリガーされます。

resolve(value) {
    if (this.promiseState !== 'pending') return;
    this.promiseState = 'fulfilled';
    this.promiseResult = value;
    this._clearAndEnqueueReactions(this.fulfillReactions);
    return this; // enable chaining
}
_clearAndEnqueueReactions(reactions) {
    this.fulfillReactions = undefined;
    this.rejectReactions = undefined;
    reactions.map(addToTaskQueue);
}

reject()resolve()に似ています。

25.16.2 チェーン

次に実装する機能はチェーンです。

明らかに、変更されるのはthen()だけです。

then(onFulfilled, onRejected) {
    const returnValue = new Promise(); // (A)
    const self = this;

    let fulfilledTask;
    if (typeof onFulfilled === 'function') {
        fulfilledTask = function () {
            const r = onFulfilled(self.promiseResult);
            returnValue.resolve(r); // (B)
        };
    } else {
        fulfilledTask = function () {
            returnValue.resolve(self.promiseResult); // (C)
        };
    }

    let rejectedTask;
    if (typeof onRejected === 'function') {
        rejectedTask = function () {
            const r = onRejected(self.promiseResult);
            returnValue.resolve(r); // (D)
        };
    } else {
        rejectedTask = function () {
            // `onRejected` has not been provided
            // => we must pass on the rejection
            returnValue.reject(self.promiseResult); // (E)
        };
    }
    ···
    return returnValue; // (F)
}

then()は新しいPromiseを作成し、返します(A行とF行)。さらに、fulfilledTaskrejectedTaskは異なる方法で設定されます。解決後…

25.16.3 フラット化

フラット化は主に、チェーン処理をより便利にするためのものです。通常、リアクションから値を返すことは、それを次のthen()に渡します。Promiseを返す場合、次の例のように「展開」されると便利です。

asyncFunc1()
.then(function (value1) {
    return asyncFunc2(); // (A)
})
.then(function (value2) {
    // value2 is fulfillment value of asyncFunc2() Promise
    console.log(value2);
});

A行でPromiseを返し、現在のメソッド内にthen()の呼び出しをネストする必要はありませんでした。メソッドの結果に対してthen()を呼び出すことができました。したがって、ネストされたthen()はなく、すべてフラットなままです。

これは、resolve()メソッドにフラット化を行わせることで実装します。

Qを(Promiseだけでなく)thenableとして許可すると、フラット化をより汎用的にすることができます。

ロックインを実装するために、新しいブール型フラグthis.alreadyResolvedを導入します。これがtrueになると、thisはロックされ、それ以上解決できなくなります。その状態はロックインされているPromiseと同じであるため、thisは依然として保留中である可能性があることに注意してください。

resolve(value) {
    if (this.alreadyResolved) return;
    this.alreadyResolved = true;
    this._doResolve(value);
    return this; // enable chaining
}

実際の解決は、プライベートメソッド_doResolve()で行われます。

_doResolve(value) {
    const self = this;
    // Is `value` a thenable?
    if (typeof value === 'object' && value !== null && 'then' in value) {
        // Forward fulfillments and rejections from `value` to `this`.
        // Added as a task (versus done immediately) to preserve async semant\
ics.
        addToTaskQueue(function () { // (A)
            value.then(
                function onFulfilled(result) {
                    self._doResolve(result);
                },
                function onRejected(error) {
                    self._doReject(error);
                });
        });
    } else {
        this.promiseState = 'fulfilled';
        this.promiseResult = value;
        this._clearAndEnqueueReactions(this.fulfillReactions);
    }
}

フラット化はA行で行われます。valueが履行された場合、selfを履行させたいですし、valueが拒否された場合、selfを拒否させたいです。alreadyResolvedによる保護を回避するために、プライベートメソッド_doResolve_doRejectを介して転送が行われます。

25.16.4 Promiseの状態の詳細

チェーン処理を使用すると、Promiseの状態はより複雑になります(ECMAScript 6仕様の25.4節で説明されているとおり)。

Promiseを使用しているだけの場合は、通常、簡略化された世界観を採用し、ロックインを無視できます。最も重要な状態関連の概念は「解決済み」です。Promiseは、履行されているか拒否されているかのいずれかの場合に解決済みです。Promiseが解決済みになると、それ以上変更されません(状態と履行または拒否値)。

Promiseを実装する場合は、「解決」も重要であり、理解が難しくなります。

25.16.5 例外

最後の機能として、ユーザーコードの例外を拒否として処理するPromiseを実装したいと思います。現時点では、「ユーザーコード」とは、then()の2つのコールバックパラメーターを意味します。

次の抜粋は、A行でその呼び出しの周りにtry-catchをラップすることにより、onFulfilled内の例外を拒否に変換する方法を示しています。

then(onFulfilled, onRejected) {
    ···
    let fulfilledTask;
    if (typeof onFulfilled === 'function') {
        fulfilledTask = function () {
            try {
                const r = onFulfilled(self.promiseResult); // (A)
                returnValue.resolve(r);
            } catch (e) {
                returnValue.reject(e);
            }
        };
    } else {
        fulfilledTask = function () {
            returnValue.resolve(self.promiseResult);
        };
    }
    ···
}

25.16.6 公開コンストラクターパターン

DemoPromiseを実際のPromise実装に変換したい場合、公開コンストラクターパターン[2]も実装する必要があります。ES6 Promiseは、メソッドを介してではなく、コンストラクターのコールバックパラメーターであるexecutorに渡される関数によって解決および拒否されます。

executorが例外をスローした場合、「その」Promiseは拒否される必要があります。

25.17 Promiseの長所と短所

25.17.1 Promiseの長所

25.17.1.1 非同期APIの統一

Promiseの重要な利点の1つは、非同期ブラウザAPIでますます使用されるようになり、現在多様で互換性のないパターンと規約を統一することです。2つの今後のPromiseベースのAPIを見てみましょう。

Fetch APIは、XMLHttpRequestに代わるPromiseベースの代替手段です。

fetch(url)
.then(request => request.text())
.then(str => ···)

fetch()は実際の要求に対するPromiseを返し、text()は文字列としてコンテンツに対するPromiseを返します。

プログラムによるモジュールのインポートのためのECMAScript 6 APIもPromiseに基づいています。

System.import('some_module.js')
.then(some_module => {
    ···
})
25.17.1.2 Promise対イベント

イベントと比較して、Promiseは一括結果の処理に適しています。結果を計算する前または後に結果に登録するかどうかは関係なく、結果を取得します。Promiseのこの利点は本質的に重要です。一方で、繰り返し発生するイベントの処理には使用できません。チェーン処理はPromiseのもう1つの利点ですが、イベント処理に追加できるものです。

25.17.1.3 Promise対コールバック

コールバックと比較して、Promiseはよりクリーンな関数(またはメソッド)シグネチャを持っています。コールバックでは、パラメーターが入力と出力に使用されます。

fs.readFile(name, opts?, (err, string | Buffer) => void)

Promiseでは、すべてのパラメーターが入力に使用されます。

readFilePromisified(name, opts?) : Promise<string | Buffer>

その他のPromiseの利点としては、以下があります。

25.17.2 Promiseが常に最良の選択肢とは限らない

Promiseは、単一の非同期結果に適しています。以下には適していません。

ECMAScript 6 Promiseには、場合によっては便利な2つの機能がありません。

Q Promiseライブラリは後者に対するサポートがあり、Promises/A+に両方の機能を追加する計画があります。

25.18 参照:ECMAScript 6 Promise API

このセクションでは、仕様で説明されているECMAScript 6 Promise APIの概要を示します。

25.18.1 Promiseコンストラクター

Promiseのコンストラクターは、次のように呼び出されます。

const p = new Promise(function (resolve, reject) { ··· });

このコンストラクターのコールバックは、executorと呼ばれます。executorは、そのパラメーターを使用して新しいPromisepを解決または拒否できます。

25.18.2 静的Promiseメソッド

25.18.2.1 Promiseの作成

次の2つの静的メソッドは、レシーバーの新しいインスタンスを作成します。

25.18.2.2 Promiseの合成

直感的には、静的メソッドPromise.all()Promise.race()は、Promiseのiterableを単一のPromiseに合成します。つまり、

メソッドは以下のとおりです。

25.18.3 Promise.prototypeメソッド

25.18.3.1 Promise.prototype.then(onFulfilled, onRejected)

省略されたリアクションのデフォルト値は、次のように実装できます。

function defaultOnFulfilled(x) {
    return x;
}
function defaultOnRejected(e) {
    throw e;
}
25.18.3.2 Promise.prototype.catch(onRejected)

25.19 さらに読む

[1] Brian CavalierとDomenic Denicolaによって編集された“Promises/A+”(JavaScript Promisesの事実上の標準)

[2] Domenic Denicolaによる“The Revealing Constructor Pattern”(このパターンはPromiseコンストラクタで使用されています)

次へ: VI その他