2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaScript Promiseの本メモ

Posted at

そもそもPromiseとは

Promiseは非同期処理を抽象化したオブジェクトとそれを操作する仕組みのことを言う。
JavaScriptを普通に書くと同期処理と呼ばれ上から順に文が実行されていく

しかし非同期では、一度関数として実行しておき、実行が終わった際に呼び出し元にその値を通知するという仕組み

コンストラクタ

Promiseは、コンストラクタ関数であるPromiseからインスタンスとなるpromiseオブジェクトを作成して利用する

const promise = new Promise((resolve, reject) => {
    // 非同期の処理
    // 処理が終わったら、resolve または rejectを呼ぶ
});

インスタンスメソッド

newによって生成されたpromiseオブジェクトにはpromiseの値をresolve/rejectしたときに呼ばれるコールバック関数を登録するためにpromise.then()というインスタンスメソッドがある


promise.then(onFulfilled, onRejected);

resolve(成功)した時
onFulfilled が呼ばれる

reject(失敗)した時
onRejected が呼ばれる

onFulfilled、onRejected どちらもオプショナルな引数となっています。

promise.then では成功時と失敗時の処理を同時に登録することができます。 また、エラー処理だけを書きたい場合には promise.then(undefined, onRejected) と同じ意味である promise.catch(onRejected) を使うことができます。

promise.catch(onRejected);

Promiseの状態

new Promiseでインスタンス化したpromiseオブジェクトには以下の3つの状態が存在します。

Fulfilled
resolve(成功)した時。このとき onFulfilled が呼ばれる

Rejected
reject(失敗)した時。このとき onRejected が呼ばれる

Pending
FulfilledまたはRejectedではない時。つまりpromiseオブジェクトが作成された初期状態等が該当する

new Promiseのショートカット


Promise.resolve(42).then((value) => {
    console.log(value);
});

Promise.resolve(value)という静的メソッドは、new Promise()のショートカットとなるメソッド、渡した値でFulfiledされるPromiseオブジェクトを返すメソッドと考える

Promise.reject

Promise.reject(error)は静的メソッドでnew Promise()のショートカットとなるメソッド


Promise.reject(new Error("BOOM!")).catch((error) => {
    console.error(error);
});

Promiseの実行順番


const promise = new Promise((resolve) => {
    console.log("inner promise"); // 1
    resolve(42);
});
promise.then((value) => {
    console.log(value); // 3
});
console.log("outer promise"); // 2
結果

inner promise // 1
outer promise // 2
42            // 3

JavaScriptは上から実行されていくため、まず最初にpromiseが実行される
次にresolveが実行され、.thenに42が渡される
しかし、Promiseではpromise.thenで登録する段階でpromiseの状態が決まっていても、そこで登録したコールバック関数は非同期で呼び出される仕様になっている
そのため2が先に呼ばれて、最後に3が呼ばれる

非同期コールバックを同期的に呼び出してはいけない

非同期コールバックは(たとえデータが即座に利用できても)決して同期的に使ってはならない。

非同期コールバックを同期的に呼び出すと、処理の期待されたシーケンスが乱され、 コードの実行順序に予期しない変動が生じるかもしれない。

非同期コールバックを同期的に呼び出すと、スタックオーバーフローや例外処理の間違いが発生するかもしれない。

非同期コールバックを次回に実行されるようスケジューリングするには、setTimeout のような非同期APIを使う。

同期と非同期処理の混在の問題が起きないようにするために、Promiseは常に非同期で処理されるということが仕様で定められている

Promise.then

Promiseではいくらでもメソッドチェーンをつなげて処理をつなげていくことができる


aPromise.then((value) => {
// task A
}).then((value) => {
// task B
}).catch((error) => {
    console.error(error);
});

thenで登録するコールバック関数をそれぞれtaskというものにしたときにtaskA->taskBという流れをPromiseのメソッドチェーンを使って書くことができる

