From b3d81db25e890b8aa6bc63b5b1b3c90203adee34 Mon Sep 17 00:00:00 2001 From: azu Date: Mon, 6 May 2019 20:22:10 +0900 Subject: [PATCH] =?UTF-8?q?feat(Ch5):=20Async=20Function=E3=81=AE=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ch4_AdvancedPromises/readme.adoc | 1 - Ch5_AsyncFunction/async-function-await.adoc | 118 ++++++++ Ch5_AsyncFunction/async-function-syntax.adoc | 71 +++++ Ch5_AsyncFunction/async-function.adoc | 105 +++++++ .../promise-chain-to-async-function.adoc | 274 ++++++++++++++++++ Ch5_AsyncFunction/readme.adoc | 19 ++ json/book.json | 4 + 7 files changed, 591 insertions(+), 1 deletion(-) create mode 100644 Ch5_AsyncFunction/async-function-await.adoc create mode 100644 Ch5_AsyncFunction/async-function-syntax.adoc create mode 100644 Ch5_AsyncFunction/async-function.adoc create mode 100644 Ch5_AsyncFunction/promise-chain-to-async-function.adoc create mode 100644 Ch5_AsyncFunction/readme.adoc create mode 100644 json/book.json diff --git a/Ch4_AdvancedPromises/readme.adoc b/Ch4_AdvancedPromises/readme.adoc index bcda97b90..2354a6b97 100644 --- a/Ch4_AdvancedPromises/readme.adoc +++ b/Ch4_AdvancedPromises/readme.adoc @@ -7,7 +7,6 @@ // promiseとレビュー include::promise-library.adoc[] - // Promise.resolveとthenable include::resolve-thenable.adoc[] diff --git a/Ch5_AsyncFunction/async-function-await.adoc b/Ch5_AsyncFunction/async-function-await.adoc new file mode 100644 index 000000000..dfb4b4887 --- /dev/null +++ b/Ch5_AsyncFunction/async-function-await.adoc @@ -0,0 +1,118 @@ +[async-function-await] +== ``await``式 + +Async Functionのことは一般的にasync/awaitとも呼ばれることがあります。 +この呼ばれ方からもわかるようにAsync Functionにおける``await``式は重要な役割を持っています。 + +``await``式はAsync Functionの関数内でのみ利用できます。 +``await``式は右辺の``Promise``インスタンスが**Fulfilled**または**Rejected**になるまでその場で非同期処理の完了を待ちます。 +そして``Promise``インスタンスの状態が変わると、処理を再開します。 + + +[source,js] +---- +async function asyncMain() { + // PromiseがFulfilledまたはRejectedとなるまで待つ + await Promiseインスタンス; + // Promiseインスタンスの状態が変わったら処理を再開する +} +---- + +普通の処理の流れでは、非同期処理を実行した場合にその非同期処理の完了を待つことなく、次の行(次の文)を実行します。 +しかし``await``式では非同期処理を実行し完了するまで、次の行(次の文)を実行しません。 +そのため``await``式を使うことで非同期処理が同期処理のように上から下へと順番に実行するような処理順で書けます。 + + +[source,js] +---- +// async functionは必ずPromiseを返す +async function doAsync() { + // 非同期処理 +} +async function asyncMain() { + // doAsyncの非同期処理が完了するまでまつ + await doAsync(); + // 次の行はdoAsyncの非同期処理が完了されるまで実行されない + console.log("この行は非同期処理が完了後に実行される"); +} +---- + +``await``式は**式**であるため右辺(``Promise``インスタンス)の評価結果を値として返します。 +この``await``式の評価方法は評価するPromiseの状態(**Fulfilled**または**Rejected**)によって異なります。 + +``await``式の右辺のPromiseが**Fulfilled**となった場合は、resolveされた値が``await``式の返り値となります。 + +次のコードでは、``await``式の右辺にある``Promise``インスタンスは``42``という値でresolveされています。 +そのため``await``式の返り値は``42``となり、``value``変数にもその値が入ります。 + +[role="executable"] +[source,javascript] +---- +async function asyncMain() { + const value = await Promise.resolve(42); + console.log(value); // => 42 +} +asyncMain(); // Promiseインスタンスを返す +---- + +これはPromiseを使って書くと次のコードと同様の意味となります。 +``await``式を使うことでコールバック関数を使わずに非同期処理の流れを表現できていることがわかります。 + +[role="executable"] +[source,javascript] +---- +function asyncMain() { + return Promise.resolve(42).then(value => { + console.log(value); // => 42 + }); +} +asyncMain(); // Promiseインスタンスを返す +---- + +``await``式の右辺のPromiseが**Rejected**となった場合は、その場でエラーを``throw``します。 +またAsync Function内で発生した例外は自動的にキャッチされます。 +そのため``await``式でPromiseが**Rejected**となった場合は、そのAsync Functionが**Rejected**なPromiseを返すことになります。 + +次のコードでは、``await``式の右辺にある``Promise``インスタンスが**Rejected**の状態になっています。 +そのため``await``式は``エラー``を``throw``するため、``asyncMain``関数は**Rejected**なPromiseを返します。 + +[role="executable"] +[source,javascript] +---- +async function asyncMain() { + const value = await Promise.reject(new Error("エラーメッセージ")); + // await式で例外が発生したため、この行は実行されません +} +// Async Functionは自動的に例外をキャッチできる +asyncMain().catch(error => { + console.log(error.message); // => "エラーメッセージ" +}); +---- + +``await``式がエラーを``throw``するということは、そのエラーは``try...catch``構文でキャッチできます。 +通常の非同期処理では完了する前に次の行が実行されてしまうため``try...catch``構文ではエラーをキャッチできませんでした。 +そのためPromiseでは``catch``メソッドを使いPromise内で発生したエラーをキャッチしていました。(<>を参照) + +次のコードでは、``await``式で発生した例外を``try...catch``構文でキャッチしています。 + +{{book.console}} +// doctest:async:16 + +[source,js] +---- +async function asyncMain() { + // await式のエラーはtry...catchできる + try { + const value = await Promise.reject(new Error("エラーメッセージ")); + // await式で例外が発生したため、この行は実行されません + } catch (error) { + console.log(error.message); // => "エラーメッセージ" + } +} +asyncMain().catch(error => { + // すでにtry...catchされているため、この行は実行されません +}); +---- + +このように``await``式を使うことで、``try...catch``構文のように非同期処理を同期処理と同じ構文を使って扱えます。 +またコードの見た目も同期処理と同じように、その行(その文)の処理が完了するまで次の行を評価しないという分かりやすい形になるのは大きな利点です。 diff --git a/Ch5_AsyncFunction/async-function-syntax.adoc b/Ch5_AsyncFunction/async-function-syntax.adoc new file mode 100644 index 000000000..062dde6dd --- /dev/null +++ b/Ch5_AsyncFunction/async-function-syntax.adoc @@ -0,0 +1,71 @@ +[async-function-syntax] +== Async Functionの構文 + +Async Functionは関数の定義に``async``キーワードをつけることで定義できます。 +JavaScriptの関数定義には関数宣言や関数式、Arrow Function、メソッドの短縮記法などがあります。 +どの定義方法でも``async``キーワードを前につけるだけでAsync Functionとして定義できます。 + +[role="executable"] +[source,javascript] +---- +// 関数宣言のAsync Function版 +async function fn1() {} +// 関数式のAsync Function版 +const fn2 = async function() {}; +// Arrow FunctionのAsync Function版 +const fn3 = async() => {}; +// メソッドの短縮記法のAsync Function版 +const object = { async method() {} }; +---- + +これらのAsync Functionは、必ずPromiseを返すことや関数中では``await``式が利用できること以外は、通常の関数と同じ性質を持ちます。 + +[#async-function-return-promise] +== Async FunctionはPromiseを返す + +Async Functionとして定義した関数は必ず``Promise``インスタンスを返します。 +具体的にはAsync Functionが返す値は次の3つのケースが考えられます。 + +1. Async Functionは値をreturnした場合、その返り値をもつ**Fulfilled**なPromiseを返す +2. Async FunctionがPromiseをreturnした場合、その返り値のPromiseをそのまま返す +3. Async Function内で例外が発生した場合は、そのエラーをもつ**Rejected**なPromiseを返す + +次のコードでは、Async Functionがそれぞれの返り値によってどのような``Promise``インスタンスを返すかを確認できます。 +これらの挙動は``Promise#then``メソッドの返り値とそのコールバック関数が返す値の関係とほぼ同じです。 + +[role="executable"] +[source,javascript] +---- +// 1. resolveFnは値を返している +// 何もreturnしていない場合はundefinedを返したのと同じ扱いとなる +async function resolveFn() { + return "返り値"; +} +resolveFn().then(value => { + console.log(value); // => "返り値" +}); + +// 2. rejectFnはPromiseインスタンスを返している +async function rejectFn() { + return Promise.reject(new Error("エラーメッセージ")); +} + +// rejectFnはRejectedなPromiseを返すのでcatchできる +rejectFn().catch(error => { + console.log(error.message); // => "エラーメッセージ" +}); + +// 3. exceptionFnは例外を投げている +async function exceptionFn() { + throw new Error("例外が発生しました"); + // 例外が発生したため、この行は実行されません +} + +// Async Functionで例外が発生するとRejectedなPromiseが返される +exceptionFn().catch(error => { + console.log(error.message); // => "例外が発生しました" +}); +---- + +どの場合でもAsync Functionは必ずPromiseを返すことがわかります。 +このようにAsync Functionを呼び出す側から見れば、Async FunctionはPromiseを返すただの関数と何も変わりません。 diff --git a/Ch5_AsyncFunction/async-function.adoc b/Ch5_AsyncFunction/async-function.adoc new file mode 100644 index 000000000..90a5ea2b1 --- /dev/null +++ b/Ch5_AsyncFunction/async-function.adoc @@ -0,0 +1,105 @@ +[[async-function]] +== Async Functionとは + +Async Functionとは非同期処理を行う関数を定義する構文です。 +Async Functionは通常の関数とは異なり、必ず``Promise``インスタンスを返す関数を定義する構文です。 + +Async Functionは次のように関数の前に``async``をつけることで定義できます。 +この``doAsync``関数は常に``Promise``インスタンスを返します。 + +[role="executable"] +[source,javascript] +---- +async function doAsync() { + return "値"; +} +// doAsync関数はPromiseを返す +doAsync().then(value => { + console.log(value); // => "値" +}); +---- + +Async Functionでは``return``した値の代わりに、``Promise.resolve(返り値)``のように返り値をラップした``Promise``インスタンスを返します。 +そのため、このAsync Functionは次のように書いた場合と同じ意味になります。 + +[role="executable"] +[source,javascript] +---- +// 通常の関数でPromiseインスタンスを返している +function doAsync() { + return Promise.resolve("値"); +} +doAsync().then(value => { + console.log(value); // => "値" +}); +---- + +またAsync Function内では``await``式というPromiseの非同期処理が完了するまで待つ構文が利用できます。 +``await``式を使うことで非同期処理を同期処理のように扱えるため、Promiseチェーンで実現していた処理の流れを読みやすくかけます。 + +まずは、Async Functionと``await``式を使った場合はどのように書けるかを簡単に見ていきます。 + +次の例ではFetch APIで``/json/book.json``を取得して、``title``を取り出す``getBookTitle``関数の実行結果をコンソールに出力しています。 +取得``book.json``は次のような内容になっています。 + +[[book.json]] +./json/book.json +[source,json] +---- +include::../json/book.json[] +---- + +次のコードでは、Promiseのみを使って`getBookTitle``関数で取得したタイトルをコンソールに出力しています。 + +[role="executable"] +[source,javascript] +---- +function getBookTitle(){ + return fetch("/json/book.json").then(function(res){ + return res.json(); // レスポンスをJSON形式としてパースする + }).then(json => { + return json.title; // JSONからtitleプロパティを取り出す + }); +} + +function main(){ + getBookTitle().then(function(title) => { + console.log(title); // => "JavaScript Promiseの本" + }); +} + +main(); +---- + +次のコードでは、Async Functionと``await``式を使って`getBookTitle``関数で取得したタイトルをコンソールに出力しています。 + +[role="executable"] +[source,javascript] +---- +function getBookTitle(){ + return fetch("/json/book.json").then(function(res){ + return res.json(); // レスポンスをJSON形式としてパースする + }).then(json => { + return json.title; // JSONからtitleプロパティを取り出す + }); +} + +// `async`をつけてAsync Functionを定義 +async function main(){ + // `await`式で`getBookTitle`の非同期処理が完了するまで待つ + // `getBookTitle`がresolveした値が返り値になる + const title = await getBookTitle(); + console.log(title); // => "JavaScript Promiseの本" +} + +main(); +---- + +Async Functionでは非同期処理が完了するまで待つ``await``式を使うことができます。 +これにより、Promiseでは結果を``then``メソッドのコールバック関数で取得していたのが、``await``式によって同期的な関数のように結果を受け取れます。 +このように、Async Functionと``await``式を使うことで非同期処理をまるで同期処理のように書けます。 + +この章では、Async Functionと``await``式について見ていきます。 + +重要なこととしてAsync FunctionはPromiseの上に作られた構文です。 +そのためAsync Functionを理解するには、Promiseを理解する必要があることに注意してください。 diff --git a/Ch5_AsyncFunction/promise-chain-to-async-function.adoc b/Ch5_AsyncFunction/promise-chain-to-async-function.adoc new file mode 100644 index 000000000..ffa44460e --- /dev/null +++ b/Ch5_AsyncFunction/promise-chain-to-async-function.adoc @@ -0,0 +1,274 @@ + +[promise-chain-to-async-function] +== Async Functionと配列 + +<>のように、配列を元にした複数の非同期処理を扱う場合のAsync Functionについて見ていきます。 + +たとえば、次のような複数のリソースを順番に取得する処理をPromiseで書いてみます。 +``fetchResources``関数では、``Array#reduce``メソッドを使った逐次処理を実装することで、複数の非同期処理を順番に実行できます。 +(<>を参照) + +[role="executable"] +[source,javascript] +---- +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()); + }); +} +// リソースAとリソースBを順番に取得する +function fetchResources(resources) { + const results = []; + return resources.reduce(function (promise, resource) { + return dummyFetch(resource).then(function(response) { + results.push(response.body); + }); + }, Promise.resolve()); +} +const resources = ["/resource/A", "/resource/B"]; +// リソースを取得して出力する +fetchResources().then((results) => { + console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"] +}); +---- + +このコードと同じ処理をAsync Functionと``await``式で書いてみます。 +Async Functionとして定義した``fetchResources``関数では、forループの中で`await`式を使うことで順番にリソースを取得する非同期処理を実行できます。 + +[role="executable"] +[source,javascript] +---- +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()); + }); +} +// リソースAとリソースBを順番に取得する +async function fetchResources(resources) { + const results = []; + for (let i = 0; i < resources.length; i++) { + const resource = resources[i]; + const response = await dummyFetch(resource); + results.push(response.body); + } + return results; +} +const resources = ["/resource/A", "/resource/B"]; +// リソースを取得して出力する +fetchResources(resources).then((results) => { + console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"] +}); +---- + +Promiseチェーンで``fetchResources``関数書いた場合は、コールバックの中で処理するためややこしい見た目になりがちです。 +一方、Async Functionと``await``式で書いた場合は、取得と追加を順番に行うだけとなりネストがなく見た目はシンプルです。 + +[await-in-async-function] +=== ``await``式はAsync Functionの中でのみ利用可能 + +先ほどの``fetchResources``関数ではforループを利用していました。 +この時、配列の反復処理に``Array#forEach```メソッドを利用したくなるかもしれません。 + +しかし、次のようにforループを``Array#forEach``に変更するだけでは、構文エラー(Syntax Error)となってしまいます。 + +[role="executable"] +[source,javascript] +---- +// リソースAとリソースBを順番に取得する +async function fetchResources(resources) { + const results = []; + // Syntax Errorとなる例 + resources.forEach(function(resources) { + const resource = resources[i]; + // Async Functionではないスコープで`await`式を利用しているためSyntax Errorとなる + const response = await dummyFetch(resource); + results.push(response.body); + }); + return results; +} +---- + +これは、``await``式はAsync Functionの直下でのみで利用可能であるからです。 + +Async Functionではない通常の関数で``await``式を使うとSyntax Errorとなります。 +これは、間違った``await``式の使い方を防止するための仕様です。 + +[role="executable"] +[source,javascript] +---- +function main(){ + // Syntax Error + await Promise.resolve(); +} +---- + +次に、Async Function内で``await``式を使って処理を待っている間も、関数の外側では通常通り処理が進みます。 +次のコードを実行してみると、Async Function内で``await``しても、Async Function外の処理は停止していないことがわかります。 + +[role="executable"] +[source,javascript] +---- +async function asyncMain() { + // 中でawaitしても、Async Functionの外側の処理まで止まるわけではない + await new Promise((resolve) => { + setTimeout(resolve, 16); + }); +}; +console.log("1. asyncMain関数を呼び出します"); +// Async Functionは外から見れば単なるPromiseを返す関数 +asyncMain().then(() => { + console.log("3. asyncMain関数が完了しました"); +}); +// Async Functionの外側の処理はそのまま進む +console.log("2. asyncMain関数外では、次の行が同期的に呼び出される"); +---- + +このように``await``式を非同期処理を一時停止しても、Async Function外の処理が停止するわけではありません。 +Async Function外の処理も停止できてしまうと、JavaScriptでは基本的にメインスレッドで多くの処理をするためのUIを含めた他の処理が止まってしまいます。 +これが``await``式がAsync Functionの範囲外で利用できない理由の一つです。 + + +``await``式はAsync Functionの中でのみ利用可能なため、コールバック関数もAsync Functionとして定義しないと``await``式が利用できないことに注意してください。 + +そのため、``fetchResources``関数の``Array#forEach``メソッドのコールバック関数に対して``async``キーワードをつける必要があります。 +この場合は、コールバック関数がAsync Functionとなるため、コールバック関数内で``await``式が利用できます。 +しかし、コールバック関数をAsync Functionに修正するだけでは、構文エラーはおきなくなりますが、``fetchResources``関数が意図した結果を返しません。 + +[role="executable"] +[source,javascript] +---- +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 results = []; + resources.forEach(async function(resource) { + const response = await dummyFetch(resource); + results.push(response.body); + }); + return results; +} +const resources = ["/resource/A", "/resource/B"]; +// リソースを取得して出力する +fetchResources(resources).then((results) => { + // resultsは空になってしまう + console.log(results); // => [] +}); +---- + +なぜなら、``forEach``メソッドのコールバック関数としてAsync Functionを渡しています。 +Async Functionの中で``await``式を利用して非同期処理の完了を待っています。 +しかし、この非同期処理の完了を待つのはコールバック関数Async Functionの中だけで、外側では``fetchResources``関数の処理が進んでいます。 +そのため、コールバック関数で``results``に結果を追加する前に、``fetchResources``関数は空の``results``を返してしまいます。 + +次のように``fetchResources``関数にコンソール出力を入れてみると動作が分かりやすいでしょう。 +``forEach``メソッドのコールバック関数が完了するのは、``fetchResources``関数の呼び出しがすべて終わった後になります。 +そのため``await``式で``dummyFetch``関数の完了を待ったつもりでも、``fetchResources``関数は先に空の``results``を返してしまいます。 + +[role="executable"] +[source,javascript] +---- +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 results = []; + console.log("1. fetchResourcesを開始"); + resources.forEach(async function(resource) { + console.log(`2. ${resource}を取得開始`); + const response = await dummyFetch(resource); + console.log(`2. ${resource}を取得完了`) + results.push(response.body); + }); + console.log("1. fetchResourcesを終了"); + return results; +} +const resources = ["/resource/A", "/resource/B"]; +// リソースを取得して出力する +fetchResources(resources).then((results) => { + console.log(results); // => [] +}); +---- + +この問題を解決する方法として、先ほどのようにコールバック関数を定義する必要がないforループを使う方法があります。 +また、別の方法として``Promise.all``メソッドを使う方法があります。 + +[relationship-promise-async-function] +=== PromiseとAsync Functionを組み合わせる + +Async Functionと``await``式でも非同期処理を同期処理のような見た目で書けます。 +一方で同期処理のような見た目となるため、複数の非同期処理を順番に行うようなケースでは無駄な待ち時間を作ってしまうコードを書きやすいです。 + +先ほど``fetchResources``関数ではリソースAを取得し終わってから、リソースBを取得していました。 +このとき、取得順が関係無い場合はリソースAとリソースBを同時に取得できます。 + +Promiseチェーンでは``Promise.all``メソッドを使い、リソースAとリソースBを取得する非同期処理を1つの``Promise``インスタンスにまとめることで同時に取得していました。 +``await``式が評価するのは``Promise``インスタンスであるため、``await``式は``Promise.all``メソッドなど``Promise``インスタンスを返す処理と組み合わせて利用できます。 + +そのため、先ほど``fetchResources``関数でリソースを同時に取得する場合は、次のように書けます。 +``Promise.all``メソッドは複数のPromiseを配列で受け取り、それを1つのPromiseとしてまとめたものを返す関数です。 +``Promise.all``メソッドの返す``Promise``インスタンスを``await``することで、非同期処理の結果を配列としてまとめて取得できます。 + +[role="executable"] +[source,javascript] +---- +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()); + }); +} +// リソースAとリソースBを同時に取得する +async function fetchResources() { + // Promise.allは [ResponseA, ResponseB] のように結果を配列にしたPromiseインスタンスを返す + const responses = await Promise.all([ + dummyFetch("/resource/A"), + dummyFetch("/resource/B") + ]); + return responses.map(response => { + return response.body; + }); +} +// リソースを取得して出力する +fetchResources().then((results) => { + console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"] +}); +---- + +このようにAsync Functionや``await``式は既存のPromiseと組み合わせて利用できます。 +Async Functionも内部的にPromiseの仕組みを利用しているため、両者は対立関係ではなく共存関係です。 diff --git a/Ch5_AsyncFunction/readme.adoc b/Ch5_AsyncFunction/readme.adoc new file mode 100644 index 000000000..ff38b74e5 --- /dev/null +++ b/Ch5_AsyncFunction/readme.adoc @@ -0,0 +1,19 @@ +[[chapter5-async-function]] += Chapter.5 - Async Function +:imagesdir: Ch5_AsyncFunction + +この章では、ECMAScript 2017で導入されたAsync Function(`async`/`await`)について学んでいきます。 + +// Async Functionとは +include::async-function.adoc[] + +// Async Function Overview +include::async-function-syntax.adoc[] + +// `await`式 +include::async-function-await.adoc[] + +// Promise Chainと await式 +include::promise-chain-to-async-function.adoc[] + +// Async Generatorによる逐次処理 diff --git a/json/book.json b/json/book.json new file mode 100644 index 000000000..42fbbf10f --- /dev/null +++ b/json/book.json @@ -0,0 +1,4 @@ +{ + "title": "JavaScript Promiseの本", + "repository": "https://github.com/azu/promises-book" +} \ No newline at end of file