【JavaScript】いまさらだけどPromiseを理解する

JavaScript

はじめに

JavaScriptで非同期処理を扱ううえで避けて通れないのがPromiseです。恥ずかしながら、Promiseについて理解が薄かったので、改めて調べてみました。

Promiseとは

MDNによると「作成された時点では分からなくてもよい値へのプロキシー」とのことです。 わかりにくい表現ですが、作成された時点では未確定な値を将来受け取るための仕掛けをもつオブジェクトです。

「Promiseを返す」とは、「結果がどうなるかわからないけど、とりあえず値を受け取れるようにしておく」程度に思っておけばよいです。

Promiseはいくつかの状態を保持します。「状態」とは、以下のいずれかとなります。

  • 待機(pending): 初期状態
  • 決定(settled): 履行または拒否された状態
    • 履行(fulfilled): 処理が成功して完了したことを表す
    • 拒否(rejected): 処理が失敗したことを表す

これとは別に解決(resolved)という用語が使われることもあります。MDNによると、

プロミスが他のプロミスの最終的な状態と一致するように決定または「ロックイン」され、さらに解決または拒否しても何の影響もないことを意味します。

とのことです。これもわかりにくいですが、「履行状態になること、もしくは他のPromiseがあればその状態と一致するように処理されること」と考えればよいです。履行と同義で使用している文献も多いですが、厳密には異なります。

コンストラクタ

Promise(executer);

executerはコールバック関数で、Promiseオブジェクトが作成されると、すぐに呼び出されます。
以下の形をとります。

function executer(resolveFunc, rejectFunc){
   :
}

引数のresolveFuncrejectFuncには、Promiseオブジェクトが作成されるときに一緒に作成される1組の関数が渡されます。それぞれ単一の引数(「履行値」または「拒否理由」)を持つことができます。
resolveFuncは履行するときに呼び出し、rejectFuncは拒否するときに呼び出します。

次の例は、Promiseベースでない非同期処理XMLHttpRequestをPromiseに対応させます。

function simpleFetch(url, timeout) {
    return new Promise((resolveFunc, rejectFunc) => {
        const xhr = new XMLHttpRequest();

        xhr.timeout = timeout;
        xhr.open("GET", url);
        xhr.setRequestHeader('Accept', 'text/html');

        xhr.onload = () => {
            const response = { data: xhr.responseText, status: xhr.status };
            resolveFunc(response);
        };
        xhr.onerror = () => {
            rejectFunc(xhr.status);
        };

        xhr.send();
    });
};

状態の決定

Promise.resolve()

resolve()は与えられた値で解決されたPromiseオブジェクトを返す静的メソッドです。引数には履行値またはPromiseオブジェクトを与えます。

Promise.resolve(value)

Promiseオブジェクトを与えた場合、与えたPromiseオブジェクトそのものを返します。状態もそのままです。

Promise.resolve()はあくまで解決するメソッドであり、必ず履行状態にするわけではないことに注意が必要です。以下の例ではp1は拒否状態になります。

const p0 = new Promise((resolve, reject) => { reject("rejected"); });
const p1 = Promise.resolve(p0);

p1.then((value) => {
    console.log(value);  // 実行されない
}).catch((reason) => {
    console.log(`catch: ${reason}`); // 実行される。catch: rejected
})

Promise.reject()

reject()は拒否状態を持つPromiseオブジェクトを返す静的メソッドです。引数には拒否理由を与えます。

Promise.reject(reason)

ハンドラの登録

then()

then()は履行/拒否時の処理をそれぞれ登録するメソッドです。登録後新しいPromiseオブジェクトを返します。

then(onFulfilled, onRejected)

Promiseが履行状態のとき、onFulfilledがコールバック関数として履行値ともに呼び出されます。拒否状態のとき、onRejectedがコールバック関数として拒否理由とともに呼び出されます。

onRejectedは省略可能です。省略された場合や関数でない値が指定された場合は、拒否理由をthrowする関数((reason) => { throw reason; })に置き換えられます。

onFulfilledは省略できませんが、関数でない値が指定された場合(例:undefined)は、履行値をそのまま返す関数((value) => value)に置き換えられます。

catch()

catch()は拒否時の処理を登録するメソッドです。登録後新しいPromiseオブジェクトを返します。
内部的にはthen()を呼び出していて、then(undefined, onRejected)に近いようです。

catch(onRejected)

finally()

finally()は履行/拒否時ともに同じ処理を登録するメソッドです。登録後新しいPromiseオブジェクトを返します。
内部的にはthen()を呼び出しています。

finally(onFinally)

onFinallyには引数を持たない関数を登録します。

Promiseチェーン

then(), catch(), finally()Promiseオブジェクトを返すことから、続けてこれらのメソッドを呼び出すことができます。これをPromiseチェーンといいます。

simpleFetch('https://izadori.net/', 1000)
    .then((response) => {
        console.log(data);
        return response.status;
    })
    .then((status) => {
        console.log(`success: ${status}`);
    })
    .catch((status) => {
        console.log(`error: ${status}`);
    })
    .finally(() => {
        console.log("connection end.");
    });

then()を複数並べたとき、前のthen()で登録した関数の戻り値は、新たな履行値として次のthen()に登録した関数で受け取ることができます。

複数のPromiseの並行処理

Promise.all()

all()は複数の非同期処理を並行に実行し、すべてが履行されたときに履行状態となる静的メソッドです。新しいPromiseオブジェクトを返します。1つでも拒否されると、すべての完了を待たずにその場で拒否状態となります。