Promise.catch

PromiseオブジェクトがRejectedになったときに呼ばれる関数を登録するためのメソッド


Promise.reject(new Error("message")).catch((error) => {
    // エラーハンドリング
});
// Promise#catchは次のPromise#thenと同じ意味
Promise.reject(new Error("message")).then(undefined, (error) => {
    // エラーハンドリング
});

Promise.finally

Promise.finallyメソッドは成功時、失敗時どちらの場合でも呼び出すコールバック関数を登録することができる


Promise.resolve("成功").finally(() => {
    console.log("成功時に実行される");
});
Promise.reject(new Error("失敗")).finally(() => {
    console.log("失敗時に実行される");
});

finallyメソッドのコールバック関数は引数を受け取らず、どの王な値を返してもpromise chainには影響を与えない
また、finallyメソッドは新しいpromiseオブジェクトを返し、新しいpromiseオブジェクトは呼び出し元のpromiseオブジェクトの状態をそのまま引き継ぐ


// リソースを取得中かどうかのフラグ
let isLoading = false;
function fetchResource(URL) {
    // リソース取得中フラグをONに
    isLoading = true;
    return new Promise((resolve, reject) => {
        const req = new XMLHttpRequest();
        req.open("GET", URL, true);
        req.onload = () => {
            if (200 <= req.status && req.status < 300) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = () => {
            reject(new Error(req.statusText));
        };
        req.send();
    }).finally(() => {
        // リソース取得に成功/失敗どちらの場合も取得中フラグをOFFに
        isLoading = false;
    });
}

console.log("リソースロード開始", isLoading);
fetchResource("https://httpbin.org/get").then((value) => {
    console.log("リソース取得に成功", isLoading);
    console.log(value);
}).catch((error) => {
    console.log("リソース取得に失敗", isLoading);
    console.error(error);
});
console.log("リソースロード中", isLoading);

thencatchメソッドでも実現できるが、Promise.finallyメソッドを使うことでisLoadingの代入を一箇所にまとめることができる

Promise.all

Promise.allはpromiseオブジェクトの配列を受け取り、その配列に入っているpromiseオブジェクトが全てresolveされた時に、つぎの.thenを呼び出す

function fetchURL(URL) {
    return new Promise((resolve, reject) => {
        const req = new XMLHttpRequest();
        req.open("GET", URL, true);
        req.onload = () => {
            if (200 <= req.status && req.status < 300) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = () => {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
const request = {
    comment() {
        return fetchURL("https://azu.github.io/promises-book/json/comment.json").then(JSON.parse);
    },
    people() {
        return fetchURL("https://azu.github.io/promises-book/json/people.json").then(JSON.parse);
    }
};
function main() {
    return Promise.all([request.comment(), request.people()]);
}



// 実行例
main().then((value) => {
    console.log(value);
}).catch((error) => {
    console.error(error);
});

Promise.all([request.comment(), request.people()]);というように処理を書いた場合は、request.comment()request.people()は同時に実行されるが、それぞれのpromiseの結果は、Promise.allに渡した配列の順番になる

Promise.race

Promise.allは、渡した全てのpromiseがFulfilledまたはRejectedになるまで次の処理を待ったが、Promise.raceは、どれか1つでもpromiseがFulfilledまたはRejectedになったら次の処理を実行する


// `delay`ミリ秒後にresolveする
function timerPromisefy(delay) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(delay);
        }, delay);
    });
}
// 一つでもresolve または reject した時点で終了
Promise.race([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then((value) => {
    console.log(value); // => 1
});

上記のコードだと、1ms後、32ms後、64ms後、128ms後にそれぞれpromiseオブジェクトがFulfilledとなりますが、 一番最初に1msのものがFulfilledとなった時点で、.then が呼ばれます。 また、resolve(1) が呼ばれるため value に渡される値も1となります。
Promise.raceでは、 一番最初のpromiseオブジェクト

がFulfilledとなっても、他のpromiseがキャンセルされるわけでは無いということがわかります。

Promiseで一定時間待つ

setTimeoutとPromiseでラップした関数で比較


setTimeout(() => {
    alert("100ms 経ったよ!");
}, 100);
// == ほぼ同様の動作
delayPromise(100).then(() => {
    alert("100ms 経ったよ!");
});

Promise.done

それらのライブラリでは Promise.prototype.done というような実装が存在し、 使い方は then と同じですが、promiseオブジェクトを返さないようになっています。


if (typeof Promise.prototype.done === "undefined") {
    Promise.prototype.done = function(onFulfilled, onRejected) {
        this.then(onFulfilled, onRejected).catch((error) => {
            setTimeout(() => {
                throw error;
            }, 0);
        });
    };
}
const promise = Promise.resolve();
promise.done(() => {
    JSON.parse("this is not json");
    // => SyntaxError: JSON.parse
});

AsyncFunction

Async Functionとは非同期処理を行う関数を定義する構文です。 Async Functionは通常の関数とは異なり、必ずPromiseインスタンスを返す関数を定義する構文です。

Async Functionは次のように関数の前にasyncをつけることで定義できます。 このdoAsync関数は常にPromiseインスタンスを返します。


async function doAsync() {
    return "";
}
// doAsync関数はPromiseを返す
doAsync().then((value) => {
    console.log(value); // => "値"
});

Async Functionではreturnした値の代わりに、Promise.resolve(返り値)のように返り値をラップしたPromiseインスタンスを返します。 そのため、このAsync Functionは次のように書いた場合と同じ意味になります。


// 通常の関数でPromiseインスタンスを返している
function doAsync() {
    return Promise.resolve("");
}
doAsync().then((value) => {
    console.log(value); // => "値"
});

またAsync Function内ではawait式というPromiseの非同期処理が完了するまで待つ構文が利用できます。 await式を使うことで非同期処理を同期処理のように扱えるため、Promiseチェーンで実現していた処理の流れを読みやすくかけます。

Promiseとasyncの比較

Promise


function fetchBookTitle() {
    // Fetch APIは指定URLのリソースを取得しPromiseを返す関数
    return fetch("https://azu.github.io/promises-book/json/book.json").then((res) => {
        return res.json(); // レスポンスをJSON形式としてパースする
    }).then((json) => {
        return json.title; // JSONからtitleプロパティを取り出す
    });
}

function main() {
    // `fetchBookTitle`関数は、取得したJSONの`title`プロパティでresolveされる
    fetchBookTitle().then((title) => {
        console.log(title); // => "JavaScript Promiseの本"
    });
}

main();

async/await


// `async`をつけて`fetchBookTitle`関数をAsync Functionとして定義
async function fetchBookTitle() {
    // リクエストしてリソースを取得する
    const res = await fetch("https://azu.github.io/promises-book/json/book.json");
    // レスポンスをJSON形式としてパースする
    const json = await res.json();
    // JSONからtitleプロパティを取り出す
    return json.title;
}

// `async`をつけて`main`関数をAsync Functionとして定義
async function main() {
    // `await`式で`fetchBookTitle`の非同期処理が完了するまで待つ
    // `fetchBookTitle`がresolveした値が返り値になる
    const title = await fetchBookTitle();
    console.log(title); // => "JavaScript Promiseの本"
}

main();

Async FunctionではPromiseの状態が変化するまで待つawait式という機能を利用できます。 Promiseでは結果をthenメソッドのコールバック関数で取得していたのが、await式の右辺にあるPromiseのresolveされた値が左辺の変数へと代入されます。そのため、Async Functionとawait式を使うことで非同期処理をまるで同期処理のように書けます。

Async Functionは、次のこと以外は通常の関数と同じ性質を持ちます。

  • Async Functionは必ずPromiseを返す
  • Async Function内ではawait式が利用できる

AsyncFunctionはPromiseをかえす

Async Functionとして定義した関数は必ずPromiseインスタンスを返します。 返されるPromiseインスタンスの状態は関数の返り値によって異なり、次の3つのケースが考えられます。

  • Async FunctionはPromise以外の値をreturnした場合、その返り値をもつFulfilledなPromiseを返す
  • Async FunctionがPromiseをreturnした場合、その返り値のPromiseをそのまま返す
  • Async Function内で例外が発生した場合は、そのエラーをもつRejectedなPromiseを返す

Async FunctionがPromiseをreturnした場合、その返り値のPromiseをそのまま返します。 これは、Promise#thenメソッドでRejectedなPromiseを返すことで、throw文を使わずにPromiseをrejectする方法と同じです。


// resolveFnは**Fulfilled**なPromiseインスタンスを返している
// Async Functionは自動的にPromiseを返すので、単に値を返しても同じ
async function resolveFn() {
    return Promise.resolve("");
}
resolveFn().then((value) => {
    console.log(value); // => "値"
});

// rejectFnは**Rejected**なPromiseインスタンスを返している
async function rejectFn() {
    return Promise.reject(new Error("エラーメッセージ"));
}
rejectFn().catch((error) => {
    console.log(error.message); // => "エラーメッセージ"
});

await式

Async Functionはasync/awaitとも呼ばれることがあります。 この呼ばれ方からも分かるように、Async Functionとawait式は共に利用します。

await式はAsync Function内でのみ利用できます。 await式は右辺のPromiseインスタンスがFulfilledまたはRejectedになるまで、その行(文)で非同期処理の完了を待ちます。 そしてPromiseインスタンスの状態が変わると、次の行(文)から処理を再開します。


async function asyncMain() {
    // PromiseがFulfilledまたはRejectedとなるまで待つ
    await Promiseインスタンス;
    // Promiseインスタンスの状態が変わったら処理を再開する
}

通常の処理の流れでは、非同期処理を実行した場合にその非同期処理の完了を待つことなく、次の行(次の文)を実行します。 しかしawait式では非同期処理を実行し完了するまで、次の行(次の文)を実行しません。 そのためawait式を使うことで非同期処理が同期処理のように上から下へと順番に実行するような流れで書けます。


// async functionは必ずPromiseを返す
async function doAsync() {
    // 非同期処理
}
async function asyncMain() {
    // doAsyncの非同期処理が完了するまでまつ
    await doAsync();
    // 次の行はdoAsyncの非同期処理が完了されるまで実行されない
    console.log("この行は非同期処理が完了後に実行される");
}

PromiseとAsync Funtionをくみあわせる

Async Functionとawait式でも非同期処理を同期処理のような見た目で書けます。 しかし、非同期処理は必ずしも順番に処理することが重要ではない場合があります。 その際に、forループとawait式で書くと複数の非同期処理を順番に行ってしまい無駄な待ち時間を作ってしまうコードになってしまいます。

Promise.allメソッドを使い、リソースAとリソースBを取得する非同期処理を1つのPromiseインスタンスにまとめることができます。 await式が評価するのはPromiseインスタンスであるため、await式はPromise.allメソッドなどPromiseインスタンスを返す処理と組み合わせて利用できます。


function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}

// 複数のリソースを取得しレスポンスボディの配列を返す
async function fetchResources(resources) {
    // リソースをまとめて取得する
    const promises = resources.map((resource) => {
        return dummyFetch(resource);
    });
    // すべてのリソースが取得できるまで待つ
    // Promise.allは [ResponseA, ResponseB] のように結果が配列となる
    const responses = await Promise.all(promises);
    // 取得した結果からレスポンスのボディだけを取り出す
    return responses.map((response) => {
        return response.body;
    });
}
const resources = ["/resource/A", "/resource/B"];
// リソースを取得して出力する
fetchResources(resources).then((results) => {
    console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"]
});
2
4
0

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
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?