準備工作
- Promises/A+規範:promisesaplus.com (中文翻譯可參考這篇部落格)
- 完整測試用例:github.com/promises-ap…
開始
首先,拋開測試用例,實現一個我們印象中的Promise
-
Promise是一個建構函式,其入參接收一個函式 (fn),
-
Promise執行會呼叫fn, 並將resolve和reject作為入參傳遞給fn (resolve和reject均是函式)
-
Promise執行後,返回其例項promise
-
promise有三種狀態:pending、fullfilled、rejected
-
promise存在then方法,then方法可以註冊回撥函式onFullfilled、onRejeted,then方法返回Promise例項 (nextPromise) ,供鏈式呼叫
- then方法內部:當promise為非pending狀態時,執行回撥。 (
回撥的執行結果會對nextPromise產生影響,所以每次註冊的回撥和對應的nextPromise需要一起儲存
)
- then方法內部:當promise為非pending狀態時,執行回撥。 (
-
promise預設pending狀態
- 執行resolve置為fullfilled,儲存resolve入參 (data),觸發onFullfilled
- 執行reject置為rejected,儲存reject入參 (data),觸發onRejeted
-
執行callback時 (onFullfilled、onRejeted) ,傳入儲存的data
- callback正常返回:觸發nextPromise的resolve,傳入返回值
- callback執行捕獲到錯誤:觸發nextPromise的reject,傳入捕獲到的錯誤資訊
-
若callback的返回值是Promise例項,保證nextPromise的狀態與返回的promise的狀態同步
function execCallback(promise) {
const defers = promise.defers;
while (defers.length > 0) {
const defer = defers.shift();
const nextPromise = defer.promise;
const callback = promise.state === 'fullfilled' ? defer.onFullfilled : defer.onRejected;
let cbRes;
try {
cbRes = callback(promise.data);
} catch (err) {
reject(nextPromise, err);
continue;
}
if (cbRes instanceof Promise) {
cbRes.then(data => { // 當cbRes也是Promise時,保證nextPromise的狀態與cbRes一致
resolve(nextPromise, data);
}, err => {
reject(nextPromise, err);
});
} else {
resolve(nextPromise, cbRes);
}
}
}
function resolve(promise, data) {
promise.data = data;
promise.state = 'fullfilled';
execCallback(promise);
}
function reject(promise, err) {
promise.data = err;
promise.state = 'rejected';
execCallback(promise);
}
function Promise(fn) {
this.state = 'pending'; // pending|fullfilled|rejected
this.data = undefined;
this.defers = []; // 儲存 callback 和 nextPromise
const promise = this;
fn(data => {
resolve(promise, data);
}, err => {
reject(promise, err);
});
return this;
};
Promise.prototype.then = function(onFullfilled, onRejected) {
const nextPromise = new Promise(function() {});
let defer = {
promise: nextPromise,
onFullfilled,
onRejected
}; // 回撥的執行會對nextPromise產生影響,故一起儲存
this.defers.push(defer)
if (this.state !== 'pending') {
execCallback(this); // 非pending狀態,觸發callback
}
return nextPromise;
}
module.exports = Promise;
複製程式碼
用幾個常用的promise demo驗證了下上述程式碼,沒發現問題
跑測試用例,結果是:
171 passing (2m)
672 failing
1) 2.2.1: Both `onFulfilled` and `onRejected` are optional arguments. 2.2.1.2: If `onRejected` is not a function, it must be ignored. applied to a promise fulfilled and then chained off of `onFulfilled` is `undefined`:
Error: timeout of 200ms exceeded. Ensure the done() callback is being called in this test.
......
複製程式碼
分析log,按順序先實現規範2.2.1:處理callback不為函式的情況
2.2.1 Both onFulfilled and onRejected are optional arguments:
2.2.1.1. If onFulfilled is not a function, it must be ignored.
2.2.1.2. If onRejected is not a function, it must be ignored.
意思是若onFulfilled、onRejected不是函式,則忽略。
考慮如下case:
new Promise((resolve, reject) => {
resolve(123);
})
.then(null, null)
.then(data => {
console.log('data: ', data)
}, err => {
console.log('error: ', err)
})
複製程式碼
程式碼修改如下:
(^_^本文寫到後面發現這兒有處錯誤: 第8行不應該使用return,應該使用continue,但是測試用例還是過了。¶¶¶)實現規範2.2.2、2.2.3、2.2.4中未通過的部分:非同步執行callback
2.2.2. If onFulfilled is a function:
2.2.2.2. it must not be called before promise is fulfilled.
2.2.3. If onRejected is a function,
2.2.3.2. it must not be called before promise is rejected.
2.2.4. onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
這幾條規範總結起來就是: callback需要非同步執行
所以為execCallback的方法體加上setTimeout就能解決問題
實現規範2.1.2與2.1.3:狀態轉換的限制
2.1.2. When fulfilled, a promise:
2.1.2.1. must not transition to any other state.
2.1.2.2. must have a value, which must not change.
2.1.3. When rejected, a promise:
2.1.3.1. must not transition to any other state.
2.1.3.2. must have a reason, which must not change.
狀態只能從pending轉掉fullfilled或者rejected, 且只能轉換一次
改動如下:
檢視diff至此,規範1、2.1、2.2的要求全部滿足
理解規範2.3:callback返回promise & resolve傳入promise的處理
promise的callback函式是可以返回promise的,
對於返回promise的情況,then方法返回的newPromise的狀態和攜帶的資料,要求與callback返回的promise一致,看demo:
const promise1 = new Promise((resolve, reject) => {
resolve(123);
});
const promise2 = new Promise((resolve, reject) => {
resolve(456);
});
new Promise((resolve, reject) => {
resolve(promise1);
}).then(data => {
console.log(data) // expect "123"
return promise2;
}).then(data => {
console.log(data); // expect "456"
})
複製程式碼
規範2.3:
- 保障了promise狀態和資料的鏈式傳遞,實現了非同步程式碼的繼發呼叫,避免callback hell
- 規定了只要物件是
thenable
,就可以當成promise處理
關於thenable
, 簡單來講,就是包含then方法的物件或函式:
new Promise((resolve, reject) => {
resolve('ok');
}).then(data => {
return { // 此處返回的就是thenable
then(resolve, reject) {
resolve('aaa');
}
};
}).then(data => {
console.log(data) // log: aaa
})
複製程式碼
為什麼會有thenable
這個定義?
主要是為了使不同的promise實現可以協同使用
例如,張三實現了個Promise1, 李四實現了個庫Promise2,那麼可以這麼使用
new Promise1(resolve => {
resolve(new Promise2())
})
複製程式碼
具體到實現程式碼,不可通過跑一遍測試用例判斷是否Promise, 所以規定通過thenable判斷是否可以協同使用
實現規範 2.3
前文的測試log,報錯指向 2.3.1
| 2.3.1. promise and x refer to the same object, reject promise with a TypeError as the reason.
case如下
let promise = new Promise((resolve, reject) => {
resolve(123);
});
let nextPromise = promise.then(data => {
return nextPromise; // 這種情況需要throw TypeError
})
複製程式碼
程式碼改動:
檢視diff2.3.3 & 2.3.4
其實就是圍繞thanable
的一堆處理邏輯,先大致按照文件寫一下
注意:
- execCallback和resolve函式都要處理thenable
- promise也可以當成thenable處理
這裡先貼一下用於處理thenable的函式:
/**
* 處理thenanble, 如果data不是thenable,返回false
* @param promise: thanable執行後會對該promis產生影響
* @param data: callback的返回,或者resolve的傳入值
*/
function doResolveThenable (promise, data) {
if (data && /object|function/.test(typeof data)) {
let then;
try {
then = data.then;
if (typeof then !== 'function') return false; // 非thenanble
then.call(data, data => {
resolve(promise, data)
}, err => {
reject(promise, err)
})
} catch(err) {
reject(promise, err)
}
} else {
return false; // 非thenanble
}
return true;
}
複製程式碼
其它修改
檢視diff再看log,只剩下60個失敗的case了
剩下的case如下:
thenable的then方法裡,如果先後執行了onFullfilled、onRejected、throw,應當以第一次執行的為準,忽略後續的執行
let promise = new Promise((resolve, reject) => {
resolve('ok');
}).then(data => {
return {
then(onFullfilled, onRejected) {
onFullfilled(111); // 只允許這一句執行
onFullfilled(222);
onRejected(333);
onRejected(444);
throw (666);
}
}
})
複製程式碼
程式碼修改:
檢視diff終於,測試用例全過了,大功告成~~~