Skip to content

Instantly share code, notes, and snippets.

@culage
Created November 29, 2013 12:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save culage/7705302 to your computer and use it in GitHub Desktop.
Save culage/7705302 to your computer and use it in GitHub Desktop.

非同期実行javascript学習

[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()

■jQuery.Deferred リファレンス

◆リファレンス

  • $.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引数が呼ばれる

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment