JavaScript Promise 詳解
原文連結:
讀完這篇文章,預計會消耗你 40 分鐘的時間。
Ajax 出現的時候,刮來了一陣非同步之風,現在 Nodejs 火爆,又一陣非同步狂風颳了過來。需求是越來越苛刻,使用者對效能的要求也是越來越高,隨之而來的是頁面非同步操作指數般增長,如果不能恰當的控制程式碼邏輯,我們就會陷入無窮的回撥地獄中。
ECMAScript 6 已經將非同步操作納入了規範,現代瀏覽器也內建了 Promise 物件供我們進行非同步程式設計,那麼此刻,還在等啥?趕緊學習學習 Promise 的內部原理吧!
由於 javascript 的單執行緒性質,我們必須等待上一個事件執行完成才能處理下一步,如下:
// DOM ready之後執行$(document).ready(function(){ // 獲取模板 $.get(url, function(tpl){ // 獲取資料 $.get(url2, function(data){ // 構建 DOMString makeHtml(tpl, data, function(str){ // 插入到 DOM 中 $(obj).html(str); }); }); }); });
為了減少首屏資料的載入,我們將一些模板和所有資料都放在伺服器端,當使用者操作某個按鈕時,需要將模板和資料拼接起來插入到 DOM 中,這個過程還必須在 DOMReady 之後才能執行。這種情況是十分常見的,如果非同步操作再多一些,整個程式碼的縮排讓人看著很不舒服,為了優雅地處理這個問題,ECMAScript 6 引入了 Promise 的概念,目前一些現代瀏覽器已經支援這些新東西了!
為了讓程式碼流程更加清晰,我們假想著能夠按照下面的流程來跑程式:
new Promise(ready).then(getTpl).then(getData).then(makeHtml).resolve();
先將要事務按照執行順序依次 push 到事務佇列中,push 完了之後再透過 resolve 函式啟動整個流程。
整個流程的操作模型如下:
promise(ok).then(ok_1).then(ok_2).then(ok_3).reslove(value)------+ | | | | | | | | | +=======+ | | | | | | | | | | | | | | | +---------|----------|----------|-------- ok() ------+ | | | | | | | | | | +----------|----------|-------- ok_1()| | | | | | | | | +----------|-------- ok_2()| | | | | | | +-------- ok_3()-----+ | | | | | @ Created By Barret Lee +=======+ exit
在 resolve 之前,promise 的每一個 then 都會將回撥函式壓入佇列,resolve 後,將 resolve 的值送給佇列的第一個函式,第一個函式執行完畢後,將執行結果再送入下一個函式,依次執行完佇列。一連串下來,一氣呵成,沒有絲毫間斷。
如果瞭解 Promise,可以移步下方,看看對 Promise 的封裝:
Github:
DEMO:
如果還不是很瞭解,可以往下閱讀全文,瞭解一二。
那麼,什麼是 Promise ?
Promise 可以簡單理解為一個事務,這個事務存在三種狀態:
已經完成了 resolved
因為某種原因被中斷了 rejected
還在等待上一個事務結束 pending
上文中我們舉了一個栗子,獲取模板和資料之後再將拼合的資料插入到 DOM 中,這裡我們將整個程式分解成多個事務:
事務一: 獲取模板 事務二: 獲取資料 事務三: 拼合之後插入到 DOM
在事務一結束之前,也就是模板程式碼從伺服器拉取過來之前,事務二和事務三都處於 pending 狀態,他們必須等待上一個事務結束。而事務一結束之後會將自身狀態標記為 resolved,並把該事務中處理的結果移交給事務二繼續處理(當然,這裡如果沒有資料返回,事務二就不會獲得上一個事務的資料),依次類推,直到最後一個事務操作結束。
在事務操作的過程中,若遇到錯誤,比如事務一獲取資料存在跨域問題,那事務就會操作失敗,此時它會將自身的狀態標記為 rejected,由於後續事務都是承接前一事務的,前一事務已經宣告工程已經玩不成了,那麼後續的所有事務都會將自己標記為 rejected,其標記理由(reason)就是出錯事務的報錯資訊(這個報錯資訊可以使用 try…catch 來捕獲,也可以透過程式自身來捕獲,如 ajax 的 onerror 事件、ajax 返回的狀態碼為 404 等)。
小結:Promise 就是一個事務的管理器。他的作用就是將各種內嵌回撥的事務用流水形式表達,其目的是為了簡化程式設計,讓程式碼邏輯更加清晰。
由於整個程式的實現比較難理解,對於 Promise,我們將分為兩部分闡述:
無錯誤傳遞的 Promise,也就是事務不會因為任何原因中斷,事務佇列中的事項都會被依次處理,此過程中 Promise 只有 pending 和 resolved 兩種狀態,沒有 rejected 狀態。
包含錯誤的 Promise,每個事務的處理都必須使用容錯機制來獲取結果,一旦出錯,就會將錯誤資訊傳遞給下一個事務,如果錯誤資訊會影響下一個事務,則下一個事務也會 rejected,如果不會,下一個事務可以正常執行,依次類推。
首先,我們需要用一個變數(status)來標記事務的狀態,然後將事務(affair)也儲存到 Promise 物件中。
var Promise = function(affair){ this.state = “pending”; this.affair = affair || function(o) { return o; }; this.allAffairs = []; };
Promise 有兩個重要的方法,一個是 then,另一個是 resolve:
then,將事務新增到事務佇列(allAffairs)中
resolve,開啟流程,讓整個操作從第一個事務開始執行
在操作事務之前,我們會先把各種事務依次放入事務佇列中,這裡會用到 then 方法:
Promise.prototype.then = function (nextAffair){ var promise = new Promise(); if (this.state == ‘resloved’){ // 如果當前狀態是已完成,則這個事務將會被立即執行 return this._fire(promise, nextAffair); }else{ // 否則將會被加入佇列中 return this._push(promise, nextAffair); } };
如果整個操作已經完成了,那 then 方法送進的事務會被立即執行,
Promise.prototype._fire = function (nextPromise, nextAffair){ var nextResult = nextAffair(this.result); if (nextResult instanceof Promise){ nextResult.then(function(obj){ nextPromise.resolve(obj); }); }else{ nextPromise.resolve(nextResult); } return nextPromise; };
被立即執行之後會返回一個結果,這個結果會被傳遞到下一個事務中作為原料,但是這裡需要考慮兩種情況:
非同步,如果這個結果也是一個 Promise,則需要等待這個 Promise 執行完畢再將最終的結果傳到下一個事務中。
同步,如果這個結果不是 Promise,則直接將結果傳遞給下一個事務。
第一種情況還是比較常見的,比如我們在一個事務中有一個子事務佇列需要處理,此時必須等待子事務完成才能回到主事務佇列中。
Promise.prototype.resolve = function (obj){ if (this.state != ‘pending’) { throw ‘流程已完成,不能再次開啟流程!’; } this.state = ‘resloved’; // 執行該事務,並將執行結果寄存到 Promise 管理器上 this.result = this.affair(obj); for (var i = 0, len = this.allAffairs.length; iresolve 接受一個引數,這個資料是交給第一個事務來處理的,因為第一個事務的啟動可能需要點原料,這個資料就是原料,它也可以是空。該事物處理完畢之後,將操作結果(result)寄存在 Promise 物件上,方便引用,然後將結果(result)作為原料送入下一個事務。依次類推。
我們看到 then 方法中還呼叫了一個 _push ,這個方法的作用是將事務推進事務管理器(Promise)。
Promise.prototype._push = function (nextPromise, nextAffair){ this.allAffairs.push({ promise: nextPromise, affair: nextAffair }); return nextPromise; };以上操作,我們就實現了一個簡單的事務管理器,可以測試下下面的程式碼:
// 初始化事務管理器var promise = new Promise(function(data){ console.log(data); return 1; });// 新增事務promise.then(function(data){ console.log(data); return 2; }).then(function(data){ console.log(data); return 3; }).then(function(data){ console.log(data); console.log(“end”); });// 啟動事務promise.resolve(“start”);可以看到依次輸出的結果為:
> start> 1> 2> 3> end由於上述實現十分簡陋,鏈式呼叫沒做太好的處理,請讀者自行完善。
下面是一個非同步操作演示:
var promise = new Promise(function(data){ console.log(data); return “end”; }); promise.then(function(data){ // 這裡需要返回一個 Promise,讓主事務切換到子事務處理 return (function(data){ // 建立一個子事務 var promise = new Promise(); setTimeout(function(){ console.log(data); // 一秒之後才啟動子事務,模擬非同步延時 promise.resolve(); }, 1000); return promise; })(data); }); promise.resolve(“start”);可以看到依次輸出的結果為:> start> end (1s之後輸出)將函式寫的稍微好看點:
function delay(data){ // 建立一個子事務 var promise = new Promise(); setTimeout(function(){ console.log(data); // 一秒之後才啟動子事務,模擬非同步延時 promise.resolve(); }, 1000); return promise; }// 主事務var promise = new Promise(function(data){ console.log(data); return “end”; }); promise.then(delay); promise.resolve(“start”);三、包含錯誤傳遞的 Promise
真的很羨慕你能看到這麼詳細的文章,當然,後面會更加精彩!
沒有錯誤處理的 Promise 只能算是一個半成品,雖說可以透過在最外層加一個 try..catch 來捕獲錯誤,但沒法具體定位是哪個事務發生的錯誤。並且這裡的錯誤不僅僅包含 JavaScript Error,還有諸如 ajax 返回的 data code 不是 200 的情況等。
先看一個瀏覽器內建 Promise 的例項(該程式碼可在現代瀏覽器下執行):
new Promise(function(resolve, reject){ resolve(“start”); }).then(function(data){ console.log(data); throw “error”; }).catch(function(err){ console.log(err); return “end”; }).then(function(data){ console.log(data) });Promise 的回撥和 then 方法都是接受兩個引數:new Promise(function(resolve, reject){ // …}); promise.then( function(value){/* code here */}, function(reason){/* code here */} );事務處理過程中,如果有值返回,則作為 value,傳入到 resolve 函式中,若有錯誤產生,則作為 reason 傳入到 reject 函式中處理。
在初始化 Promise 物件時,若傳入的回撥中沒有執行 resolve 或者 reject,這需要我們主動去啟動事務佇列。
promise.resolve();promise.reject();上面兩種都是可以啟動一個佇列的。這裡跟第二章第二節的 resolve 函式用法類似。Promise 物件還提供了 catch 函式,起用法等價於下面所示:
promise.catch();// 等價於promise.then(null, function(reason){});還有兩個 API:
promise.all();promise.race();後續再講。先看看這個有錯誤處理的 Promise 是如何實現的。
function Promise(resolver){ this.status = “pending”; this.value = null; this.handlers = []; this._doPromise.call(this, resolver); }_doPromise 方法在例項化 Promise 函式時就執行。如果送入的回撥函式 resolver 中已經 resolve 或者 reject 了,程式就已經啟動了,所以在例項化的時候就開始判斷。
_doPromise: function(resolver){ var called = false, self = this; try{ resolver(function(value){ // 如果沒有 call 則繼續,並標記 called 為 true !called && (called = !0, self.resolve(value)); }, function(reason){ // 同上 !called && (called = !0, self.reject(reason)); }); } catch(e) { // 同上,捕獲錯誤,傳遞錯誤到下一個 then 事務 !called && (called = !0, self.reject(e)); } },只要 resolve 或者 reject 就會標記程式 called 為 true,表示程式已經啟動了。
resolve: function(value) { try{ if(this === value){ throw new TypeError(‘流程已完成,不能再次開啟流程!’); } else { // 如果還有子事務佇列,繼續執行 value && value.then && this._doPromise(value.then); } // 執行完了之後標記為完成 this.status = “fulfilled”; this.value = value; this._dequeue(); } catch(e) { this.reject(e); } }, reject: function(reason) { // 標記狀態為出錯 this.status = “rejected”; this.value = reason; this._dequeue(); },可以看到,每次 resolve 的時候都會用一個 try..catch 包裹來捕獲未知錯誤。
_dequeue: function(){ var handler; // 執行事務,直到佇列為空 while (this.handlers.length) { handler = this.handlers.shift(); this._handle(handler.thenPromise, handler.onFulfilled, handler.onRejected); } },無論是 resolve 還是 reject 都會讓程式往後奔流,直到結束所有事務,所以這兩個方法中都有 _dequeue 函式。
_handle: function(thenPromise, onFulfilled, onRejected){ var self = this; setTimeout(function() { // 判斷下次操作採用哪個函式,reject 還是 resolve var callback = self.status == “fulfilled” ? onFulfilled : onRejected; // 只有是函式才會繼續回撥 if (typeof callback === ‘function’) { try { self.resolve.call(thenPromise, callback(self.value)); } catch(e) { self.reject.call(thenPromise, e); } return; } // 否則就將 value 傳遞給下一個事務了 self.status == “fulfilled” ? self.resolve.call(thenPromise, self.value) : self.reject.call(thenPromise, self.value); }, 1); },這個函式跟上一節提到的 _fire 類似,如果 callback 是 function,就會進入子事務佇列,處理完了之後退回到主事務佇列。最後一個 then 方法,將事務推進佇列。
then: function(onFulfilled, onRejected){ var thenPromise = new Promise(function() {}); if (this.status == “pending”) { this.handlers.push({ thenPromise: thenPromise, onFulfilled: onFulfilled, onRejected: onRejected }); } else { this._handle(thenPromise, onFulfilled, onRejected); } return thenPromise; }如果第二節沒有理解清楚,這一節也會讓人頭疼,這一部分講的比較粗糙。
或許你在面試的時候,有面試官問你:
$.ajax()
執行後返回的結果是什麼?在 jQuery1.5 版本就已經引入了 Defferred 物件,當時為了引入這個東西,整個 jQuery 都被重構了。Defferred 跟 Promise 類似,它表示一個還未完成任務的物件,而 Promise 確切的說,是一個代表未知值的物件。
$.ajax({ url: url }).done(function(data, status, xhr){ //…}).fail(function(){ //…});回憶下第二章第一節中的 Promise,是不是如出一轍,只是 jQuery 還提供了更多的語法糖:
$.ajax({ url: url, success: function(data){ //… }, error: funtion(){ //… } });他允許將 done 和 fail 兩個函式的回撥放在 ajax 初始化的引數 success 和 fail 上,其原理還是一樣的,同樣,還有這樣的東西:
$.when(taskOne, taskTwo).done(function () { console.log(“都執行完畢後才會輸出我!”); }).fail(function(){ console.log(“只要有一個失敗,就會輸出我!”) });當 taskOne 和 taskTwo 都完成之後才執行 done 回撥,這個瀏覽器內建的 Promise 也有對應的函式:Promise.all([true, Promise.resolve(1), …]).then(function(value){ //....});瀏覽器內建的 Promise 還提供了一個 API:
Promise.race([true, Promise.resolve(1), …]).then(function(value){ //....}, function(reason){ //…});只要 race 引數中有一個 resolve 或者 reject,then 回撥就會出發。
寫的 就是基於事件響應的非同步模型,按理說,這個實現的邏輯是最清晰的,不過程式碼量稍微多一點。
function taskA(){ setTimeout(function(){ var result = “A”; E.emit(“taskA”, result); }, 1000); }function taskB(){ setTimeout(function(){ var result = “B”; E.emit(“taskB”, result); }, 1000); } E.all([“taskA”, “taskB”], function(A, B){ return A + B; });我沒有看他的原始碼,但是想想,應該是這個邏輯。只需要在訊息中心管理各個 emit 以及訊息註冊。這裡的錯誤處理值得思考下。
在半年前,也寫過一篇關於非同步程式設計的文章:,感興趣的可以去讀一讀。
文章比較長,閱讀了好幾天別人寫的東西,自己提筆還是比較輕鬆的,本文大概花費了 6 個小時撰寫。
本文主要解說了 Promise 的應用場景和實現原理,如果你能夠順暢的讀完全文並且之處文中的一些錯誤,說明你已經悟到了:)
Promise 使用起來不難,但是理解其原理還是有點偏頭痛的,所以下面列舉的幾篇相關閱讀也建議讀者點進去看看。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2035/viewspace-2801903/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- promise詳解Promise
- 詳解promisePromise
- TS 版 Promise 詳解Promise
- Promise用法詳解(一)Promise
- [Javascript] Promise ES6 詳細介紹JavaScriptPromise
- Promise和async await詳解PromiseAI
- Promise in JavascriptPromiseJavaScript
- Javascript — PromiseJavaScriptPromise
- javascript非同步解決方案之promiseJavaScript非同步Promise
- JavaScript this詳解JavaScript
- 手寫 Promise 詳細解讀Promise
- Javascript Promise用法JavaScriptPromise
- JavaScript Promise物件JavaScriptPromise物件
- JavaScript Promise 物件JavaScriptPromise物件
- ES6中Promise用法詳解Promise
- 詳解JavaScript原型JavaScript原型
- JavaScript之this詳解JavaScript
- JavaScript事件詳解JavaScript事件
- JavaScript Promise(基礎)JavaScriptPromise
- JavaScript閉包詳解JavaScript
- JavaScript中的this詳解JavaScript
- JavaScript小球碰壁詳解JavaScript
- JavaScript表格排序詳解JavaScript排序
- JavaScript arguments物件詳解JavaScript物件
- ES6中的Promise和Generator詳解Promise
- [Javascript] Promise question with async awaitJavaScriptPromiseAI
- JavaScript Promise 的使用技巧JavaScriptPromise
- JavaScript 裡的 Promise ChainingJavaScriptPromiseAI
- Javascript基礎之-PromiseJavaScriptPromise
- JavaScript之淺析PromiseJavaScriptPromise
- [JavaScript] Promise 與 Ajax/AxiosJavaScriptPromiseiOS
- javascript非同步與promiseJavaScript非同步Promise
- javascript 進階之 - PromiseJavaScriptPromise
- JavaScript 在 Promise.then 方法裡返回新的 PromiseJavaScriptPromise
- JavaScript中 Map 物件詳解JavaScript物件
- JavaScript之原型深入詳解JavaScript原型
- JavaScript繼承詳解(二)JavaScript繼承
- 玩轉 JavaScript 之詳解 thisJavaScript