今回は「非同期処理」について見ていきましょう。
JavaScriptの概念の中でも非常に重要な位置づけとなるものの一つです。
JavaScriptを初めて学習する時につまづきやすいポイントは幾つもあるのですが、その中でも最もつまづきやすいのが今回ご紹介する「非同期処理」だと思います。
私自身も完全に理解しているわけではないので、ぜひ一緒に学習していきましょう。
非同期処理と同期処理
非同期処理とは
「非同期処理」と言うからには、当然「同期処理」というものもあります。
実はこの「同期処理」と言うのは、ここまで学習してきたものの殆どが該当します。
では「同期処理」とは何かというと、コードを順番に処理していくことです。
当たり前のような話ですが、ただ順番に処理していくだけでなく、処理しているものは一つだけです。
一つの処理が終わったら、次の処理を実行するという処理を行っています。
では「非同期処理」とは何かというと、一つの処理が終わる前に、他の処理を実行することを言います。
つまり、ある処理が行われている時に、その処理が終わる前に、他の処理を先に行うことを言います。
(並列処理ではなく、割り込み処理という感じです)
非同期処理の必要性
先程の説明でお分かり頂けると思いますが、コードを順番に処理していくということは、すべてのコードを処理するまでに時間がかかります。
小さなプログラムであれば問題ありませんが、大きなプログラムであればWebサイトを操作する時に遅延が発生する場合もあります。
(例えば画面をスクロール出来ないとか、、)
また後に書かれたコードが単純な計算をするだけのものであり、実行すればすぐに処理が終わるようなものであっても、最初に書かれたコードが複雑だとその処理が終了するまで待たなくてはなりません。
こういった非効率な処理を解消するために、「非同期処理」が必要になるのです。
JavaScriptはシングルスレッド
ここですべてを理解する必要はありませんが、「thread(スレッド)」について頭の片隅に置いておいてください。
スレッドとは「糸」のことであり、ここでは一つのプログラムが実行される処理の流れだと思ってください。
そしてスレッドには、「シングルスレッド」と「マルチスレッド」があり、それぞれの違いは、処理の過程で枝分かれするかどうかです。
JavaScriptはシングルスレッドになりますので、枝分かれして同時に処理することが出来ません。
そのため、「非同期処理」が必要になるのです。
スレッドについてとても簡単に書いていますが、実際はもう少し複雑で分かりにくいものになります。
いずれスレッドの概念も必要になりますので、以下のページも時間のある時にお読みになってください。
MDN:Thread (スレッド)
Promise
基本
Promiseには3つの状態があります。
- pending(待機):初期状態
- fulfilled(履行):処理が成功して完了(終了)した状態
- rejected(拒否);処理が失敗(異常終了)した状態
実際の例を見ていきましょう。
new Promise(function(resolve,reject){ }); Promise {<pending>}
基本的なPromiseの構文はこのようになります。
「new Promise()」でオブジェクトを作成し、引数に関数を指定します。
- resolve:非同期処理が正常終了した場合
- reject:非同期処理が異常終了した場合
この状態でコンソールに出力させると、「pending」の状態であることが表示されます。
色々な書き出し方
色々な書き出し方がありますので、確認しておきましょう。
new Promise((resolve,reject) => { }); Promise {<pending>}
アロー関数を使っています。
let promise = new Promise((resoleve,reject) => { }); promise; Promise {<pending>}
変数を使っています。
let test = (resolve,reject) =>{ }; let promise = new Promise(test); promise; Promise {<pending>}
引数に変数を指定することも出来ます。
resolve
new Promise((resolve,reject) => { }).then( ).catch( ).finally( ); Promise {<pending>}
色々な書き方があるのですが、ここでは、「.then()」、「.catch()」、「.finally()」の3つのメソッドを使用した例で見ていきます。
- then():非同期処理が正常終了した場合のメソッド
- catch():非同期処理が異常終了した場合のメソッド
new Promise((resolve,reject) => { resolve('正常終了しました'); }).then((reso) => { console.log(reso); }); 正常終了しました Promise {<fulfilled>: undefined}
Promiseが「fulfilled」になった場合は、「resolve()」が実行されます。
「resolve()」が実行された場合は、thenメソッドが実行されます。
「resolve」は関数なので、引数を指定することが出来るので、ここでは「then()」に引数を渡しています。
なお、「resoleve」した場合は、catchメソッドが書かれていても実行されることはありません。
new Promise((resolve,reject) => { resolve('正常終了しました'); }).then((reso) => { console.log(reso); }).catch(() => { console.log('異常終了です') }).finally(() => { console.log('Finally') }); 正常終了しました Finally
ただし、finallyメソッドは実行されます。
このあたりは例外処理を思い出してください。
reject
new Promise((resolve,reject) => { reject(); }).then(() => { console.log('正常終了しました'); }).catch(() => { console.log('異常終了です'); }); 異常終了です Promise {<fulfilled>: undefined}
Promiseが「reject」になった場合は、thenメソッドは実行されずに、catchメソッドが実行されます。
「異常終了です」と表示されているのを確認してください。
チェーン
new Promise((resolve,reject) => { reject(); }).then(() => { console.log('正常終了しました'); }).catch(() => { console.log('異常終了です'); }).then(() => { console.log('then 2回目です'); }).then(() => { console.log('then 3回目です'); }).then(() => { console.log('then 4回目です'); }); 異常終了です then 2回目です then 3回目です then 4回目です
Promiseはこのようにチェーンでつなぐことも出来ます。
Promise.all
const all1 = new Promise((resolve,reject) => { setTimeout(() => { resolve('プロミス1') },1000) }) const all2 = new Promise((resolve,reject) => { setTimeout(() => { resolve('プロミス2') },2000) }) const all3 = new Promise((resolve,reject) => { setTimeout(() => { resolve('プロミス3') },3000) }) Promise.all([all1, all2, all3]).then((mes) => { console.log(mes); console.log('プロミス完了'); }).catch(() => { console.log('プロミス失敗'); }); (3) ['プロミス1', 'プロミス2', 'プロミス3'] プロミス完了
「Promise.all」は配列でオブジェクトを受け取り、すべてがresolveしたらthenの処理を実行します。
一つでもrejectされた場合は、catchを実行します。
Promise.race
const all1 = new Promise((resolve,reject) => { setTimeout(() => { resolve('プロミス1') },1000) }) const all2 = new Promise((resolve,reject) => { setTimeout(() => { resolve('プロミス2') },2000) }) const all3 = new Promise((resolve,reject) => { setTimeout(() => { resolve('プロミス3') },3000) }) Promise.race([all1, all2, all3]).then((mes) => { console.log(mes); console.log('プロミス完了'); }).catch(() => { console.log('プロミス失敗'); }); プロミス1 プロミス完了
「Promise.race」はどれか一つでもresolveしたら、thenを実行します。
「Promise.all」との違いを確認してください。
async
asyncとは
「async(エイシンク)」とは、「async」を使う関数宣言のことになります。
async function test() {}
「async」を使うことにより、Promiseで書く非同期処理よりも、簡潔に書くことが出来るようになります。
async関数は、常にPromiseオブジェクトを返します。
(基本的に返すのはresolveになります。)
ここまでasyncの説明を読んで理解出来た方は、かなりセンスがある方です。
言葉だけでは理解出来ないと思いますので、基本的な部分から見ていきます。
ぜひ一緒にコードを書いて、色々な形に変えていきましょう。
書き方
new Promise((resolve,reject) => { resolve('正常終了しました'); }).then((reso) => { console.log(reso); }); 正常終了しました
これは先程、Promiseの「resolve」を解説する時に使用したコードです。
Promiseが「resolve」の場合、「then」メソッドが実行されています。
これを、async関数を使って書き換えてみます。
async function test (){ return '正常終了しました'; } test().then((reso) => { console.log(reso); }); 正常終了しました
特に注目して欲しいのは、最初の部分です。
new Promise((resolve,reject) => { resolve('正常終了しました'); }) ↓↓ async function test (){ return '正常終了しました'; }
asyncを理解する上で最も重要な部分です。
ここをきちんと理解できれば、後の部分はすっきりすると思います。
まだスッキリしない方は、以下のコードも見てください。
async function test (){ return Promise.resolve('正常終了しました'); } test().then((reso) => { console.log(reso); }); 正常終了しました
「Promise.resolve()」を書くことにより、よりPromise構文のイメージが湧くと思います。
まずは、ここまで確実に理解してください。
理解出来たら、以下色々な書き方を見ていきましょう。
関数式
const all3 = async function(){ setTimeout(() => { console.log('正常') },1000) } all3().then((mes) => { console.log(mes); }); Promise {<fulfilled>: undefined} 正常
関数式の形で書いた場合です。
ご自身のコードでも書き換えすることが出来たでしょうか。
アロー関数
const all3 = async() =>{ setTimeout(() => { console.log('正常') },1000) }
関数式が書けたら、アロー関数に変更してみましょう。
await
書き方
今までthenメソッドを使用した例を見てきましたが、async関数と一緒に使われるのは、「await」演算子になります。
早速使い方を見ていきましょう。
なお、await演算子は、async関数の中でしか使用することが出来ませんので注意してください。
async function test (){ return '正常終了しました'; } async function test2() { let result = await test(); console.log(result); } test2(); 正常終了しました
少し分かりにくいですが、「test()」の結果が返るまで待機して実行します。
「test()」の結果が返ってきて、「console.log(result);」が実行されます。
チェーン処理での比較
Promiseでthenメソッドを使ったチェーン処理での違いを見てみましょう。
function plus(num){ return new Promise((resolve) => { setTimeout(() => { console.log('合計${num}です'); num += 1; resolve(num); },1000); }); } plus(1).then((num) => { return plus(num); }).then((num) => { return plus(num); }).then((num) => { return plus(num); }).then((num) => { return plus(num); }); Promise {<pending>} 合計1です 合計2です 合計3です 合計4です 合計5です
こちらはPromiseでのチェーン処理になります。
async function plusAsync() { let result = await plus(1) result = await plus(result) result = await plus(result) result = await plus(result) result = await plus(result) console.log('終了') } plusAsync(); Promise {<pending>} 合計1です 合計2です 合計3です 合計4です 合計5です 終了
asyncを使ったチェーン処理になります。
簡単なコードなのであまり違いはありませんが、asyncの方がスッキリとして管理しやすいと思います。
あとがき
少し長くなりましたが、何となく非同期処理について理解出来たでしょうか。
私自身もあまり得意な部分ではないので、ぜひ色んなサイトを見て非同期処理の理解を深めてください。
いずれまた、実践的な使い方の時にご紹介させて頂くこともあるかと思います。
今回も最後までお読み頂きありがとうございました。
この書き方、どこかで見たことがありませんか。
そうです、以前学習した「例外処理」で学習した「try…catch…finally」と似ています。
共通する部分というか、イメージは近いものがありますので、「例外処理」を理解している方は分かりやすいかもしれません。