【編者按】JavaScript是一種基于對象和事件驅(qū)動(dòng)并具有相對安全性的客戶端腳本語言。自推出后就大受開發(fā)者的青睞,基于JavaScript的開發(fā)工具也不計(jì)其數(shù),開發(fā)者們可以靈活選擇,輕松構(gòu)建應(yīng)用。原文作者TAT.dmyang就JavaScript中的Promise規(guī)范給出了一些見解,目前高級(jí)瀏覽器如Chrome、Firefox都已經(jīng)內(nèi)置了Promise對象,提供更多的操作接口,如此優(yōu)雅的Promise具備哪些特性呢?且看下文:
一直以來,JavaScript處理異步都是以callback的方式,在前端開發(fā)領(lǐng)域callback機(jī)制幾乎深入人心。在設(shè)計(jì)API的時(shí)候,不管是瀏覽器廠商還是SDK開發(fā)商亦或是各種類庫的作者,基本上都已經(jīng)遵循著callback的套路。
近幾年隨著JavaScript開發(fā)模式的逐漸成熟,CommonJS規(guī)范順勢而生,其中就包括提出了Promise規(guī)范,Promise完全改變了js異步編程的寫法,讓異步編程變得十分的易于理解。
在callback的模型里邊,我們假設(shè)需要執(zhí)行一個(gè)異步隊(duì)列,代碼看起來可能像這樣:
loadImg('a.jpg', function() { loadImg('b.jpg', function() { loadImg('c.jpg', function() { console.log('all done!'); }); }); });
這也就是我們常說的回調(diào)金字塔,當(dāng)異步的任務(wù)很多的時(shí)候,維護(hù)大量的callback將是一場災(zāi)難。當(dāng)今Node.js大熱,好像很多團(tuán)隊(duì)都要用它來做點(diǎn)東西以沾沾“洋氣”,曾經(jīng)跟一個(gè)運(yùn)維的同學(xué)聊天,他們也是打算使用Node.js做一些事情,可是一想到j(luò)s的層層回調(diào)就望而卻步。
好,扯淡完畢,下面進(jìn)入正題。
Promise可能大家都不陌生,因?yàn)镻romise規(guī)范已經(jīng)出來好一段時(shí)間了,同時(shí)Promise也已經(jīng)納入了ES6,而且高版本的chrome、firefox瀏覽器都已經(jīng)原生實(shí)現(xiàn)了Promise,只不過和現(xiàn)如今流行的類Promise類庫相比少些API。
所謂Promise,字面上可以理解為“承諾”,就是說A調(diào)用B,B返回一個(gè)“承諾”給A,然后A就可以在寫計(jì)劃的時(shí)候這么寫:當(dāng)B返回結(jié)果給我的時(shí)候,A執(zhí)行方案S1,反之如果B因?yàn)槭裁丛驔]有給到A想要的結(jié)果,那么A執(zhí)行應(yīng)急方案S2,這樣一來,所有的潛在風(fēng)險(xiǎn)都在A的可控范圍之內(nèi)了。
上面這句話,翻譯成代碼類似:
var resB = B(); var runA = function() { resB.then(execS1, execS2); }; runA();
只看上面這行代碼,好像看不出什么特別之處。但現(xiàn)實(shí)情況可能比這個(gè)復(fù)雜許多,A要完成一件事,可能要依賴不止B一個(gè)人的響應(yīng),可能需要同時(shí)向多個(gè)人詢問,當(dāng)收到所有的應(yīng)答之后再執(zhí)行下一步的方案。最終翻譯成代碼可能像這樣:
var resB = B(); var resC = C(); ... var runA = function() { reqB .then(resC, execS2) .then(resD, execS3) .then(resE, execS4) ... .then(execS1); }; runA();
在這里,當(dāng)每一個(gè)被詢問者做出不符合預(yù)期的應(yīng)答時(shí)都用了不同的處理機(jī)制。事實(shí)上,Promise規(guī)范沒有要求這樣做,你甚至可以不做任何的處理(即不傳入then的第二個(gè)參數(shù))或者統(tǒng)一處理。
好了,下面我們來認(rèn)識(shí)下Promise/A+規(guī)范:
then
方法(可以說,then就是promise的核心),而且then必須返回一個(gè)promise,同一個(gè)promise的then可以調(diào)用多次,并且回調(diào)的執(zhí)行順序跟它們被定義時(shí)的順序一致可以看到,Promise規(guī)范的內(nèi)容并不算多,大家可以試著自己實(shí)現(xiàn)以下Promise。
以下是筆者自己在參考許多類Promise庫之后簡單實(shí)現(xiàn)的一個(gè)Promise,代碼請移步promiseA。
簡單分析下思路:
構(gòu)造函數(shù)Promise接受一個(gè)函數(shù)resolver
,可以理解為傳入一個(gè)異步任務(wù),resolver接受兩個(gè)參數(shù),一個(gè)是成功時(shí)的回調(diào),一個(gè)是失敗時(shí)的回調(diào),這兩參數(shù)和通過then傳入的參數(shù)是對等的。
其次是then的實(shí)現(xiàn),由于Promise要求then必須返回一個(gè)promise,所以在then調(diào)用的時(shí)候會(huì)新生成一個(gè)promise,掛在當(dāng)前promise的_next
上,同一個(gè)promise多次調(diào)用都只會(huì)返回之前生成的_next
。
由于then方法接受的兩個(gè)參數(shù)都是可選的,而且類型也沒限制,可以是函數(shù),也可以是一個(gè)具體的值,還可以是另一個(gè)promise。下面是then的具體實(shí)現(xiàn):
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類庫的實(shí)現(xiàn)比這個(gè)要復(fù)雜得多,同時(shí)功能也更多,比如還有第三個(gè)參數(shù)――notify,表示promise當(dāng)前的進(jìn)度,這在設(shè)計(jì)文件上傳等時(shí)很有用。對then的各種參數(shù)的處理是最復(fù)雜的部分,有興趣的同學(xué)可以參看其他類Promise庫的實(shí)現(xiàn)。
在then的基礎(chǔ)上,應(yīng)該還需要至少兩個(gè)方法,分別是完成promise的狀態(tài)從pending到resolved或rejected的轉(zhuǎn)換,同時(shí)執(zhí)行相應(yīng)的回調(diào)隊(duì)列,即
resolve()
和reject()
方法。到此,一個(gè)簡單的promise就設(shè)計(jì)完成了,下面簡單實(shí)現(xiàn)下兩個(gè)promise化的函數(shù):
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構(gòu)造函數(shù)接受一個(gè)異步任務(wù)作為參數(shù),所以
getImg
還可以這樣調(diào)用: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; }); };接下來(見證奇跡的時(shí)刻),假設(shè)有一個(gè)BT的需求要這么實(shí)現(xiàn):異步獲取一個(gè)json配置,解析json數(shù)據(jù)拿到里邊的圖片,然后按順序隊(duì)列加載圖片,沒張圖片加載時(shí)給個(gè)loading效果
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!當(dāng)然,Node.js的例子可查看這里。
在這里,
Promise.resolve(v)
靜態(tài)方法只是簡單返回一個(gè)以v為肯定結(jié)果的promise,v可不傳入,也可以是一個(gè)函數(shù)或者是一個(gè)包含then
方法的對象或函數(shù)(即thenable)。類似的靜態(tài)方法還有
Promise.cast(promise)
,生成一個(gè)以promise為肯定結(jié)果的promise;
Promise.reject(reason)
,生成一個(gè)以reason為否定結(jié)果的promise。我們實(shí)際的使用場景可能很復(fù)雜,往往需要多個(gè)異步的任務(wù)穿插執(zhí)行,并行或者串行同在。這時(shí)候,可以對Promise進(jìn)行各種擴(kuò)展,比如實(shí)現(xiàn)
Promise.all()
,接受promises隊(duì)列并等待他們完成再繼續(xù),再比如Promise.any()
,promises隊(duì)列中有任何一個(gè)處于完成態(tài)時(shí)即觸發(fā)下一步操作。標(biāo)準(zhǔn)的Promise
可參考html5rocks的這篇文章JavaScript Promises,目前高級(jí)瀏覽器如Chrome、Firefox都已經(jīng)內(nèi)置了Promise對象,提供更多的操作接口,比如
Promise.all()
,支持傳入一個(gè)promises數(shù)組,當(dāng)所有promises都完成時(shí)執(zhí)行then,還有就是更加友好強(qiáng)大的異常捕獲,應(yīng)對日常的異步編程,應(yīng)該足夠了。第三方庫的Promise
現(xiàn)今流行的各大js庫,幾乎都不同程度的實(shí)現(xiàn)了Promise,如dojo,jQuery、Zepto、when.js、Q等,只是暴露出來的大都是
Deferred
對象,以jQuery(Zepto類似)為例,實(shí)現(xiàn)上面的getImg()
: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(); };當(dāng)然,jQuery中,很多的操作都返回的是Deferred或promise,如
animate
、ajax
:// 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還實(shí)現(xiàn)了
done()
和fail()
方法,其實(shí)都是then方法的shortcut。處理promises隊(duì)列,jQuery實(shí)現(xiàn)的是
$.when()
方法,用法和Promise.all()
類似。其他類庫,這里值得一提的是when.js,本身代碼不多,完整實(shí)現(xiàn)Promise,同時(shí)支持browser和Node.js,而且提供更加豐富的API,是個(gè)不錯(cuò)的選擇。這里限于篇幅,不再展開。
尾聲
我們看到,不管Promise實(shí)現(xiàn)怎么復(fù)雜,但是它的用法卻很簡單,組織的代碼很清晰,從此不用再受callback的折磨了。
最后,Promise是如此的優(yōu)雅!但Promise也只是解決了回調(diào)的深層嵌套的問題,真正簡化JavaScript異步編程的還是Generator,在Node.js端,建議考慮Generator。
參考文獻(xiàn)
上一篇 MySQL原生HA方案