本篇,主要普及promise的用法。
一直以來,JavaScript處理異步都是以callback的方式,在前端開發領域callback機制幾乎深入人心。在設計API的時候,不管是瀏覽器廠商還是SDK開發商亦或是各種類庫的作者,基本上都已經遵循着callback的套路。
近幾年隨着JavaScript開發模式的逐漸成熟,CommonJS規範順勢而生,其中就包括提出了Promise規範,Promise完全改變了js異步編程的寫法,讓異步編程變得十分的易於理解。
在callback的模型裏邊,我們假設需要執行一個異步隊列,代碼看起來可能像這樣:
1
2
3
4
5
6
7
|
loadImg( 'a.jpg' , function ()
{ loadImg( 'b.jpg' , function ()
{ loadImg( 'c.jpg' , function ()
{ console.log( 'all
done!' ); }); }); }); |
這也就是我們常說的回調金字塔,當異步的任務很多的時候,維護大量的callback將是一場災難。當今Node.js大熱,好像很多團隊都要用它來做點東西以沾沾“洋氣”,曾經跟一個運維的同學聊天,他們也是打算使用Node.js做一些事情,可是一想到js的層層回調就望而卻步。
好,扯淡完畢,下面進入正題。
Promise可能大家都不陌生,因爲Promise規範已經出來好一段時間了,同時Promise也已經納入了ES6,而且高版本的chrome、firefox瀏覽器都已經原生實現了Promise,只不過和現如今流行的類Promise類庫相比少些API。
所謂Promise,字面上可以理解爲“承諾”,就是說A調用B,B返回一個“承諾”給A,然後A就可以在寫計劃的時候這麼寫:當B返回結果給我的時候,A執行方案S1,反之如果B因爲什麼原因沒有給到A想要的結果,那麼A執行應急方案S2,這樣一來,所有的潛在風險都在A的可控範圍之內了。
上面這句話,翻譯成代碼類似:
1
2
3
4
5
|
var resB
= B(); var runA
= function ()
{ resB.then(execS1,
execS2); }; runA(); |
只看上面這行代碼,好像看不出什麼特別之處。但現實情況可能比這個複雜許多,A要完成一件事,可能要依賴不止B一個人的響應,可能需要同時向多個人詢問,當收到所有的應答之後再執行下一步的方案。最終翻譯成代碼可能像這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var resB
= B(); var resC
= C(); ... var runA
= function ()
{ reqB .then(resC,
execS2) .then(resD,
execS3) .then(resE,
execS4) ... .then(execS1); }; runA(); |
在這裏,當每一個被詢問者做出不符合預期的應答時都用了不同的處理機制。事實上,Promise規範沒有要求這樣做,你甚至可以不做任何的處理(即不傳入then的第二個參數)或者統一處理。
好了,下面我們來認識下Promise/A+規範:
- 一個promise可能有三種狀態:等待(pending)、已完成(fulfilled)、已拒絕(rejected)
- 一個promise的狀態只可能從“等待”轉到“完成”態或者“拒絕”態,不能逆向轉換,同時“完成”態和“拒絕”態不能相互轉換
- promise必須實現
then
方法(可以說,then就是promise的核心),而且then必須返回一個promise,同一個promise的then可以調用多次,並且回調的執行順序跟它們被定義時的順序一致 - then方法接受兩個參數,第一個參數是成功時的回調,在promise由“等待”態轉換到“完成”態時調用,另一個是失敗時的回調,在promise由“等待”態轉換到“拒絕”態時調用。同時,then可以接受另一個promise傳入,也接受一個“類then”的對象或方法,即thenable對象。
可以看到,Promise規範的內容並不算多,大家可以試着自己實現以下Promise。
以下是筆者自己在參考許多類Promise庫之後簡單實現的一個Promise,代碼請移步promiseA。
簡單分析下思路:
構造函數Promise接受一個函數resolver
,可以理解爲傳入一個異步任務,resolver接受兩個參數,一個是成功時的回調,一個是失敗時的回調,這兩參數和通過then傳入的參數是對等的。
其次是then的實現,由於Promise要求then必須返回一個promise,所以在then調用的時候會新生成一個promise,掛在當前promise的_next
上,同一個promise多次調用都只會返回之前生成的_next
。
由於then方法接受的兩個參數都是可選的,而且類型也沒限制,可以是函數,也可以是一個具體的值,還可以是另一個promise。下面是then的具體實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
Promise.prototype.then
= function (resolve,
reject) { var next
= this ._next
|| ( this ._next
= Promise()); var status
= this .status; var x; if ( 'pending' ===
status) { isFn(resolve)
&& this ._resolves.push(resolve); isFn(reject)
&& this ._rejects.push(reject); return next; } if ( 'resolved' ===
status) { if (!isFn(resolve))
{ next.resolve(resolve); } else { try { x
= resolve( this .value); resolveX(next,
x); } catch (e)
{ this .reject(e); } } return next; } if ( 'rejected' ===
status) { if (!isFn(reject))
{ next.reject(reject); } else { try { x
= reject( this .reason); resolveX(next,
x); } catch (e)
{ this .reject(e); } } return next; } }; |
這裏,then做了簡化,其他promise類庫的實現比這個要複雜得多,同時功能也更多,比如還有第三個參數——notify,表示promise當前的進度,這在設計文件上傳等時很有用。對then的各種參數的處理是最複雜的部分,有興趣的同學可以參看其他類Promise庫的實現。
在then的基礎上,應該還需要至少兩個方法,分別是完成promise的狀態從pending到resolved或rejected的轉換,同時執行相應的回調隊列,即resolve()
和reject()
方法。
到此,一個簡單的promise就設計完成了,下面簡單實現下兩個promise化的函數:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
function sleep(ms)
{ return function (v)
{ var p
= Promise(); setTimeout( function ()
{ p.resolve(v); }); return p; }; }; function getImg(url)
{ var p
= Promise(); var img
= new Image(); img.onload
= function ()
{ p.resolve( this ); }; img.onerror
= function (err)
{ p.reject(err); }; img.url
= url; return p; }; |
由於Promise構造函數接受一個異步任務作爲參數,所以getImg
還可以這樣調用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
function getImg(url)
{ return Promise( function (resolve,
reject) { var img
= new Image(); img.onload
= function ()
{ resolve( this ); }; img.onerror
= function (err)
{ reject(err); }; img.url
= url; }); }; |
接下來(見證奇蹟的時刻),假設有一個BT的需求要這麼實現:異步獲取一個json配置,解析json數據拿到裏邊的圖片,然後按順序隊列加載圖片,沒張圖片加載時給個loading效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
function addImg(img)
{ $( '#list' ).find( '>
li:last-child' ).html( '' ).append(img); }; function prepend()
{ $( '<li>' ) .html( 'loading...' ) .appendTo($( '#list' )); }; function run()
{ $( '#done' ).hide(); getData( 'map.json' ) .then( function (data)
{ $( 'h4' ).html(data.name); return data.list.reduce( function (promise,
item) { return promise .then(prepend) .then(sleep(1000)) .then( function ()
{ return getImg(item.url); }) .then(addImg); },
Promise.resolve()); }) .then(sleep(300)) .then( function ()
{ $( '#done' ).show(); }); }; $( '#run' ).on( 'click' ,
run); |
這裏的sleep只是爲了看效果加的,可猛擊查看demo!當然,Node.js的例子可查看這裏。
在這裏,Promise.resolve(v)
靜態方法只是簡單返回一個以v爲肯定結果的promise,v可不傳入,也可以是一個函數或者是一個包含then
方法的對象或函數(即thenable)。
類似的靜態方法還有Promise.cast(promise)
,生成一個以promise爲肯定結果的promise;
Promise.reject(reason)
,生成一個以reason爲否定結果的promise。
我們實際的使用場景可能很複雜,往往需要多個異步的任務穿插執行,並行或者串行同在。這時候,可以對Promise進行各種擴展,比如實現Promise.all()
,接受promises隊列並等待他們完成再繼續,再比如Promise.any()
,promises隊列中有任何一個處於完成態時即觸發下一步操作。
標準的Promise
可參考html5rocks的這篇文章JavaScript Promises,目前高級瀏覽器如chrome、firefox都已經內置了Promise對象,提供更多的操作接口,比如Promise.all()
,支持傳入一個promises數組,當所有promises都完成時執行then,還有就是更加友好強大的異常捕獲,應對日常的異步編程,應該足夠了。
第三方庫的Promise
現今流行的各大js庫,幾乎都不同程度的實現了Promise,如dojo,jQuery、Zepto、when.js、Q等,只是暴露出來的大都是Deferred
對象,以jQuery(Zepto類似)爲例,實現上面的getImg()
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
function getImg(url)
{ var def
= $.Deferred(); var img
= new Image(); img.onload
= function ()
{ def.resolve( this ); }; img.onerror
= function (err)
{ def.reject(err); }; img.src
= url; return def.promise(); }; |
當然,jQuery中,很多的操作都返回的是Deferred或promise,如animate
、ajax
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//
animate $( '.box' ) .animate({ 'opacity' :
0}, 1000) .promise() .then( function ()
{ console.log( 'done' ); }); //
ajax $.ajax(options).then(success,
fail); $.ajax(options).done(success).fail(fail); //
ajax queue $.when($.ajax(options1),
$.ajax(options2)) .then( function ()
{ console.log( 'all
done.' ); }, function ()
{ console.error( 'There
something wrong.' ); }); |
jQuery還實現了done()
和fail()
方法,其實都是then方法的shortcut。
處理promises隊列,jQuery實現的是$.when()
方法,用法和Promise.all()
類似。
其他類庫,這裏值得一提的是when.js,本身代碼不多,完整實現Promise,同時支持browser和Node.js,而且提供更加豐富的API,是個不錯的選擇。這裏限於篇幅,不再展開。
尾聲
我們看到,不管Promise實現怎麼複雜,但是它的用法卻很簡單,組織的代碼很清晰,從此不用再受callback的折磨了。
最後,Promise是如此的優雅!但Promise也只是解決了回調的深層嵌套的問題,真正簡化JavaScript異步編程的還是Generator,在Node.js端,建議考慮Generator。
下一篇,研究下Generator。
github原文: https://github.com/chemdemo/chemdemo.github.io/issues/6
參考文獻
- JavaScript Promises
- JavaScript Promises(中文)
- when.js
- Asynch JS: The Power Of $.Deferred
- jQuery: $.Deferred()
轉自:http://www.alloyteam.com/2014/05/javascript-promise-mode/