// 3つのサイトに接続する
const p1 = simpleFetch('https://example1.com/', 1000);
const p2 = simpleFetch('https://example2.com/', 1000);
const p3 = simpleFetch('https://example3.com/', 1000);

Promise.all([p1, p2, p3])
    .then((values) => {
        // すべて接続に成功した場合はこちらが処理される
        // valuesは履行値の配列として渡される
    })
    .catch((reason) => {
        // 1つでも接続に失敗した場合はこちらが処理される
        // reasonは最初に失敗した非同期処理の拒否理由
    });

Promise.any()

any()は複数の非同期処理を並列に実行し、どれか1つでも履行されたときに履行状態となる静的メソッドです。新しいPromiseオブジェクトを返します。すべてが拒否されたときに拒否状態となります。

// 3つのサイトに接続する
const p1 = simpleFetch('https://example1.com/', 1000);
const p2 = simpleFetch('https://example2.com/', 1000);
const p3 = simpleFetch('https://example3.com/', 1000);

Promise.any([p1, p2, p3])
    .then((value) => {
        // どれか1つでも接続に成功した場合はこちらが処理される
        // valueは最初に成功した非同期処理の履行値
    })
    .catch((e) => {
        // すべて接続に失敗した場合はこちらが処理される
        // eはAggregateErrorオブジェクト
        console.log(e.errors);
    });

拒否時に渡される拒否理由はAggregateErrorオブジェクトで、そのerrorsプロパティに各非同期処理の拒否理由が配列として格納されています。

Promise.allSettled()

allSettled()は複数の非同期処理を並列に実行し、すべてが決定したときに履行状態となる静的メソッドです。新しいPromiseオブジェクトを返します。拒否状態にはなりません。

// 3つのサイトに接続する
const p1 = simpleFetch('https://example1.com/', 1000);
const p2 = simpleFetch('https://example2.com/', 1000);
const p3 = simpleFetch('https://example3.com/', 1000);

Promise.allSettled([p1, p2, p3])
    .then((results) => {
        // すべての接続結果が決定した場合に処理される
        // resultsはオブジェクト
    });

履行値はオブジェクトが渡されます。そのstatusプロパティに履行/拒否の状態が文字列("fulfilled", "rejected")で格納されています。また、status"fulfilled"の場合は、valueプロパティを持ち、履行された非同期処理の履行値が格納されています。status"rejected"の場合は、reasonプロパティを持ち、拒否された非同期処理の拒否理由が格納されています。

Promise.race()

race()は複数の非同期処理を並列に実行し、どれか1つでも決定したときに履行/拒否状態となる静的メソッドです。新しいPromiseオブジェクトを返します。どちらの状態になるかは、最初に決定した非同期処理の状態に依存します。

// 3つのサイトに接続する
const p1 = simpleFetch('https://example1.com/', 1000);
const p2 = simpleFetch('https://example2.com/', 1000);
const p3 = simpleFetch('https://example3.com/', 1000);

Promise.race([p1, p2, p3])
    .then((value) => {
        // 最初に処理が完了したものが接続に成功していた場合はこちらが処理される
        // valueは最初に処理が完了した非同期処理の履行値
    })
    .catch((reason) => {
        // 最初に処理が完了したものが接続に失敗していた場合はこちらが処理される
        // reasonは最初に処理が完了した非同期処理の拒否理由
    });

async/await

async

async functionasyncをつけて関数を定義すると、それは非同期関数として定義されます。Promiseを使わずに、非同期関数をあたかも通常の関数のように書くことができますが、実際はPromiseオブジェクトを返す関数となります。

async function asyncFunc0() {}

console.log(asyncFunc0()); // 出力: Promise { undefined }

asyncで定義された非同期関数はreturn文で値を返すことができます。この場合、実際に値を返しているのではなく、値で解決されたPromiseオブジェクトを返します。

たとえば、

async function asyncFunc1() {
    return 1;
}

は、実際にはPromise.resolve(1)を返しています。

return文がない、もしくはただ単にreturn;とした場合は、Promise.resolve(undefined)が返されます。

await

asyncで定義された非同期関数の中では、await演算子を使うことができます。await演算子を使うと、awaitを付けた式が解決されるまで、非同期関数の実行が一時的に停止されます。式が解決されたのち、履行値が戻り値として得られます。

たとえば、

async function asyncFunc2(url, timeout) {
    const resp = await simpleFetch(url, timeout);
    console.log(resp.data);
}

は、simpleFetchが解決されるまでasyncFunc2の実行が停止されます。解決後はその履行値が戻り値としてrespに代入されます。これは次の表現に近いです。

function func2(url, timeout) {
    simpleFetch(url, timeout).then((resp) = {
        console.log(resp.data);
    });
}

awaitを付けた式が拒否された場合、その場でエラーがthrowされます。このエラーは通常のtry...catch構文で捕捉できます。

async function asyncFunc3(url, timeout) {
    try {
        const resp = await simpleFetch(url, timeout);
        console.log(resp.data);
        console.log(resp.status);
    }
    catch(status) {
        console.log(status);
    }
    finally {
        console.log("connection end.");
    }
}

まとめ

JavaScriptにおけるPromiseについて調べてまとめてみました。Promiseはわかりにくいですが、非同期処理を扱ううえで避けて通れないものです。改めて調べたことで、理解の薄かったPromiseについて、理解することができました。

Promiseを使うと、コールバック関数を入り組んだ形にすることなく、比較的シンプルに書くことができます。さらにasync/awaitを使うと、非同期処理を同期的な処理と同じような書き方で書くことができ、コードが見やすくなります。

JavaScript

Posted by izadori