本文首發(fā)于 vivo互聯(lián)網(wǎng)技術 微信公眾號
鏈接: https://mp.weixin.qq.com/s/UNzYgpnKzmW6bAapYxnXRQ
作者:孔垂亮目前創(chuàng)新互聯(lián)建站已為成百上千家的企業(yè)提供了網(wǎng)站建設、域名、虛擬主機、綿陽服務器托管、企業(yè)網(wǎng)站設計、同心網(wǎng)站維護等服務,公司將堅持客戶導向、應用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。
很多同學在學習 Promise 時,知其然卻不知其所以然,對其中的用法理解不了。 本系列文章由淺入深逐步實現(xiàn) Promise,并結(jié)合流程圖、實例以及動畫進行演示,達到深刻理解 Promise 用法的目的。
本系列文章有如下幾個章節(jié)組成:
圖解 Promise 實現(xiàn)原理(一)—— 基礎實現(xiàn)
圖解 Promise 實現(xiàn)原理(二)—— Promise 鏈式調(diào)用
圖解 Promise 實現(xiàn)原理(三)—— Promise 原型方法實現(xiàn)
圖解 Promise 實現(xiàn)原理(四)—— Promise 靜態(tài)方法實現(xiàn)
本文適合對 Promise 的用法有所了解的人閱讀,如果還不清楚,請自行查閱阮一峰老師的 《 ES6入門 之 Promise 對象》。
Promise 規(guī)范有很多,如 Promise/A,Promise/B,Promise/D 以及 Promise/A 的升級版 Promise/A+,有興趣的可以去了解下,最終 ES6 中采用了 Promise/A+ 規(guī)范。所以本文的Promise源碼是按照 Promise/A+規(guī)范來編寫的(不想看英文版的移步 Promise/A+規(guī)范中文翻譯)。
為了讓大家更容易理解,我們從一個場景開始,一步一步跟著思路思考,會更容易看懂。
考慮下面一種獲取用戶 id 的請求處理:
//不使用Promise http.get('some_url', function (result) { //do something console.log(result.id); });//使用Promisenew Promise(function (resolve) { //異步請求 http.get('some_url', function (result) { resolve(result.id) }) }).then(function (id) { //do something console.log(id); })
說到底,Promise 也還是使用回調(diào)函數(shù),只不過是把回調(diào)封裝在了內(nèi)部,使用上一直通過 then 方法的鏈式調(diào)用,使得多層的回調(diào)嵌套看起來變成了同一層的,書寫上以及理解上會更直觀和簡潔一些。
//極簡的實現(xiàn)class Promise { callbacks = []; constructor(fn) { fn(this._resolve.bind(this)); } then(onFulfilled) { this.callbacks.push(onFulfilled); } _resolve(value) { this.callbacks.forEach(fn => fn(value)); } }//Promise應用let p = new Promise(resolve => { setTimeout(() => { console.log('done'); resolve('5秒'); }, 5000); }).then((tip) => { console.log(tip); })
調(diào)用 then 方法,將想要在 Promise 異步操作成功時執(zhí)行的 onFulfilled 放入callbacks隊列,其實也就是注冊回調(diào)函數(shù),可以向觀察者模式方向思考;
創(chuàng)建 Promise 實例時傳入的函數(shù)會被賦予一個函數(shù)類型的參數(shù),即 resolve,它接收一個參數(shù) value,代表異步操作返回的結(jié)果,當異步操作執(zhí)行成功后,會調(diào)用resolve方法,這時候其實真正執(zhí)行的操作是將 callbacks 隊列中的回調(diào)一一執(zhí)行。
(圖:基礎版本實現(xiàn)原理)
首先 new Promise 時,傳給 Promise 的函數(shù)設置定時器模擬異步的場景,接著調(diào)用 Promise 對象的 then 方法注冊異步操作完成后的 onFulfilled,最后當異步操作完成時,調(diào)用 resolve(value), 執(zhí)行 then 方法注冊的 onFulfilled。
then 方法注冊的 onFulfilled 是存在一個數(shù)組中,可見 then 方法可以調(diào)用多次,注冊的多個onFulfilled 會在異步操作完成后根據(jù)添加的順序依次執(zhí)行。如下:
//then 的說明let p = new Promise(resolve => { setTimeout(() => { console.log('done'); resolve('5秒'); }, 5000); }); p.then(tip => { console.log('then1', tip); }); p.then(tip => { console.log('then2', tip); });
上例中,要先定義一個變量 p ,然后 p.then 兩次。而規(guī)范中要求,then 方法應該能夠鏈式調(diào)用。 實現(xiàn)也簡單,只需要在 then 中 return this 即可。如下所示:
//極簡的實現(xiàn)+鏈式調(diào)用class Promise { callbacks = []; constructor(fn) { fn(this._resolve.bind(this)); } then(onFulfilled) { this.callbacks.push(onFulfilled); return this;//看這里 } _resolve(value) { this.callbacks.forEach(fn => fn(value)); } }let p = new Promise(resolve => { setTimeout(() => { console.log('done'); resolve('5秒'); }, 5000); }).then(tip => { console.log('then1', tip); }).then(tip => { console.log('then2', tip); });
(圖:基礎版本的鏈式調(diào)用)
上面 Promise 的實現(xiàn)存在一個問題:如果在 then 方法注冊 onFulfilled 之前,resolve 就執(zhí)行了,onFulfilled 就不會執(zhí)行到了。比如上面的例子中我們把 setTimout 去掉:
//同步執(zhí)行了resolvelet p = new Promise(resolve => { console.log('同步執(zhí)行'); resolve('同步執(zhí)行'); }).then(tip => { console.log('then1', tip); }).then(tip => { console.log('then2', tip); });
執(zhí)行結(jié)果顯示,只有 "同步執(zhí)行" 被打印了出來,后面的 "then1" 和 "then2" 均沒有打印出來。再回去看下 Promise 的源碼,也很好理解,resolve 執(zhí)行時,callbacks 還是空數(shù)組,還沒有onFulfilled 注冊上來。
這顯然是不允許的,Promises/A+規(guī)范明確要求回調(diào)需要通過異步方式執(zhí)行,用以保證一致可靠的執(zhí)行順序。因此要加入一些處理,保證在 resolve 執(zhí)行之前,then 方法已經(jīng)注冊完所有的回調(diào):
//極簡的實現(xiàn)+鏈式調(diào)用+延遲機制class Promise { callbacks = []; constructor(fn) { fn(this._resolve.bind(this)); } then(onFulfilled) { this.callbacks.push(onFulfilled); return this; } _resolve(value) { setTimeout(() => {//看這里 this.callbacks.forEach(fn => fn(value)); }); } }
(圖:延遲機制)
但是這樣依然存在問題,在 resolve 執(zhí)行后,再通過 then 注冊上來的 onFulfilled 都沒有機會執(zhí)行了。如下所示,我們加了延遲后,then1 和 then2 可以打印出來了,但下例中的 then3 依然打印不出來。所以我們需要增加狀態(tài),并且保存 resolve 的值。
let p = new Promise(resolve => { console.log('同步執(zhí)行'); resolve('同步執(zhí)行'); }).then(tip => { console.log('then1', tip); }).then(tip => { console.log('then2', tip); }); setTimeout(() => { p.then(tip => { console.log('then3', tip); }) });
為了解決上一節(jié)拋出的問題,我們必須加入狀態(tài)機制,也就是大家熟知的 pending、fulfilled、rejected。
Promises/A+ 規(guī)范中明確規(guī)定了,pending 可以轉(zhuǎn)化為 fulfilled 或 rejected 并且只能轉(zhuǎn)化一次,也就是說如果 pending 轉(zhuǎn)化到 fulfilled 狀態(tài),那么就不能再轉(zhuǎn)化到 rejected。并且 fulfilled 和 rejected 狀態(tài)只能由 pending 轉(zhuǎn)化而來,兩者之間不能互相轉(zhuǎn)換。
增加狀態(tài)后的實現(xiàn)是這樣的
//極簡的實現(xiàn)+鏈式調(diào)用+延遲機制+狀態(tài)class Promise { callbacks = []; state = 'pending';//增加狀態(tài) value = null;//保存結(jié)果 constructor(fn) { fn(this._resolve.bind(this)); } then(onFulfilled) { if (this.state === 'pending') {//在resolve之前,跟之前邏輯一樣,添加到callbacks中 this.callbacks.push(onFulfilled); } else {//在resolve之后,直接執(zhí)行回調(diào),返回結(jié)果了 onFulfilled(this.value); } return this; } _resolve(value) { this.state = 'fulfilled';//改變狀態(tài) this.value = value;//保存結(jié)果 this.callbacks.forEach(fn => fn(value)); } }
注意:當增加完狀態(tài)之后,原先的_resolve中的定時器可以去掉了。當reolve同步執(zhí)行時,雖然callbacks為空,回調(diào)函數(shù)還沒有注冊上來,但沒有關系,因為后面注冊上來時,判斷狀態(tài)為fulfilled,會立即執(zhí)行回調(diào)。
(圖:Promise 狀態(tài)管理)
實現(xiàn)源碼中只增加了 fulfilled 的狀態(tài) 和 onFulfilled 的回調(diào),但為了完整性,在示意圖中增加了 rejected 和 onRejected 。后面章節(jié)會實現(xiàn)。
resolve 執(zhí)行時,會將狀態(tài)設置為 fulfilled ,并把 value 的值存起來,在此之后調(diào)用 then 添加的新回調(diào),都會立即執(zhí)行,直接返回保存的value值。
(Promise 狀態(tài)變化演示動畫)
詳情請點擊: https://mp.weixin.qq.com/s/UNzYgpnKzmW6bAapYxnXRQ
至此,一個初具功能的Promise就實現(xiàn)好了,它實現(xiàn)了 then,實現(xiàn)了鏈式調(diào)用,實現(xiàn)了狀態(tài)管理等等。但仔細想想,鏈式調(diào)用的實現(xiàn)只是在 then 中 return 了 this,因為是同一個實例,調(diào)用再多次 then 也只能返回相同的一個結(jié)果,這顯然是不能滿足我們的要求的。下一節(jié),講述如何實現(xiàn)真正的鏈式調(diào)用。