# JavaScript 基礎編9(学習日:9/24,25)(非同期処理) ## 非同期処理 ### 同期処理と非同期処理 これまで学習してきたものは全て同期処理(sync)で、1つの処理が終わらないと次の処理が実行できなかった 非同期処理(async)は、複数の処理を同時に実行可能である。 非同期処理の代表例としてsetTimeout関数がある。 同期処理と非同期処理は、基本的にどちらもメインスレッドで処理される。 また、両処理は、一定単位ごとに処理を切り替えながら実行する並行処理(concurrent)として扱われるため、非同期処理は非同期的なタイミングで実行される。 setTimeout関数のように、非同期処理の呼び出しより前に同期処理の呼び出しが完了する場合もあれば、逆に同期処理が重いと非同期処理の切り替えが遅れる場合もある。 ```javascript= // 指定した`timeout`ミリ秒経過するまで同期的にブロックする関数 function blockTime(timeout) { const startTime = Date.now(); while (true) { const diffTime = Date.now() - startTime; if (diffTime >= timeout) { return; // 指定時間経過したら関数の実行を終了 } } } console.log("1. setTimeoutのコールバック関数を10ミリ秒後に実行します"); setTimeout(() => { console.log("3. ブロックする処理を開始します"); blockTime(1000); // 他の処理を1秒間ブロックする console.log("4. ブロックする処理が完了しました"); }, 10); // ブロックする処理は非同期なタイミングで呼び出されるので、次の行が先に実行される console.log("2. 同期的な処理を実行します"); ``` ### 非同期処理と例外処理 例外処理のtry…catch文は、通常同期処理のため、同期処理終了後に非同期処理が実行されても例外をcatchできない。 したがって、非同期処理処理でtry…catch文を実行するには、非同期処理内に定義する必要がある。この場合、非同期処理外では、非同期処理内の例外は判別できない。 ```javascript= // 非同期処理の外 setTimeout(() => { // 非同期処理の中 try { throw new Error("エラー"); } catch (error) { console.log("エラーをキャッチできる"); } }, 10); console.log("この行は実行されます"); ``` ### エラーファーストコールバック ES2015以前は、コールバック関数自体に例外処理機能を持たせていた。 非同期処理内の例外を非同期処理外に伝える共通のルールの一つである。 * 処理が失敗した場合は、コールバック関数の1番目の引数にエラーオブジェクトを渡して呼び出す * 処理が成功した場合は、コールバック関数の1番目の引数にはnullを渡し、2番目以降の引数に成功時の結果を渡して呼び出す ### Promise<ES2015以降対応> Promiseは、ES2015に対応したビルドインオブジェクトである。 エラーファーストコールバックで行っていた事項をオブジェクト化し、非同期処理を統一的なインターフェースで扱うのが目的である。 * Promiseインスタンスの作成 Promiseインスタンスは、newで作成する。非同期判定はコールバック関数executorにより判定される。(処理成功→resolve、処理失敗→reject呼び出し) ```javascript= // 非同期処理の外 count executor=(resolve, reject) => { // 非同期処理成功→resolveを返す。 // 非同期処理失敗→rejectを返す。 }; count promise = new Promise(executor); ``` * promise.thenとpromise.catch  promise(リソース).then(onFullfilled, onRejected); promiseでリソースが定義されているか判断した後、thenで コールバック関数を呼び出し、存在するか否かを判定する。 失敗時の処理のみを登録する場合は、catchの利用が推奨される。 * promiseと例外 promise中に例外が発生した場合、自動的にキャッチされて失敗扱い(reject呼び出しと同じ)となる。 ### promiseの状態 promiseには次の3種類の状態がある。 * Fullfilled:成功(resolve)状態。この時、onFullfilledが呼ばれる。 * Rejected:失敗(reject)及び例外状態。この時、onRejectedが呼ばれる。 * Pending:上記2つ以外の状態。newで新規作成したときの状態。 一度FullfilledかRejectedになれば、その後は変化しない。(Settled) ### promiseとシンタックスシュガー promiseは原則としてnewで呼び出すが、次のようにも記述できる。これを糖衣構文(シンタックスシュガー)という。 * Promise.resolve 成功状態のインスタンスを作成する。 ```javascript= //成功状態のインスタンスを作成。 const fullfilledPromise = Promise.resolve(); promise.then(()=>{ console.log("2.コールバック関数が実行されました") }); console.log("1.同期的な処理が実行されました") //=>同期処理=>非同期処理の順に呼ばれる。 ``` * Promise.reject 失敗状態のインスタンスを作成する。 ### Promiseチェーン Promise.thenではその都度新しいPromiseインスタンスを返す。これを利用して、連続で.then処理を登録できる。これをPromiseチェーンという。 * resolve時→成功処理が呼び出される。 * rejectまたは例外発生時→最も近い失敗処理(thenの第二引数or catch)が呼び出される。但し、一度失敗がcatchされた後は、新たに失敗しない限り成功処理が行われる。 ```javascript= // ランダムでFulfilledまたはRejectedの`Promise`インスタンスを返す関数 function asyncTask() { return Math.random() > 0.5 ? Promise.resolve("成功") : Promise.reject(new Error("失敗")); } // asyncTask関数は新しい`Promise`インスタンスを返す asyncTask() // thenメソッドは新しい`Promise`インスタンスを返す .then(function onFulfilled(value) {  console.log(value); // => "成功" }) // catchメソッドは新しい`Promise`インスタンスを返す .catch(function onRejected(error) { console.log(error.message); // => "失敗" }); ``` Promiseチェーンは、コールバックで返した値を次のコールバックへ引数として渡すことができる。 この場合、失敗処理をcatchした後は成功処理が続く。 Promiseインスタンスをコールバックで返すと、同じ状態のPromiseインスタンスが引数となる。 したがって、失敗処理のインスタンスをコールバックすると、then.メソッドチェーンが続く限り、延々と失敗処理が返る。 したがって、複数の非同期処理を順番に処理する逐次処理は、thenで非同期処理をつなげることで実現できる。 ```javascript= //(1) 値を返す場合 Promise.resolve(1).then((value) => { console.log(value); // => 1 return value * 2; }).then(value => { console.log(value); // => 2 return value * 2; }).then(value => { console.log(value); // => 4 // 値を返さない場合は undefined を返すのと同じ }).then(value => { console.log(value); // => undefined }); //(2) 失敗のコールバック関数を返す場合 Promise.resolve().then(function onFulfilledA() { return Promise.reject(new Error("失敗")); }).then(function onFulfilledB() { console.log("onFulfilledBは呼び出されません"); }).catch(function onRejected(error) { console.log(error.message); // => "失敗" }).then(function onFulfilledC() { console.log("onFulfilledCは呼び出されます"); }); ``` ※ES2018以降は、Promise.finallyが利用できる。成功・失敗によらず、行う処理を記述できる。 ### Promise.allとPromise.race * Promise.all promise.allで複数のPromiseを用いた非同期処理を、ひとつのPromiseとした扱える。 複数のPromiseがすべてFullfilledの場合のみ返り値はFullfilledとなる。 一方、ひとつでもRejectの場合、返り値はRejectedとなる。 ```javascript= // `timeoutMs`ミリ秒後にresolveする function delay(timeoutMs) { return new Promise((resolve) => { setTimeout(() => { resolve(timeoutMs); }, timeoutMs); }); } const promise1 = delay(1); const promise2 = delay(2); const promise3 = delay(3); Promise.all([promise1, promise2, promise3]).then(function(values) { console.log(values); // => [1, 2, 3] }); ``` * promise.race Promise.raceは、複数のPromiseのうち、1つでもPromiseが完了しSettle状態となった時点で次の処理を実行する。 複数のPromiseを競争させることで、タイムアウト処理などが実現できる。 ### Async Function<ES2017以降対応> Async Functionは、非同期処理を行う関数を定義する構文である。 返り値は必ずPromiseインスタンスとなる。 ```javascript= async function doAsync() { return "値"; } // doAsync関数はPromiseを返す doAsync().then(value => { console.log(value); // => "値" }); ``` * await式 awaitの右辺に記述するPromiseの状態が変化するまで、その後の処理を一時停止する指令を含んだ式。 通常は非同期処理の結果に関係なく次の行が処理されるが、await文で指定した非同期処理の結果が出る(Fulfilled or Rejected)までその後の処理が保留される。 await式で指定したPromiseインスタンスの結果が、Async Functionの結果に直結する。 事実上Promiseチェーンの代替として利用できるため、同期処理のような見た目でコードを書ける利点がある。 ```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 fetchAB() { const results = []; const responseA = await dummyFetch("/resource/A"); results.push(responseA.body); const responseB = await dummyFetch("/resource/B"); results.push(responseB.body); return results; } // リソースを取得して出力する fetchAB().then((results) => { console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"] }); ``` ### Async Functionの応用と注意点 * for文等の繰り返し構文が利用可能である。 * promise.all, promise.race式が利用可能である。 * await式はAsync Function内のみで有効である。 非同期処理がメインスレッドで実行される都合上、Async Function外の処理まで止まると悪影響を及ぼすため。 * Async Functionとコールバック関数の組み合わせができない場合がある。 ###### tags: `JavaScript`