こんにちは。ECF Tech担当
Michiharu.Tです。
JavaScript中級編 第5回をお送りしたいと思います。今回は、JavaScriptによるデータのダウンロードなどで必要となる非同期通信についてご紹介します。ぜひ、日ごろの学習にお役立て頂ければと思います。
本連載の章立ての一覧については下記のリンクから確認できます。
5-1 非同期処理とは
本章では非同期処理および非同期通信について学習します。非同期処理を学習するにあたり、まずはこれまで学んできた処理について確認しましょう。
これまで学んできた処理は基本的に上から順に処理が行われ、前の処理が終わってから次の処理に進むという処理の流れをとっていました。これを同期処理と言います。
いっぽう非同期処理とは、ある処理の完了を待たずして、別の処理を同時に実行させる処理のことです。同期処理と非同期処理をイメージにすると、下のようになります。
概念だけだと理解が難しいと思いますので、プログラム例をご紹介します。
プログラム
function downLoader(data){ console.log(data + "-DL開始!"); setTimeout(() => { console.log(data + "-DL完了!"); },1000); } //非同期メソッドの呼び出し downLoader('AAA'); downLoader('BBB'); downLoader('CCC');
実行結果
AAA-DL開始! BBB-DL開始! CCC-DL開始! AAA-DL完了! BBB-DL完了! CCC-DL完了!
setTimeout
setTimeout関数はsetTimeout(function,time)
のように使います。time
で指定した時間(単位:ミリ秒)だけ待って、その後function
で指定した関数を実行します。setTimeout関数は非同期な関数です。時間が来たら引数function
の関数が動くように設定して終わりです。下図のように、引数time
の時間だけ待つ処理は行わず、次の処理に移ります。
downloader関数は、setTimeout関数を使ってデータのダウンロードを模した関数です。setTimeout関数を含んでいるため非同期的に動作します。そのためAAA-DL開始!
表示直後、AAA-DL完了!
の表示を待たずしてBBB-DL開始!
が表示されることとなります。
このプログラムの処理の流れを図に整理すると次のようになります。3つの関数呼び出しが同時に実行されているようなイメージです。これが非同期処理です。
5-2 同期をとるプログラム
それではここから、5-1で例示した3つの非同期処理の「同期をとる」プログラムを考えます。同期をとるとは、最初の処理が終わったら次の処理、といった具合に前処理の終了を待ってから次の処理を始める動きにすることを言います。
ここでは、5-1のプログラムで次のような実行結果になることを目指します。
目標の実行結果
AAA-DL開始! AAA-DL完了! BBB-DL開始! BBB-DL完了! CCC-DL開始! CCC-DL完了!
AAA,BBB,CCCそれぞれが、直前のダウンロード終了後に行われています。
それでは、3つのダウンロードの同期をとったプログラムをみてみましょう。
プログラム
function downLoader(data, nextfunc){ console.log(data + "-DL開始!"); setTimeout(() => { console.log(data + "-DL完了!"); if(nextfunc != undefined) nextfunc(); },1000); } downLoader('AAA',function(){ downLoader('BBB',function(){ downLoader('CCC',undefined); }); });
実行結果
AAA-DL開始! AAA-DL完了! BBB-DL開始! BBB-DL完了! CCC-DL開始! CCC-DL完了!
それぞれのダウンロード処理が順番よく行われていることがわかります。
プログラムを見てみましょう。5-1のプログラムとの違いの1つは、downloader関数に引数が1つ増えていることです。2つめの引数に指定された関数は下のプログラムのようにXXX-DL完了!
表示直後に呼び出されるようになっています。
console.log(data + "-DL完了!"); if(nextfunc != undefined) nextfunc();
これにより、ダウンロードが完了してから次のダウンロードに移る。という処理を実現しています。
ただし、このプログラムには1つ問題があります。それは次の部分です。
downLoader('AAA',function(){ downLoader('BBB',function(){ downLoader('CCC',undefined); }); });
2番目の引数に次に呼び出す関数を指定する必要があるため、関数呼び出しが入れ子のようになっていることがわかります。現在は3つのダウンロードの同期をとるだけなのでこれだけで済んでいますが、同期をとるべき処理が増えるたびに下のように増えていくのは好ましくありません。
downLoader('AAA',function(){ downLoader('BBB',function(){ downLoader('CCC',function(){ downLoader('DDD',function(){ ..... }); }); }); });
このプログラムをさらに改良することを目指しましょう。
5-3 Promiseオブジェクト
PromiseオブジェクトはES2015より導入されたオブジェクトです。複数の非同期処理を同期的に扱うためのオブジェクトです。本節ではこのPromiseオブジェクトを使って同期をとるプログラムを確認します。
5-3-1 基本的な使い方
Promiseオブジェクトの使い方例を具体的なプログラムを作りながら、順番に確認しましょう。
手順1
Promiseオブジェクトを返す関数を定義します。引数は任意に設定可能です。
function downLoader(data){ //ここにPromiseオブジェクトを返す処理を記述 }
手順2
Promiseオブジェクトを生成して返す処理を書きます。生成時の引数に非同期処理を行う関数を設定します。この関数は2つの引数を(resolve, reject)
とする必要があります。
function downLoader(data){ //Promiseオブジェクトを返す処理 return new Promise((resolve, reject) => { //ここに非同期処理を記述 }); }
手順3
非同期処理を書きます。処理の終了を伝える手段としてresolve(値)
、reject(値)
の2つの関数を呼び出すことができます。resolveは処理成功を表し、rejectは処理失敗を表します。引数の値
は任意のものが設定可能です。どのように使われるかは後程説明します。
function downLoader(data){ //Promiseオブジェクトを返す処理 return new Promise((resolve, reject) => { console.log(data + "-DL開始!"); setTimeout(() => { console.log(data + "-DL完了!"); resolve(data); //非同期処理成功を通知する役割 },1000); }); }
手順4
ここから実際に関数を呼び出す処理です。作成したdownloader関数を呼び出します。一般的に次のように記述します。
downloader('AAA') .then(/* 非同期処理の成功時に実行したい処理 */);
then
はPromiseオブジェクトの持つメソッドです。引数として非同期処理が成功した時に実行したい関数を登録できます。
手順5
同期させたい処理の分だけthenメソッドをつなげることで、各処理の同期をとることができます。呼び出し部分は最終的に次のようになります。
downLoader('AAA') .then(function(){ return downLoader('BBB'); }) .then(function(){ return downLoader('CCC'); });
動作は次のようになります。
AAA
のダウンロードを開始する。AAA
のダウンロードが正常終了したら、BBB
のダウンロードを開始する。BBB
のダウンロードが正常終了したら、CCC
のダウンロードを開始する。
ではプログラムの動作確認をしてみましょう。改めて、プログラム全体を示します。
プログラム
function downLoader(data){ return new Promise((resolve, reject) => { console.log(data + "-DL開始!"); //1秒後に「処理1完了!」を表示 setTimeout(() => { console.log(data + "-DL完了!"); resolve(data); },1000); }); } downLoader("AAA") .then(function(){ return downLoader("BBB"); }) .then(function(){ return downLoader("CCC"); });
実行結果
AAA-DL開始! AAA-DL完了! BBB-DL開始! BBB-DL完了! CCC-DL開始! CCC-DL完了!
5-2のプログラムと同様、3つのダウンロードが同期する処理が作成できました。この方法であれば、さらに複数のダウンロードが要求されたとしても、下のようなthenメソッド部分をつなげるだけで増やしていけます。
.then(function(){ //次の非同期処理 });
5-3-2 エラー時の処理
非同期処理の途中でエラーが発生した場合の対処方法についても確認します。始めに5-3-1のプログラム7行目のresolve(data);
をreject(data);
に変更して実行してみます。実行すると、次のようにプログラムが途中でエラー終了します。
実行結果
AAA-DL開始! AAA-DL完了! node:internal/process/promises:389 new UnhandledPromiseRejection(reason); ^ ...(以降省略)
rejectはPromiseオブジェクト内の非同期処理において、処理が異常終了したことを示します。5-3-1のプログラムは正常終了する処理resolve(data)
しかなかったので問題は発生しませんでしたが、実際には異常終了も考えられます。
異常終了に対処するには、thenメソッド呼び出しにつなげてcatchメソッドを追加します。プログラム例を示します。
プログラム
function downLoader(data){ return new Promise((resolve, reject) => { console.log(data + "-DL開始!"); //1秒後に「処理1完了!」を表示 setTimeout(() => { console.log(data + "-DL完了!"); reject(data); },1000); }); } downLoader("AAA") .then(function(){ return downLoader("BBB"); }) .then(function(){ return downLoader("CCC"); }) .catch(function(){ console.log('ダウンロードでエラーが発生しました。'); });
実行結果
AAA-DL開始! AAA-DL完了! ダウンロードでエラーが発生しました。
上のプログラムは非同期処理完了時点にreject(data)
が入っているため必ずエラー扱いとなります。catchメソッドはthenメソッド同様に記述でき、エラー発生時の処理を関数として登録できます。今回はコンソールにメッセージを表示するだけの処理としています。
catchメソッドは一連の非同期処理の最後に記述します。これにより、どの非同期処理でエラーが発生したとしてもその時点で処理を終了し、catchメソッド内のエラー処理に移行します。
5-3-3 非同期処理間のデータ受け渡し
Promiseオブジェクトを使った非同期処理間でデータの受け渡しができます。resolve(data)
やreject(data)
のdata
が、その受け渡しデータです。thenメソッドで指定する非同期処理用の関数に、引数を指定しておくことで受け取ることができます。プログラム例を示します。
プログラム
function downLoader(data){ return new Promise((resolve, reject) => { console.log(data + "-DL開始!"); //1秒後に「処理1完了!」を表示 setTimeout(() => { console.log(data + "-DL完了!"); resolve(data); },1000); }); } downLoader("AAA") .then(function(data){ return downLoader(data + "BBB"); }) .then(function(data){ return downLoader(data + "CCC"); });
実行結果
AAA-DL開始! AAA-DL完了! AAABBB-DL開始! AAABBB-DL完了! AAABBBCCC-DL開始! AAABBBCCC-DL完了!
今回の例では、thenメソッドで関数を定義する際に.then(function(data){
のように、引数data
を定義しています。この引数に、直前の非同期処理のresolve(data)
で渡したdata
が代入されるようになっています。これにより、前の非同期処理から渡されたデータを次々とつなげる処理を実現しています。
5-4 async関数とawait式
ES2017からは非同期処理の記述方法として、async関数とawait式の2つの記述方法が追加されました。5-3まで見てきたダウンロードを模したプログラムをこの2つを使って書き換えたプログラムをご紹介します。
プログラム
function downLoader(data){ return new Promise((resolve, reject) => { console.log(data + "-DL開始!"); //1秒後に「処理1完了!」を表示 setTimeout(() => { console.log(data + "-DL完了!"); resolve(data); },1000); }); } async function allDownload(){ const dataA = await downLoader("AAA"); const dataB = await downLoader("BBB"); const dataC = await downLoader("CCC"); console.log(dataA + dataB + dataC); } allDownload();
実行結果
AAA-DL開始! AAA-DL完了! BBB-DL開始! BBB-DL完了! CCC-DL開始! CCC-DL完了! AAABBBCCC
実行結果は同期を保っていることがわかります。プログラムの内容を確認しましょう。downloader関数はこれまでと変わりません。
async関数
非同期処理を行う関数を定義できます。書き方は次のように通常の関数定義の前にasync
をつけます。
async function(引数){ //処理 }
例ではallDownload関数を、async関数として定義しています。async関数内では、次に説明するawait式を使うことができます。
await式
await式は非同期処理の同期をことができる式です。次のように記述します。
await 式;
式の部分はPromiseオブジェクトと評価される必要があるため、式部分は基本的にいずれかを記述することになります。
- Promiseオブジェクトを生成(new)する式
- Promiseオブジェクトを返す関数の呼び出し
例では、Promiseオブジェクトを返すdownLoader関数の呼び出しを行っています。
await式は「Promiseオブジェクト内の非同期処理を実行し、完了を待つ。」動作をするため、allDownload関数ではこの式を利用し、同期をとる処理をスッキリとした形で書くことができています。また、変数dataA
~dataC
には、resolve(data)
のdata
が値として代入されます。
5-5 非同期通信
ここで簡単な非同期通信にチャレンジしてみましょう。実務ではaxiosなどの通信用ライブラリを使用するのが一般的ですが、今回は学習のため、標準使用可能なfetch関数を使った通信を利用します。
5-5-1 fetch関数
fetch関数による非同期通信のプログラム例を示します。
プログラム
fetch('https://tech.e3factory.com/wp-content/uploads/2024/09/fetch_test.txt') .then((response) => { if(response.ok){ return response.text(); } else { throw new Error('ステータス:' + response.status); } }) .then((text)=>{ console.log("通信データ:"); console.log(text); }) .catch((error) => { console.log("何らかのエラー"); console.log(error.message); });
実行結果(成功時)
通信データ: Hello Async!!
実行結果(失敗時)
何らかのエラー Error: ステータス:404 ...(以降省略)...
fetch関数でHTTP通信を行うことができます。基本的な呼び出し方fetch(URL)
となります。
fetch関数の戻り値はPromiseオブジェクトなので、thenメソッドを記述することになります。最初のthenメソッドでは、レスポンスが返ってきた際のコールバック関数をセットします。
.then((response) => { if(response.ok){ return response.text(); } else { throw new Error(`ステータス:${response.status}`); } })
関数内では引数responseが提供する情報を用いて処理を記述できます。通信が成功した場合(response.ok
)にボディ部を取得しています。ボディ部の取得にresponse.text()
を使用しています。こちらも非同期処理となっており、戻り値はPromiseです。したがって、さらにthenメソッドが続きます。
続くthenメソッドは次のようになっています。response.text()
の処理で取得されたボディ部のテキストを表示しています。
.then((text)=>{ console.log("通信データ:"); console.log(text); })
一方通信が失敗した場合はthrow new Error('ステータス:' + response.status);
で例外がスローされ、catchメソッドの関数処理が行われます。
5-5-2 fetch関数(async-await)
次に4-4-1のプログラムをasyncとawaitの構文を用いた例で示します。
プログラム
async function download(){ try{ //非同期処理(HTTP通信) const response = await fetch('https://tech.e3factory.com/wp-content/uploads/2024/09/fetch_test.txt'); if(response.ok){ //非同期処理(ボディ部取得) const text = await response.text(); console.log(text); } else { throw new Error(`ステータス:${response.status}`); } } catch(error){ console.log(error.message); } } //downloadの実行 download();
実行結果は5-5-1のプログラムと同様です。最初のawaitでHTTP通信を行っています。正しく通信できた場合は、await response.text();
の記述でボディ部を取得しています。