[2013-11-27]
http://miniapp.org/blog/2013/04/16/523/
基本的に、javascriptで非同期実行はできない。しかし、ある処理が終わったタイミングで次の処理を 開始することはできる。それによって擬似的に非同期実行のような動作を行わせることはできる。
上記処理を単純に実装するとコールバックを利用することになるが、そうすると処理が連続すればするほど コールバックネストが深くなるというバッドノウハウに陥ってしまう。 上記処理をコールバックネストが深くならないようにするために方法として、 CommonJSが「Promises/A」という仕様を提唱しており、jQueryや他のライブラリでDeferredという 名前で実装されている。
このDeferredを利用した擬似的な非同期実行について記述する。
非同期実行を正常に行うためには jQuery 1.9.0 以降が必要。 jQuery 1.5.0 以降でも実装はされているのだが、thenの仕様が1.9から変わっているため、 thenをチェーンして非同期実行するという動作は行えない。 (代わりにdeferred.pipeを同じ用途で用いることができるが、現在はpipeメソッドは非推奨になっている)
ちなみに「google.load("jquery", "1")」はどうも最新が読み込まれない挙動になってしまったようで、 1.7.1 が読み込まれてしまうため注意。 (2013-11-27 現在の最新は、jQuery 1.10.2 ) (jQueryのバージョンは、$().jquery で確認できる。)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js" type="text/javascript" ></script>
<script>
$(function() {
$("#a").click(function(){
var d1 = $.Deferred();
d1
.then(function(){
console.log("0");
})
.then(function(){
var d2 = $.Deferred();
setTimeout(function(){ d2.resolve(); }, 1000);
return d2;
})
.then(function(){
console.log("1");
});
d1.resolve();
});
});
</script>
</head>
<body>
<input id="a" type="button" value="click">
Deferredオブジェクトは、resolveが呼ばれたときに、thenで登録しておいた関数を実行する。 thenはDeferredオブジェクトを戻すので、チェーンして複数の処理を登録することが出来る。
thenで登録しておいた関数が実行されたとき、戻り値としてDeferredオブジェクトを戻した場合は、 残りの登録関数は実行されなくなる。 代わりに戻されたDeferredオブジェクトに残りの登録関数が登録さる。 戻されたDeferredオブジェクトのresolveが呼ばれたときに残りの登録関数が実行が行われる。
つまり、Deferredオブジェクトとは 「このオブジェクトに後で実行したい処理を登録しておいてね。終わったら実行するから」 というオブジェクトである。
自前で再現するとこんな感じだろうか? 実際はstateとかfailとかも考えなければならないんだが、thenだけだとわりと単純に実装できた。
class myDeferred
constructor:(f)->
@list = []
f?.call(@)
then:(f)->
@list.push f
@
resolve:->
newDeferred = null
for f in @list
if newDeferred == null
result = f(arguments...)
if result instanceof myDeferred
newDeferred = result
else
newDeferred.then f
main =->
d1 = new myDeferred()
d1 .then ->
console.log "0"
.then ->
d2 = new myDeferred()
setTimeout ( -> d2.resolve() ), 1000
return d2;
.then ->
console.log "1"
d1.resolve()
main()
CoffeeScriptだと最後の評価値が戻り値になるので、こういう書き方をすると綺麗かもしれない。
sleep = (ms) ->
new myDeferred ->
setTimeout ( => @resolve() ), ms
main =->
d1 = new myDeferred()
d1 .then ->
console.log "0"
.then ->
sleep 1000
.then ->
console.log "1"
d1.resolve()
-
$.Deferred([func])
Deferredオブジェクトを生成します。 funcが渡されると生成直後に、Deferredオブジェクトをthisとして関数が実行されます。
ex.
return $.Deferred(function() { var self = this; setTimeout(function() { data1 = {func: 'first', data: true}; self.resolve(); }, 2000); });
-
$.when(args...)
指定したすべてのオブジェクトを1つのDeferredオブジェクトとして管理します。 全てのdeferredオブジェクトでresolve()が実行されたあと、 はじめて $.when.then() で登録された関数が実行されます。
-
deferred オブジェクト
-
resolve()
正常終了状態(resolved)にして、doneで登録された関数を実行します。
-
reject()
異常終了状態(rejected)にして、failで登録された関数を実行します。
-
notify()
progressで登録された関数を実行します。 ただし、状態が既に正常終了か異常終了になっている場合は実行しません。
-
resolveWith(context, argv)
-
rejectWith(context, argv)
-
notifyWith(context, argv)
関数オブジェクトのapplyっぽい動きをすること以外は、Withが付いていない関数と同じです。
ex.
d.resolveWith($("#divItem"), ["argv2", "argv2"])
-
always(func)
resolveかrejectされたときに実行される関数を登録します。
-
done(func)
resolveされたときに実行される関数を登録します。
-
fail(func)
rejectされたときに実行される関数を登録します。
-
progress(func)
notifyされたときに実行される関数を登録します。
-
then(doneFunc, failFunc, progressFunc)
resolveかrejectかnotifyされたときに実行される関数を登録します。 登録関数がDeferredオブジェクトを戻した場合は、戻したDeferredオブジェクトに制御を移します。 (then以外で登録された関数は戻り値に関係なく、同じdeferredオブジェクトに登録された処理が実行されます)
-
promise()
resolve/reject/notifyのメソッドを持たないこと以外はDeferedオブジェクトと同じである、promiseオブジェクトを戻します。
-
state()
以下のいずれかの文字列を戻します。
- "pending" : まだresolveもrejectも実行されていない。
- "resolved": resolveが実行された。
- "rejected": rejectが実行された。
-
呼び出し | always | done | fail | then | progress |
---|---|---|---|---|---|
resolve() | ○ | ○ | × | ○ | × |
reject() | ○ | × | ○ | △※1 | × |
notify() | × | × | × | △※2 | ○ |
※1: 登録されていれば、thenの第2引数が呼ばれる
※2: 登録されていれば、thenの第3引数が呼ばれる