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 );
