前言
按照文檔說明簡單地實現 ES6 Promise
的各個方法並不難,但是Promise
的一些特殊需求實現起來並不簡單,我首先提出一些不好實現或者容易忽略的需求:
- 數據傳遞
- 回調綁定
- 將回調變成 microtask
- 實現 then/finally 返回的
pending
promise “跟隨”它們的回調返回的pending
promise - 實現 resolve 返回的 promise “跟隨”它的
thenable
對象參數
實現框架
在解決上述問題前,我們先實現一個框架。
首先,我的目的是實現一個Promise
插件,它包括:
- 構造函數:Promise
- 靜態方法:resolve、reject、all 和 race
- 實例方法:then、catch 和 finally
- 私有函數:identity、thrower 和 isSettled 等
如下:
;(function() {
function Promise(executor) {
}
Object.defineProperties(Promise, {
resolve: {
value: resolve,
configurable: true,
writable: true
},
reject: {
value: reject,
configurable: true,
writable: true
},
race: {
value: race,
configurable: true,
writable: true
},
all: {
value: all,
configurable: true,
writable: true
}
});
Promise.prototype = {
constructor: Promise,
then: function(onFulfilled, onRejected) {
},
catch: function(onRejected) {
},
finally: function(onFinally) {
},
}
function identity(value) {
return value;
}
function thrower(reason) {
throw reason;
}
function isSettled(pro) {
return pro instanceof Promise ? pro.status === 'fulfilled' || pro.status === 'rejected' : false;
}
window.Promise = Promise;
})();
解決問題
接下來,我們解決各個問題。
數據傳遞
爲了傳遞數據——回調函數需要用到的參數以及 promise 的狀態,我們首先在構造函數Promise
中添加status
、value
和reason
屬性,並且在構造函數中執行 executor
函數:
function Promise(executor) {
var self = this;
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
typeof executor === 'function' ? executor.call(null,
function(value) {
self.value = value;
self.status = 'fulfilled';
},
function(reason) {
self.reason = reason;
self.status = 'rejected';
}) : false;
}
按照文檔說明,爲了實現鏈式調用,Promise
的所有方法都會返回一個 Promise 對象,而且除了Promise.resolve(peomiseObj) 這種情況外都是新生成的 Promise 對象。所以接下來我的大部分方法都會返回一個新的 promise 對象。不生成新對象的特例:
var a = Promise.resolve('a'),
b = Promise.resolve(a);
console.log(a === b) //true
回調綁定
接下來,我們要將then
、catch
和finally
中的回調方法綁定到Promise
對象的狀態這個事件上。
我想到的第一個事件就是onchange
事件,但是 promiseObj.status 屬性上並沒有change
事件。但是,我馬上想到每次設置accessor
屬性的值時,就會調用 accessor 屬性的setter
方法。那麼,我只要把status
屬性設置爲存取屬性,然後在它的 setter 方法裏綁定回調函數就行啦!如下:
function Promise(executor) {
var self = this;
//存儲狀態的私有屬性
this._status = 'pending';
this.value = undefined;
this.reason = undefined;
//this.events = new Events();
//存儲狀態的公開屬性
Object.defineProperty(this, 'status', {
get: function() {
return self._status;
},
set: function(newValue) {
self._status = newValue;
//self.events.fireEvent('change');
},
configurable: true
});
typeof executor === 'function' ? executor.call(null,
function(value) {
self.value = value;
self.status = 'fulfilled';
},
function(reason) {
self.reason = reason;
self.status = 'rejected';
}) : false;
}
爲了綁定回調函數,我使用了發佈訂閱模式。在then
、catch
和finally
方法執行的時候訂閱事件change
,將自己的回調函數綁定到change
事件上,promiseObj.status 屬性是發佈者,一旦它的值發生改變就發佈change
事件,執行回調函數。
爲了節省篇幅,不那麼重要的發佈者Events
() 構造函數及其原型我就不貼代碼了,文章末尾我會給出源代碼。
實現 microtask
then
、catch
和finally
方法的回調函數都是microtask
,當滿足條件(promise 對象狀態改變)時,這些回調會被放入microtask
隊列。每當調用棧中的macrotask
執行完畢時,立刻執行microtask
隊列中的所有microtask
,這樣一次事件循環就結束了,js引擎等待下一次循環。
要我實現microtask
我是做不到的,我就只能用macrotask
模仿一下microtask
了,就像有些大人喜歡冒充小學生過兒童節一樣,我用宏任務冒充一下微任務應該問題不大(纔怪)。
我用 setTimeout 發佈的macrotask
進行模仿:
Object.defineProperty(this, 'status', {
get: function() {
return self._status;
},
set: function(newValue) {
self._status = newValue;
setTimeout(() = >{
self.events.fireEvent('change');
},
0);
},
configurable: true
});
實現函數
接下來,我們實現各個函數和方法。在知道方法的參數和返回值後再實現方法如有神助,而實現過程中最難處理的就是 pending 狀態的 promise 對象,因爲我們要等它變成其它狀態時,再做真正的處理。下面我拿出兩個最具代表性的方法來分析。
靜態方法all
如果忘記了 Promise.all
(iterable) 的參數和返回值,可以返回我上一篇文章查看。
function all(iterable) {
//如果 iterable 不是一個可迭代對象
if (iterable[Symbol.iterator] == undefined) {
let err = new TypeError(typeof iterable + iterable + ' is not iterable (cannot read property Symbol(Symbol.iterator))');
return Promise.reject(err);
}
//如果 iterable 對象爲空
if (iterable.length === 0) {
return Promise.resolve([]);
}
//其它情況用異步處理
var pro = new Promise(),
//all 返回的 promise 對象
valueArr = []; //all 返回的 promise 對象的 value 屬性
setTimeout(function() {
var index = 0,
//記錄當前索引
count = 0,
len = iterable.length;
for (let val of iterable) { -
function(i) {
if (val instanceof Promise) { //當前值爲 Promise 對象時
if (val.status === 'pending') {
val.then(function(value) {
valueArr[i] = value;
count++;
//Promise.all([new Promise(function(resolve){setTimeout(resolve, 100, 1)}), 2, 3, 4])
if (count === len) {
pro.value = valueArr;
pro.status = 'fulfilled';
}
},
function(reason) {
pro.reason = reason;
pro.status = 'rejected';
//當一個pending Promise首先完成時,解除其它 pending Promise的事件,防止之後其它 Promise 改變 pro 的狀態
for (let uselessPromise of iterable) {
if (uselessPromise instanceof Promise && uselessPromise.status === 'pending') {
uselessPromise.events.removeEvent('change');
}
}
});
} else if (val.status === 'rejected') {
pro.reason = val.reason;
pro.status = 'rejected';
return;
} else {
//val.status === 'fulfilled'
valueArr[i] = val.value;
count++;
}
} else {
valueArr[i] = val;
count++;
}
index++;
} (index);
}
//如果 iterable 對象中的 promise 對象都變爲 fulfilled 狀態,或者 iterable 對象內沒有 promise 對象,
//由於我們可能需要等待 pending promise 的結果,所以要額外花費一個變量計數,而不能用valueArr的長度判斷。
if (count === len) {
pro.value = valueArr;
pro.status = 'fulfilled';
}
},
0);
return pro;
}
這裏解釋兩點:
1、如何保證 value 數組中值的順序
如果iterable對象中的 promise 對象都變爲 fulfilled 狀態,或者 iterable 對象內沒有 promise 對象,all 返回一個 fulfilled promise 對象,且其 value 值爲 iterable 中各項值組成的數組,數組中的值將會按照 iterable 內的順序排列,而不是由 pending promise 的完成順序決定。
爲了保證 value 數組中值的順序,最簡單的方法是
valueArr[iterable.indexOf(val)] = val.value;
但是像除 Array、TypedArray 和 String 外的 Map 和 Set 原生 iterabe 對象,以及其它通過myIterable[Symbol.iterator] 創建的自定義的 iterable 對象都沒有 indexOf 方法,所以我選擇用閉包來保證 value 數組值的順序。
2、處理 pending promise 對象。
pending promise 是導致這個函數要額外添加很多變量存儲狀態,額外做很多判斷和處理的罪魁禍首。
如果 iterabe 對象中有一個pending
狀態的 promise(通常爲一個異步的 promise),我們就使用then
方法來持續關注它的動態。
- 一旦它變成
fulfilled
promise,就將它的 value 加入 valueArr 數組。我們添加一個 count 變量記錄目前 valueArr 獲取到了多少個值,當全部獲取到值後,就可以給 pro.value 和pro.status 賦值了。之所以用 count 而不是 valueArr.length 判斷,是因爲 valueArr = [undefined,undefined,undefined,1] 的長度也爲4,這樣可能導致還沒獲取到 pending promise 的值就改變 pro.status 了。 - 而當它變成
rejected
promise 時,我們就更新 all 方法返回的對象的 reason 值,同時改變狀態 status 爲 rejected,觸發綁定的onrejected
函數。另外,爲了與原生 Promise 表現相同:如果 iterable 對象中任意一個 pending promise 對象狀態變爲rejected
,將不再持續關注其它 pending promise 的動態。而我早就在所有的 pending promise 上都綁定了 onfulfilled 和 onrejected 函數,用來跟蹤它們。所以我需要在某個 pending promise 變爲 rejected promise 時,刪除它們綁定的回調函數。
實例方法then
Promise.prototype.then
(onFulfilled, onRejected):
Promise.prototype.then = function(onFulfilled, onRejected) {
var pro = new Promise();
//綁定回調函數,onFulfilled 和 onRejected 用一個回調函數處理
this.events.addEvent('change', hander.bind(null, this));
function hander(that) {
var res; //onFulfilled 或 onRejected 回調函數執行後得到的結果
try {
if (that.status === 'fulfilled') {
//如果onFulfilled不是函數,它會在then方法內部被替換成一個 Identity 函數
typeof onFulfilled !== 'function' ? onFulfilled = identity: false;
//將參數 this.value 傳入 onFulfilled 並執行,將結果賦給 res
res = onFulfilled.call(null, that.value);
} else if (that.status === 'rejected') {
//如果onRejected不是函數,它會在then方法內部被替換成一個 Thrower 函數
typeof onRejected !== 'function' ? onRejected = thrower: false;
res = onRejected.call(null, that.reason);
}
} catch(err) {
//拋出一個錯誤,情況3
pro.reason = err;
pro.status = 'rejected';
return;
}
if (res instanceof Promise) {
if (res.status === 'fulfilled') { //情況4
pro.value = res.value;
pro.status = 'fulfilled';
} else if (res.status === 'rejected') { //情況5
pro.reason = res.reason;
pro.status = 'rejected';
} else { //情況6
//res.status === 'pending'時,pro 跟隨 res
pro.status = 'pending';
res.then(function(value) {
pro.value = value;
pro.status = 'fulfilled';
},
function(reason) {
pro.reason = reason;
pro.status = 'rejected';
});
}
} else {
//回調函數返回一個值或不返回任何內容,情況1、2
pro.value = res;
pro.status = 'fulfilled';
}
}
return pro;
};
我想我已經註釋得很清楚了,可以對照我上一篇文章進行閱讀。
我再說明一下pending promise 的“跟隨”情況,和 all 方法的實現方式差不多,這裏也是用 res.then
來“跟隨”的。我相信大家都看得懂代碼,下面我舉個例子來實踐一下:
var fromCallback;
var fromThen = Promise.resolve('done')
.then(function onFulfilled(value) {
fromCallback = new Promise(function(resolve){
setTimeout(() => resolve(value), 0); //未執行 setTimeout 的回調方法之前 fromCallback 爲'pending'狀態
});
return fromCallback; //then 方法返回的 fromThen 將跟隨 onFulfilled 方法返回的 fromCallback
});
setTimeout(function() {
//目前已執行完 onFulfilled 回調函數,fromCallback 爲'pending'狀態,fromThen ‘跟隨’ fromCallback
console.log(fromCallback.status); //fromCallback.status === 'pending'
console.log(fromThen.status); //fromThen.status === 'pending'
setTimeout(function() {
//目前已執行完 setTimeout 中的回調函數,fromCallback 爲'fulfilled'狀態,fromThen 也跟着變爲'fulfilled'狀態
console.log(fromCallback.status + ' ' + fromCallback.value); //fromCallback.status === 'fulfilled'
console.log(fromThen.status + ' ' + fromThen.value); //fromThen.status === 'fulfilled'
console.log(fromCallback === fromThen); //false
}, 10); //將這個 delay 參數改爲 0 試試
}, 0);
看完這個例子,我相信大家都搞懂了then
的回調函數返回 pending promise 時它會怎麼處理了。
另外,這個例子也體現出我用 setTimeout 分發的macrotask
模擬microtask
的不足之處了,如果將倒數第二行的的 delay 參數改爲 0,那麼 fromThen.status === 'pending',這說明修改它狀態的代碼在 log 它狀態的代碼之後執行,至於原因大家自己想一下,這涉及到 event loop。
測試
大俠請點下面的鏈接進行測試:
https://codepen.io/lyl123321/...
或者點這裏查看源代碼:
https://github.com/lyl123321/...
結語
本菜雞終於做完這個 Promise 的 polyfill 了,各種改 bug 累個半死,還是老老實實滾去做畢設好了:)