JavaScript|Asynchronous Processing
JavaScript における非同期処理
CONTENTS
非同期処理とは
プログラミングには、同期処理(sync)と非同期処理(async)という大きな区別があります。
- 同期処理とは、コードを書かれた順番に処理するもので、ひとつの処理が終わるまで次の処理は行いません。実行順が明らかなのでわかりやすくはなりますが、ひとつの処理が終わるまで、次の処理へ進むことができないため、効率が悪くなる(最悪そこで止まってしまう)場合があります。
- 非同期処理では処理が終わるのを待たずに次の処理を実行します。 結果、非同期処理では同時に複数の処理が実行中となります。
JavaScriptにおいて非同期処理の代表的な関数として setTimeout 関数があります。以下のコードは delay ミリ秒後に、コールバック関数を呼び出す非同期処理と説明することができます。setTimeout(コールバック関数, delay);
Promise
非同期処理の問題
非同期処理の問題は「コードの順番通りには実行されない」という点です。
例えば以下、
setTimeout(() => console.log('Hello'), 500); console.log('world!');
このコードでは、setTimeoutのリクエストを終えた直後に2行目に移動して「world」が先に表示され、500ミリ秒が経過してから「Hello」が表示されます。処理が極端に遅く進行する状況であれば「Hello world!」となるかもしれませんが、通常は「world! Hello」となります。非同期処理では、実行順序がどうなるかわからない・・という問題があるわけです。
また、「xxxxが終わったらXXXXを実行せよ」というかたちで事前に処理内容を予約する仕組みは、これが連続する状況になるとコードが一気に複雑怪奇なものになってしまいます。
Promise の概要
そこで、非同期処理をわかりやすく記述できるように導入されたのが Promise という仕組みで、以下のような手続きを踏みます。
- Promise を new して Promiseオブジェクトを作成
- Promise のコンストラクタに、実行したい処理を書いた関数を渡す
- 処理が済んだら、resolve関数を呼び出すことで終了を明示
- thenメソッドに、Promise終了後に処理したい関数を渡す
以上で「Promiseの実行後にXXXXする」という処理を書くことができます。
事例
const promise = new Promise( (resolve, reject) => { setTimeout( () => { console.log('Hello'); resolve(); }, 500 ); }); promise.then( () => console.log('world!') );
このコードの Promiseオブジェクトでは、500ミリ秒後に「hello」を表示した後 resolve関数で Promiseの終了を明示しています。Promise が resolve されると、thenメソッドに登録した関数が呼ばれて「world!」が表示されます。
resolve関数
Promiseを終了させる resolve関数には値を渡すこともできます。resolve関数に渡した値は thenメソッドで受け取ることができます。
const promise = new Promise( (resolve, reject) => { // Do Something resolve('Done!'); }); promise.then((r) => console.log(r)); Done!
reject関数
Promiseをエラー終了させる reject関数にも値を渡すことができます。
const promise = new Promise( (resolve, reject) => { // Do Something reject('Error!'); }); promise.then( () => console.log('Done!') ) // この場合は実行されない .catch( (e) => console.log(e) ); // こちらが実行される Error!
reject関数に渡された値は catchメソッドで受け取ります(Promise が reject された場合は then に登録した関数は呼ばれません)。
thenチェーン
thenメソッドをチェーンする(thenの後に更にthenをつなげて書く)ことで、複数の非同期処理を直列に書くことができます。
非同期処理が必要な場合でも、実際には順序通りの動作が求められる場合があります。最も代表的な例は XHR(XMLHttpRequest)です。XHRを利用した外部ファイルの読み込みは基本的には非同期処理になりますが、例えば、sample01.txt, sample02.txt, sample03.txt を順番通りにひとつずつ読み込むことが必要になる場合、以下のように記載することで、順に実行することができるようになります。
function openFile(url) { const p = new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.addEventListener('load', (e) => resolve(xhr)); xhr.send(); }); return p; } openFile('sample01.txt') .then((xhr) => openFile('sample02.txt')) .then((xhr) => openFile('sample03.txt')) .then((xhr) => console.log('Done!'));
async / await
await の意味
await は Promise を同期的に展開する(ように見せかける)機能です。Promiseの前に await を書くことで、Promiseの終了を待つことができます。例えば await を使わない Promise と then による記述では・・
const promise = new Promise( (resolve, reject) => { // Do Something. resolve('Hello world!'); }); let hw; promise .then((r) => hw = r) .then(() => console.log(hw));
これに await を利用すると非同期処理を同期処理のように書くことができます。上記と同様の動作を await を用いて記述すると、以下のような記述になります(ただしこのままではエラー)。
const promise = new Promise( (resolve, reject) => { // Do Something. resolve('Hello world!'); }); const hw = await promise; console.log(hw); // Hello world!
async
ただし await は「トップレベルでの使用は不可」となっていて「async がついた関数の中でしか利用できない」という条件があります。そこで、上の例を実際に動く形で書くと以下のようになります。
async function helloWorld() { const promise = new Promise( (resolve, reject) => { // Do Something. resolve('Hello world!'); }); const hw = await promise; console.log(hw); } helloWorld(); Hello world!
事例
前述の XHR(XMLHttpRequest)を用いて、sample01.txt, sample02.txt, sample03.txt を順番通りにひとつずつ読み込む事例を紹介します。
function openFile(url) { const p = new Promise( (resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.addEventListener('load', (e) => resolve(xhr)); xhr.send(); }); return p; } async function loadAllFiles() { const xhr1 = await openFile('sample01.txt'); const xhr2 = await openFile('sample02.txt'); const xhr3 = await openFile('sample03.txt'); console.log('Done!'); } loadAllFiles();