Promiseを用いて、非同期関数を同期的に動かす
Promiseを用いて、JavaScriptの非同期関数を同期的に動かします。
本記事は、記事「setTimeoutを用いて、非同期関数を同期的に動かす」の続きになります。
基本はsetTimeoutですが、setTimeoutを利用すると「見た目」が複雑になります。
setTimeoutにPromiseを追加することで、「見た目」がシンプルになります。
(注)「見た目」が複雑
プログラムのネスト(nest、入れ子)が多段階になり、ネストが深くなった状態。
プログラムの可読性が低い状態。
(注)「見た目」がシンプル
プログラムのネストが浅くフラットな状態。プログラムの可読性が高い。
同期的と非同期的
同期的に動く
プログラムが「同期的に動く」とは、プログラムの上から下に向かって、順番に処理が進むことです。
プログラム1
ボタンをクリックすると、関数A()、B()、C() を実行するプログラム。
$('#btn').on('click', function() { A(); B(); C(); });
プログラム1で、上から順番に処理が進んだ場合、同期的に動いたことになります。
- A()が動く
- B()が動く
- C()が動く
非同期的に動く
プログラムが「非同期的に動く」とは、「同期的には動かない」ことです。
プログラム1で、同期的に動かなかった場合、非同期的に動いたことになります。
例えば、
- C()が動く
- A()が動く
- B()が動く
同期関数と非同期関数
同期関数
同期関数は同期的に動きます(同期処理)。
プログラム2
ボタンをクリックすると、テキストエリアに数字を表示するプログラム。
$('#btn').on('click', function() { $("#textarea").html('10'); // 同期関数 $("#textarea").html('20'); // 同期関数 $("#textarea").html('30'); // 同期関数 $("#textarea").html('40'); // 同期関数 });
プログラム2は、上から下に向かって順番に1行ずつ処理が進みます。
- テキストエリアに「10」と表示
- テキストエリアに「20」と表示
- テキストエリアに「30」と表示
- テキストエリアに「40」と表示
非同期関数
非同期関数は非同期的に動きます(非同期処理)。
例えば、ボタンをクリックすると、次の動きをするプログラムを作りたいとします。
仕様A
- テキストエリアを、1秒かけて非表示にする(スライドアップ)
- テキストエリアに、「10」と表示
- テキストエリアを、1秒かけて表示する(スライドダウン)
- テキストエリアに、「20」と表示
仕様Aを、単純にプログラムにすると、以下となります。
プログラム3
$('#btn').on('click', function() { $("#textarea").slideUp(1000); // 非同期関数 $("#textarea").html('10'); // 同期関数 $("#textarea").slideDown(1000); // 非同期関数 $("#textarea").html('20'); // 同期関数 });
(注)slideUp(1000)
スライドアップ(slideUP)は、要素を上方向にスライドして隠します。
数字の1000は、デュレーション(duration)で、スライド開始から完了までの時間です。
単位はm秒で、1000と書けば、1000m秒すなわち1秒となります。
(注)slideDown(1000)
スライドダウン(slideDown)は、隠れている要素を下方向にスライドして表示します。
数字の1000は、デュレーション(duration)です。
プログラム3の、slideUp()、slideDown()は非同期関数です。
そのため、プログラム3は非同期的に動きます。
実際に動作させると、以下の順に動きます。
- テキストエリアに、「10」と表示
- テキストエリアに、「20」と表示
- テキストエリアを、1秒かけて非表示にする(スライドアップ)
- テキストエリアを、1秒かけて表示する(スライドダウン)
この動きは、意図した動き(仕様A)になっていません。
仕様A通りに動くプログラムを作るには、非同期関数を同期的に動かす必要があります。
非同期関数を同期的に動かす
setTimeoutを用いた同期処理
setTimeoutを用いることで、非同期関数を同期的に動かすことができます。
プログラム4
A(); // 非同期関数 setTimeout( function() { B(); // 同期関数 }, duration_A);
プログラム4の説明
非同期関数A()の処理時間がduration_Aの場合、setTimeoutを用いて、同期関数B()をduration_A後に動かしています。
1. 非同期関数A()が動き始め、duration_A後に動き終わる
2. 同期関数B()が動く
setTimeoutを用いることで、A()が動き、その後にB()が動いています。
(注)詳細は、記事「setTimeoutを用いて、非同期関数を同期的に動かす」をご参照ください。
仕様Aを、setTimeoutを用いたプログラムにします(プログラム5)。
プログラム5
<!DOCTYPE html> <html lang="jp"> <head> <meta charset="utf-8"> <title>Control Asynchronous Function Synchronously</title> <script src="../lib/jquery.min.js"></script> <script> var duration_A = 2000; var duration_C = 2000; $(function(){ $('#btn').on('click', function() { A(); // Asynchro. Func. setTimeout(function() { B(); // Synchro. Func. C(); // Asynchro. Func. setTimeout(function() { D(); // Synchro. Func. }, duration_C); }, duration_A); }); }); function A() { $("#textarea").slideUp(duration_A); } function B() { $("#textarea").html('10'); } function C() { $("#textarea").slideDown(duration_C); } function D() { $("#textarea").html('20'); } </script> </head> <body> <input type="button" id="btn" value="Click"><br> <textarea id="textarea" cols="50" rows="10" readonly></textarea> </body> </html>
プログラムの説明
グローバル変数
・duration_A: スライドアップのデュレーションです。2秒に設定。
・duration_C: スライドダウンのデュレーションです。2秒に設定。
ボタンをクリックすると、以下の順で、同期的にプログラムが動きます。
- 13行目: A()が動き始め、2秒後に動き終わる
- 15行目: B()が動く
- 16行目: C()が動き始め、2秒後に動き終わる
- 18行目: D()が動く
Promiseを用いた同期処理
基本的には、上述した「setTimeoutを用いた同期処理」です。
それに、DeferredやPromiseを追加することで、「ネストが深くなる」難点を回避します。
プログラムの変形
非同期関数A()の次の処理(ここでは、B())を、以下の手順で変形します。
function B() { // 元のプログラム $("#textarea").html('10'); }
機能: テキストエリアに、「10」と表示する
まず、setTimeoutを用いて、同期処理に対応するよう変形します。
function B() { setTimeout(function(){ $("#textarea").html('10'); }, duration_A); }
機能: duration_A後に、テキストエリアに「10」と表示する
次に、DeferredやPromiseを追加します(2、5、7行目)。
function B() { var defer = new $.Deferred(); setTimeout(function(){ $("#textarea").html('10'); defer.resolve(); }, duration_A); return defer.promise(); }
機能:
- 2行目: deferを生成
- 7行目: promiseを返す(promiseの状態は「pending」)
- 4行目: duration_A後、テキストエリアに「10」と表示
- 5行目: duration_A後、promiseの状態を「resolve」に変更
プログラム6
A(); var promise = B(); promise.then( C ); function B() { var defer = new $.Deferred(); setTimeout(function(){ $("#textarea").html('10'); defer.resolve(); }, duration_A); return defer.promise(); }
プログラム6は、以下の流れで動作します。
- 1行目: A() が動作
- 2行目: B() を呼出し、B() の返り値であるpromise(状態: pending)を保持
- 3行目: .then(promiseの状態が「resolve」に変化したら)、C()を実行
duration_A後に実行されるのは、以下となります。
- テキストエリアに「10」と表示
- C()
仕様Aを、Promiseを用いたプログラムにします(プログラム7)。
プログラム7
<!DOCTYPE html> <html lang="jp"> <head> <meta charset="utf-8"> <title>Control Asynchronous Function Synchronously</title> <script src="../lib/jquery.min.js"></script> <script> var duration_A = 2000; var duration_C = 2000; $(function(){ $('#btn').on('click', function() { A(); // Asynch. Func. B() // Synch. Func. .then( C ) // Asynch. Func. .then( D ); // Synch. Func. function A() { $("#textarea").slideUp(duration_A); } function B() { var defer = new $.Deferred(); setTimeout(function(){ $("#textarea").html('10'); defer.resolve(); }, duration_A); return defer.promise(); } function C() { $("#textarea").slideDown(duration_C); } function D() { var defer = new $.Deferred(); setTimeout(function(){ $("#textarea").html('20'); defer.resolve(); }, duration_C); return defer.promise(); } }); }); </script> </head> <body> <input type="button" id="btn" value="Click"><br> <textarea id="textarea" cols="50" rows="10" readonly></textarea> </body> </html>
プログラムの説明
- 14-16行目: Promiseによる連結
.then() は必ずPromiseを返します。
そのため、.then()で返したPromiseは以下のように連結できます。
var promise1 = B(); var promise2 = promise1.then( C ); var promise3 = promise2.then( D );
また、以下のように表記を簡略化できます。
B() .then( C ) .then( D );