摘要:因此,當作為參數的執行任意結果的回調函數時,就會將參數傳遞給外層的,執行對應的回調函數。
背景
在上一篇博客[[譯]前端基礎知識儲備——Promise/A+規范](https://segmentfault.com/a/11...,我們介紹了Promise/A+規范的具體條目。在本文中,我們來選擇了promiz,讓大家來看下一個具體的Promise庫的內部代碼是如何運作的。
promiz是一個體積很小的promise庫(官方介紹約為913 bytes (gzip)),作為一個ES2015標準中的Promise的polyfill,實現了諸如resolve、all和race等API。
知識儲備我們在這里簡單回顧一下Promise/A+的主要關鍵點,如果需要了解詳細內容的同學,可以閱讀我的上一篇博客。
Promise有三個狀態,分別為pending、fulfilled和rejected,且只能從pending到fulfilled或者rejected,沒有其他的流轉方式。
Promise的返回值是一個新的Promise,原因見上一條。
傳遞給then函數的兩個回調函數,有且僅有一次機會被執行(即執行了onfulfilled就不會執行onrejected函數,且只執行一次)。
代碼實現與分析 異步執行器在介紹Promise之前,我們先介紹一下異步執行器。在Promise中,我們需要一個異步的執行器來異步執行我們的回調函數。在規范中提到,通常情況下,我們可以使用微任務(nextTick)或者宏任務(setTimeout)來實現。但是,如果我們需要兼容Web Worker這種情況的話,我們可能還需要一些更多的方式來處理。具體代碼如下:
var queueId = 1 var queue = {} var isRunningTask = false // 使用postMessage來執行異步函數 if (!global.setImmediate) global.addEventListener("message", function (e) { if (e.source == global) { if (isRunningTask) nextTick(queue[e.data]) else { isRunningTask = true try { queue[e.data]() } catch (e) {} delete queue[e.data] isRunningTask = false } } }) /** * 異步執行方法 * @param {function} fn 需要執行的回調函數 */ function nextTick(fn) { if (global.setImmediate) setImmediate(fn) // 如果在Web Worker中使用以下方法 else if (global.importScripts) setTimeout(fn) else { queueId++ queue[queueId] = fn global.postMessage(queueId, "*") } }
以上代碼比較簡單,我們簡單說明下:
在代碼中,promiz使用了setImmediate、setTimeout和postMessage這三個方法來執行異步函數,其中:
setImmedeate,只有IE實現了該方法,在執行完隊列中的代碼后立即執行。
PostMessage,新增的H5中的方法。
setTimeout,兼容性最佳,可以適用各種場景。
因此,在promiz的這段代碼中,有一定的兼容性問題,應該把setTimeout放到最后作為一個兜底策略,否則無法在老瀏覽器中執行。
構造函數說完了異步函數執行器,我們來看下promise的構造函數。
首先我們來看下內存數據,我們需要存儲當前promise的狀態、成功的值或者失敗的原因、下一個promise的引用和成功與失敗的回調函數。因此,我們需要以下變量:
// states // 0: pending // 1: resolving // 2: rejecting // 3: resolved // 4: rejected var self = this, state = 0, // promise狀態 val = 0, // success callback返回值 next = [], // 返回的新的promise對象 fn, er; // then方法中的成功回調函數和失敗回調函數
在存儲完相關數據后,我們來看下構造函數。
function Deferred(resolver) { ... self = this; try { if (typeof resolver == "function") resolver(self["resolve"], self["reject"]) } catch (e) { self["reject"](e) } }
構造函數非常簡單,除了聲明相關的函數,就只有執行傳入的callback而已。當然,如果我們不是鏈式調用的第一個promise,那么我們會沒有resolver參數,因此不需要在此執行,我們會在then函數執行resolve方法。
下面我們來看下上面提到的處理函數resovle和reject。
self["resolve"] = function (v) { fn = self.fn er = self.er if (!state) { val = v state = 1 nextTick(fire) } return self } self["reject"] = function (v) { fn = self.fn er = self.er if (!state) { val = v state = 2 nextTick(fire) } return self } self["then"] = function (_fn, _er) { if (!(this._d == 1)) throw TypeError() var d = new Deferred() d.fn = _fn d.er = _er if (state == 3) { d.resolve(val) } else if (state == 4) { d.reject(val) } else { next.push(d) } return d }
在resolve和reject這兩個函數中,都是改變了內部promise的狀態,給定了參數值,同時異步觸發了fire函數。而then方法,則是生成了一個新的Deferred對象,并且完成了相關的初始化(執行完then方法我們就會得到這個新生成的Deferred對象,也就是一個新的Promise);當前一個promise到達resolved狀態時,不需要等待則直接出發resolve方法,rejected狀態時也一樣。那么,讓我們來看下fire方法到底是做什么的呢?
function fire() { // 檢測是不是一個thenable對象 var ref; try { ref = val && val.then } catch (e) { val = e state = 2 return fire() } thennable(ref, function () { state = 1 fire() }, function () { state = 2 fire() }, function () { try { if (state == 1 && typeof fn == "function") { val = fn(val) } else if (state == 2 && typeof er == "function") { val = er(val) state = 1 } } catch (e) { val = e return finish() } if (val == self) { val = TypeError() finish() } else thennable(ref, function () { finish(3) }, finish, function () { finish(state == 1 && 3) }) }) }
從上面的代碼來看,fire函數只是判斷了ref是不是一個thenable對象,然后調用了thenable函數,傳遞了3個回調函數。那么這些回調函數到底是做什么用的呢?我們需要來看下thenable函數的實現代碼。
// ref:指向thenable對象的`then`函數 // cb, ec, cn : successCallback, failureCallback, notThennableCallback function thennable(ref, cb, ec, cn) { // Promises can be rejected with other promises, which should pass through if (state == 2) { return cn() } if ((typeof val == "object" || typeof val == "function") && typeof ref == "function") { try { // cnt變量用來保證成功和失敗的回調函數總共只會被執行一次 var cnt = 0 ref.call(val, function (v) { if (cnt++) return val = v cb() }, function (v) { if (cnt++) return val = v ec() }) } catch (e) { val = e ec() } } else { cn() } };
在thenable函數中,如果判斷當前的promise的狀態是處于rejecting時,會直接執行cn,也就是將reject狀態傳遞下去。而如果當ref不是一個thenable對象的then函數時(那么此時值為undefined),那么就會直接執行cn。
通過fire函數傳遞的三個callback我們可以看到,cn是在promise的狀態改變時,針對特定的狀態來觸發相對應的onfulfilled或者onrejected回調函數。
只有當ref是一個thenable時(傳遞給resolve的是一個promise),代碼才會進入上面的try catch邏輯中。
Promise執行流程看完了上面的各部分代碼,我相信大家可能對整個執行流程仍然不夠熟悉,下面,我們將這些流程拼接起來,通過幾個完整的流程來說明下。
鏈式調用第一個Promise當我們聲明一個promise式,我們會傳入一個resolver。此時,整個Deferred對象的state是0。如果我們在resolver里面調用了resolve方法,那么我們的state就會變成1,然后出發fire函數注冊到thenable函數里面的第三個回調函數,從而將值傳遞給下一個thenable。當thenable的then函數執行完成(即我們看到的Promise后面跟著的then函數執行完成以后),我們的state才會變成3,也就是說上一個Promise才會結束,返回一個新的Promise。
鏈式調用非第一個Promise如果不是第一個Promise,那么我們就沒有resolver參數。因此,我們的resolve方法并不是通過在resolver中進行調用的,而是將回調函數fn注冊進來,在上一個Promise完成后主動調用執行的。也就是說,我們在上一個Promise執行完then函數并且返回一個新的Promise時,我們這個返回的Promise就已經進入了resolving的狀態。
給resolve傳遞一個Promise在Promise/A+規范中,如果我們給resolve傳遞一個promise,那么我們的通過resolve獲取到的值就是傳遞進去的這個promise返回的值。當然,我們也必須等待作為參數的這個promise處理完成后,才會處理外面的這個promise。
在promiz的代碼中,我們如果通過resolve接收到一個promise,那么我們在fire函數中就會吧promise.then的引用傳遞給thenable函數。在thenable函數中,我們會將我們當前promise需要執行的onfulfilled和onrejected封裝成一個函數,傳遞給作為參數的promise的then函數。因此,當作為參數的promise執行任意結果的回調函數時,就會將參數傳遞給外層的promise,執行對應的回調函數。
全局執行方法 Promise.all讓我們先看代碼。
Deferred.all = function (arr) { if (!(this._d == 1)) throw TypeError() if (!(arr instanceof Array)) return Deferred.reject(TypeError()) var d = new Deferred() function done(e, v) { if (v) return d.resolve(v) if (e) return d.reject(e) var unresolved = arr.reduce(function (cnt, v) { if (v && v.then) return cnt + 1 return cnt }, 0) if (unresolved == 0) d.resolve(arr) arr.map(function (v, i) { if (v && v.then) v.then(function (r) { arr[i] = r done() return r }, done) }) } done() return d }
在Promise.all中,我們使用了一個計數器來進行統計,在每一個Promise后面都增加一個then函數用于增加計數。當Promise成功時則計數+1。當整個數組中的Promise都已經進入resolved狀態時,我們才會執行thenable的then函數。如果有一個失敗的話,則立即進入reject流程。
總結從代碼設計層面來看,promiz的代碼量較少,閱讀也較為簡單。但是,在某些細節的設計上,promiz還是體現出了較為巧妙的思路,如在處理作為入參的promise時,能夠在這個promise后面動態的添加一個then函數,從而獲取數據給外面的promise。
如果大家有興趣,建議自己根據本文的說明閱讀一遍源碼,配合Promise/A+規范來看下是如何實現每一條規范的。
下一篇博客,我們將為大家從頭開始,來實現一個Promise庫。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/98911.html
摘要:寫在最后總體來說,是一個小而美的框架,值得我們來折騰一下,以上均為本人理解,如有錯誤還請指出,不勝感激一個硬廣我所在團隊工作地點在北京求大量前端社招實習,有意者可發簡歷至 寫在前面 沒錯,又是一個新的前端框架,hyperapp非常的小,僅僅1kb,當然學習起來也是非常的簡單,可以說是1分鐘入門。聲明式:HyperApp 的設計基于Elm Architecture(這也意味著組件更多的是...
摘要:專有的內容更少,而更多符合標準的成分。當前標簽實例的方法被調用時當前標簽的任何一個祖先的被調用時更新從父親到兒子單向傳播。相對來說,微型場景會更適合,不想要太多的外部依賴,又需要組件化數據驅動等更現代化框架的能力。 Riot.js是什么? Riot 擁有創建現代客戶端應用的所有必需的成分: 響應式 視圖層用來創建用戶界面 用來在各獨立模塊之間進行通信的事件庫 用來管理URL和瀏覽器回...
摘要:專有的內容更少,而更多符合標準的成分。當前標簽實例的方法被調用時當前標簽的任何一個祖先的被調用時更新從父親到兒子單向傳播。相對來說,微型場景會更適合,不想要太多的外部依賴,又需要組件化數據驅動等更現代化框架的能力。 Riot.js是什么? Riot 擁有創建現代客戶端應用的所有必需的成分: 響應式 視圖層用來創建用戶界面 用來在各獨立模塊之間進行通信的事件庫 用來管理URL和瀏覽器回...
摘要:這部分比較容易讓人產生疑惑的是循環部分,這個循環有什么用呢舉個例子以上代碼是最簡單的情況,監聽事件,當變化時,打印出在源碼中,當第一次進入后,緊接著被置為,而事件觸發回調函數也不會更改的值,因此再次判斷時條件不成立,內的代碼段只會執行一次。 本文已同步在我的博客 在這個react和vue如日中天、jquery逐漸被大家拋棄的年代,我還是想要來說一說backbone。 16年6月初,在沒...
摘要:主張,小而美被實踐是最好用的,本文將介紹筆者收集的一些非常贊的開源庫。是帶有消息通知的數據中心,我稱其為會說話的數據。迷你檢查庫,這個幾乎涵蓋了全部的各種檢測。最后向大家推薦依稀,這里收集了太多小而美的庫,自己來淘寶吧。 最近看著下自己的github star,把我嚇壞了,手賤黨,收藏癖的我都收藏了300+個倉庫了,是時候整理一下了。 Unix主張kiss,小而美被實踐是最好用的,本文...
閱讀 2577·2021-11-22 13:53
閱讀 4085·2021-09-28 09:47
閱讀 870·2021-09-22 15:33
閱讀 820·2020-12-03 17:17
閱讀 3321·2019-08-30 13:13
閱讀 2126·2019-08-29 16:09
閱讀 1183·2019-08-29 12:24
閱讀 2455·2019-08-28 18:14