javascript
JS Promise的实现原理
轉(zhuǎn)載自? ?JS Promise的實現(xiàn)原理
在前端開發(fā)過程中,會經(jīng)常使用到 Promise 模式,可以使異步代碼看起來如同步般清新易讀,從而從回調(diào)地獄中解脫出來。ES6中 已原生支持 Promise,但在未支持的瀏覽器中還需要通過 polyfill 模擬實現(xiàn)。下面介紹一下自己的實現(xiàn)過程,此實現(xiàn)可通過?Promise/A+測試集?的所有測試。
Promise 是一個關(guān)聯(lián)了執(zhí)行任務(wù)的承諾,當(dāng)你的任務(wù)完成時,會根據(jù)任務(wù)的成功與否,執(zhí)行相應(yīng)的操作。所以創(chuàng)建 promise 對象時,構(gòu)造函數(shù)中需要傳遞一個函數(shù)類型的參數(shù)(Chrome 的實現(xiàn)中,參數(shù)名叫resolver,我覺得叫task或worker也許會直觀一些。但此處采用它的命名,誰叫我是 Chrome 粉呢),來作為與此 promise 對象關(guān)聯(lián)的任務(wù)。因此,現(xiàn)在 Promise 構(gòu)造函數(shù)定義如下:
function Promise(resolver) {}Promise 對象有三種狀態(tài):pending、fullfilled?和?rejected,分別代表了 promise 對象處于等待、執(zhí)行成功和執(zhí)行失敗狀態(tài)。創(chuàng)建 promise 對象后處于pending狀態(tài),pending狀態(tài)可以轉(zhuǎn)化為fullfilled或rejected狀態(tài),但不能逆向轉(zhuǎn)化,且轉(zhuǎn)化過程只能有一次,即resolve或reject后不能再resolve或reject。因此需要在 promise 對象中持有狀態(tài)的引用,通過添加一個名為_state(為了說明是內(nèi)部屬性,用戶不要直接使用,屬性名前加了下劃線,后面同理)的屬性實現(xiàn)。現(xiàn)在 Promise 構(gòu)造函數(shù)定義如下:
function Promise(resolver) {this._status = 'pending'; }任務(wù)(resolver)內(nèi)封裝了需要執(zhí)行的異步操作(當(dāng)然,也可以是同步操作)。同時resolver調(diào)用時會被傳遞兩個參數(shù):resolve和reject函數(shù),來自于 Promise 內(nèi)部的封裝,分別代表任務(wù)執(zhí)行成功或者失敗時需要執(zhí)行的操作。任務(wù)成功與否由調(diào)用者控制,且需要在成功或失敗時調(diào)用resolve或reject函數(shù),以此來標(biāo)識當(dāng)前 promise 對象的完成,并會觸發(fā)后續(xù) promise 的執(zhí)行。
在調(diào)用Promise構(gòu)造函數(shù)時,resolver會被立即調(diào)用。因此,現(xiàn)在Promise構(gòu)造函數(shù)如下:
function Promise(resolver) {this._status = 'pending';resolver(resolve, reject);... }Promise 代表著一個承諾。作為承諾,總需要有一個結(jié)果,無論成功與否。如果成功,我們會獲得需要的結(jié)果;當(dāng)然也有可能會失敗。因此我們需要在這個承諾在未來某個時刻有結(jié)果時,分別針對結(jié)果的成功或失敗做相應(yīng)的處理。因此 Promise 中提供了then方法來完成這個任務(wù)。then方法接收兩個參數(shù):onResolve和onReject,分別代表當(dāng)前 promise 對象在成功或失敗時,接下來需要做的操作。現(xiàn)實生活中,人們總系喜歡給出各種許諾,同樣在代碼的世界里,我們也經(jīng)常會有一連串前后依賴的 promise 需要執(zhí)行,如下面的調(diào)用方式:promise.then().then()...。因此為了方便鏈?zhǔn)秸{(diào)用,then方法的實現(xiàn)中,都會返回一個新的 promise 對象,就像 jQuery 的方法中一般都會將自己(this)返回一樣(不同的是,jQuery中返回的是自身,但在 Promise 中,返回的是一個新的 promise 對象。如果此處也返回自身的話,則串行操作就變成并行操作了,顯然不符合我們的目標(biāo))。因此,then方法的定義如下:
Promise.prototype.then = function(onResolve, onReject) {var promise = new Promise(function() {});...return promise; }此處then方法內(nèi)創(chuàng)建的 promise 對象和暴露給用戶直接調(diào)用的 Promise 構(gòu)造函數(shù)所創(chuàng)建的 promise 對象有些不同。用戶調(diào)用 Promise 構(gòu)造函數(shù)時需要傳遞resolver參數(shù)代表與此 promise 對象關(guān)聯(lián)的任務(wù),且任務(wù)會立即執(zhí)行。在未來某個時刻,用戶根據(jù)任務(wù)執(zhí)行的結(jié)果來判斷任務(wù)是成功還是失敗,并且需要調(diào)用resolver中被傳入的參數(shù)resolve或reject來結(jié)束此 promise,并由此觸發(fā)下一個 promise(即當(dāng)前 promise 對象調(diào)用then方法所創(chuàng)建的 promise 對象)所關(guān)聯(lián)的任務(wù)的執(zhí)行。由此可知以下兩點:首先then方法中創(chuàng)建的 promise 關(guān)聯(lián)的任務(wù)不能在 promise 對象創(chuàng)建時立即執(zhí)行,所以先傳入一個空函數(shù)以符合 Promise 構(gòu)造函數(shù)調(diào)用格式;其次前一個 promise 對象需要能夠知道下一個 promise 對象是誰,其關(guān)聯(lián)的任務(wù)是什么,這樣才能在自己完成后調(diào)用下一個 promise 的任務(wù)。因此前一個 promise 需要持有下一個 promise 以及其任務(wù)的引用。由于 promise 的執(zhí)行可能會成功也可能會失敗,因此后一個 promise 一般會提供成功或失敗后需要執(zhí)行的任務(wù)供前一個 promise 調(diào)用。因此前一個 promise 持有下一個 promise 的任務(wù)引用時需要區(qū)分這一點。promise 的調(diào)用不一定都如promise.then().then()...這樣的串行方式,也可以有如下的并行方式:
var promise = new Promise(xxx);promise.then();promise.then();...此時當(dāng)前一個 promise 對象完成后,會同時調(diào)用兩個then方法中創(chuàng)建的 promise 關(guān)聯(lián)的任務(wù)。因此,前一個 promise 對象可能需要持有多個 promise 對象以及它們關(guān)聯(lián)的成功和失敗任務(wù)的引用。因此需要給 promise 對象添加屬性用于這些數(shù)據(jù)的記錄。可以有不同的方式實現(xiàn),如可以添加一個對象數(shù)組屬性,數(shù)組中的每一項是一個對象,里面有下一個 promise 以及成功、失敗回調(diào)的引用。即如下:
[{promise: promise1,doneCallback: doneCallback1,failCallback: failCallback1},{promise: promise2,doneCallback: doneCallback2,failCallback: failCallback2},...]當(dāng)然也可以有其它的方式實現(xiàn)。此處我采用了閉包的方式實現(xiàn):在 promise 對象中增加分別代表成功回調(diào)和失敗回調(diào)的兩個數(shù)組,數(shù)組中的每一項是通過內(nèi)部封裝的閉包函數(shù)調(diào)用的結(jié)果,也是一個函數(shù)。只不過這個函數(shù)可以訪問到內(nèi)部調(diào)用閉包時傳遞的 promise 對象,因此通過這種方式也可以訪問到我們需要的下一個 promise 以及其關(guān)聯(lián)的成功、失敗回調(diào)的引用。所以現(xiàn)在有兩處改動。首先需要在 Promise 構(gòu)造函數(shù)中增加兩個屬性。現(xiàn)在 Promise 構(gòu)造函數(shù)的定義如下:
function Promise(resolver) {this._status = 'pending';this._doneCallbacks = [];this._failCallbacks = [];resolver(resolve, reject);... }其次,需要在then方法中增加閉包調(diào)用以及為前一個 promise 對象保存引用。現(xiàn)在then的定義如下:
Promise.prototype.then = function(onResolve, onReject) {var promise = new Promise(function() {});this._doneCallbacks.push(makeCallback(promise, onResolve, 'resolve'));this._failCallbacks.push(makeCallback(promise, onReject, 'reject'));return promise; }then方法中調(diào)用的makeCallback即上面說到的閉包函數(shù)。調(diào)用時會把 promise 對象以及相應(yīng)的回調(diào)傳遞進(jìn)去,且會返回一個新的函數(shù),前一個 promise 對象持有返回函數(shù)的引用,這樣在調(diào)用返回函數(shù)時,在函數(shù)內(nèi)部就可以訪問到 promise 對象以及回調(diào)函數(shù)了。由于成功回調(diào)onResolve和失敗回調(diào)onReject都通過此閉包封裝,所以在閉包中增加了第三個參數(shù)action,以區(qū)分是哪種回調(diào)。現(xiàn)在makeCallback的定義如下:
function makeCallback(promise, callback, action) {return function promiseCallback(value) {...}; }前面說過,調(diào)用構(gòu)造函數(shù)創(chuàng)建 promise 對象時需要傳遞作為任務(wù)的函數(shù)resolver,resolver會被立即調(diào)用,并被傳遞參數(shù)resolve和reject函數(shù),用于結(jié)束當(dāng)前 promise 并觸發(fā)接下來的 promise 的調(diào)用。下面將介紹resolve和reject函數(shù)的實現(xiàn)。
我們使用 promise,是期望在未來的某個時刻能獲得一個結(jié)果,并且可用于接下來的 promise 調(diào)用。所以resolve函數(shù)需要有一個參數(shù)來接收結(jié)果(同樣,promise 執(zhí)行失敗后,我們也希望在后續(xù) promise 中獲得此失敗信息,做相應(yīng)處理。所以reject函數(shù)也需要有一個參數(shù)來接收錯誤)。前面說過 promise 對象的狀態(tài)只能由pending狀態(tài)轉(zhuǎn)換為fullfilled或rejected狀態(tài),且只能轉(zhuǎn)換一次。所以resolve或reject時,需要判斷一下狀態(tài)。所以,現(xiàn)在resolve和reject函數(shù)的定義如下:
function resolve(promise, data) {if (promise._status !== 'pending') {return;}promise._status = 'fullfilled';promise._value = data;run(promise);}function reject(promise, reason) {if (promise._status !== 'pending') {return;}promise._status = 'rejected';promise._value = reason;run(promise);}resolve和reject函數(shù)也可以定義在 Promise 構(gòu)造函數(shù)的 prototype 上,這樣可直接通過promise.resolve(data)或promise.reject(reason)調(diào)用,不用傳遞第一個參數(shù)promise。但由于此函數(shù)是內(nèi)部調(diào)用,為了不暴露不必要的接口給用戶,所以定義為內(nèi)部函數(shù)。由于執(zhí)行時需要知道是 resolve 或 reject 哪一個 promise 對象,所以需要多一個名為promise參數(shù)。resolve和reject函數(shù)中首先判斷了當(dāng)前 promise 的狀態(tài),如果不是pending(即已經(jīng)被 resolve 或 reject 過了,不再重復(fù)執(zhí)行),則直接返回。然后賦予 promise 新的狀態(tài),并保存成功或失敗的值。最后調(diào)用run函數(shù)。run函數(shù)用于觸發(fā)接下來的 promise 的執(zhí)行。run函數(shù)中需要注意的一點是,需要異步執(zhí)行相關(guān)的回調(diào)函數(shù)。run函數(shù)的定義如下:
function run(promise) {// `then`方法中也會調(diào)用,所以此處仍需做一次判斷if (promise._status === 'pending') {return;}var value = promise._value;var callbacks = promise._status === 'fullfilled'? promise._doneCallbacks: promise._failCallbacks;// Promise需要異步操作setTimeout(function () {for (var i = 0, len = callbacks.length; i < len; i++) {callbacks[i](value);}});// 每個promise只能被執(zhí)行一次。雖然`_doneCallbacks`和`_failCallbacks`用戶不應(yīng)該直接訪問,// 但還是可以訪問到,保險起見,做清空處理。promise._doneCallbacks = [];promise._failCallbacks = [];}run函數(shù)中調(diào)用的 callback,就是前面閉包函數(shù)makeCallback返回的函數(shù)。makeCallback函數(shù)是整個代碼中較復(fù)雜的部分。其實現(xiàn)過程基本是按照?Promises/A+規(guī)范(中文)(英文參見?Promises/A+規(guī)范(英文))中的[Promise 解決過程]部分來完成的。可參照規(guī)范部分,此處就不具體介紹了。
function makeCallback(promise, callback, action) {return function promiseCallback(value) {// 如果傳遞了callback,則使用前一個promise傳遞過來的值作為參數(shù)調(diào)用callback,// 并根據(jù)callback的調(diào)用結(jié)果來處理當(dāng)前promiseif (typeof callback === 'function') {var x;try {x = callback(value);}catch (e) {// 如果調(diào)用callback時拋出異常,則直接用此異常對象reject當(dāng)前promisereject(promise, e);}// 如果callback的返回值是當(dāng)前promise,為避免造成死循環(huán),需要拋出異常// 根據(jù)Promise+規(guī)范,此處應(yīng)拋出TypeError異常if (x === promise) {var reason = new TypeError('TypeError: The return value could not be same with the promise');reject(promise, reason);}// 如果返回值是一個Promise對象,則當(dāng)返回的Promise對象被resolve/reject后,再resolve/reject當(dāng)前Promiseelse if (x instanceof Promise) {x.then(function (data) {resolve(promise, data);},function (reason) {reject(promise, reason);});}else {var then;(function resolveThenable(x) {// 如果返回的是一個Thenable對象(此處邏輯有點坑,參照Promise+的規(guī)范實現(xiàn))if (x && (typeof x === 'object'|| typeof x === 'function')) {try {then = x.then;}catch (e) {reject(promise, e);return;}if (typeof then === 'function') {// 調(diào)用Thenable對象的`then`方法時,傳遞進(jìn)去的`resolvePromise`和`rejectPromise`方法(及下面的兩個匿名方法)// 可能會被重復(fù)調(diào)用。但Promise+規(guī)范規(guī)定這兩個方法有且只能有其中的一個被調(diào)用一次,多次調(diào)用將被忽略。// 此處通過`invoked`來處理重復(fù)調(diào)用var invoked = false;try {then.call(x,function (y) {if (invoked) {return;}invoked = true;// 避免死循環(huán)if (y === x) {throw new TypeError('TypeError: The return value could not be same with the previous thenable object');}// y仍有可能是thenable對象,遞歸調(diào)用resolveThenable(y);},function (e) {if (invoked) {return;}invoked = true;reject(promise, e);});}catch (e) {// 如果`resolvePromise`和`rejectPromise`方法被調(diào)用后,再拋出異常,則忽略異常// 否則用異常對象reject此Promise對象if (!invoked) {reject(promise, e);}}}else {resolve(promise, x);}}else {resolve(promise, x);}}(x));}}// 如果未傳遞callback,直接用前一個promise傳遞過來的值resolve/reject當(dāng)前Promise對象else {action === 'resolve'? resolve(promise, value): reject(promise, value);}};}至此,Promise 的實現(xiàn)過程大致就介紹完了,當(dāng)然還有一些細(xì)節(jié),如 Promise 中一般會提供done和fail方法(可能是其它命名,不要在意這些細(xì)節(jié)),用于在不需要考慮成功或失敗處理時調(diào)用,其實就是then方法的簡略形式;還有一般還會提供 Promise 構(gòu)造函數(shù)上的靜態(tài)方法resolve和reject用于直接返回一個被 resolved 或 rejected 的 promise 對象;另外,還會提供race和all靜態(tài)方法,用于處理當(dāng)一組 promise 中任意一個完成和全部都完成情況時的情況。具體代碼可參考?我的Promise實現(xiàn)。
之前比較懶,第一次開始寫 blog。雖然花了不少時間,但依然覺得寫得和狗屎一樣爛,再接再厲吧。
總結(jié)
以上是生活随笔為你收集整理的JS Promise的实现原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: i5电脑(i5配置的电脑)
- 下一篇: 探讨JS合并两个数组的方法