「一次寫過癮」手寫Promise全家桶+Generator+async/await
手寫 Promise 全家桶
Promise/A+ 規(guī)范[4]鎮(zhèn)樓!
如果你沒讀過 Promise/A+ 規(guī)范也沒關(guān)系,我?guī)湍憧偨Y(jié)了如下三部分重點(diǎn):
不過建議看完本文后還是要親自去讀一讀,不多 bb,開始展示。

規(guī)范重點(diǎn)
1.Promise 狀態(tài)
Promise 的三個狀態(tài)分別是 pending、fulfilled 和 rejected。
pending: 待定,Promise 的初始狀態(tài)。在此狀態(tài)下可以落定 (settled)為fulfilled或rejected狀態(tài)。fulfilled: 兌現(xiàn)(解決),表示執(zhí)行成功。Promise 被 resolve 后的狀態(tài),狀態(tài)不可再改變,且有一個私有的值 value。rejected: 拒絕,表示執(zhí)行失敗。Promise 被 reject 后的狀態(tài),狀態(tài)不可再改變,且有一個私有的原因 reason。
注意:value 和 reason 也是不可變的,它們包含原始值或?qū)ο蟮牟豢尚薷牡囊?,默認(rèn)值為 undefined。
2.Then 方法
要求必須提供一個 then 方法來訪問當(dāng)前或最終的 value 或 reason。
promise.then(onFulfilled, onRejected)
1.then 方法接受兩個函數(shù)作為參數(shù),且參數(shù)可選。2.如果可選參數(shù)不為函數(shù)時會被忽略。3.兩個函數(shù)都是異步執(zhí)行,會放入事件隊(duì)列等待下一輪 tick。4.當(dāng)調(diào)用 onFulfilled 函數(shù)時,會將當(dāng)前 Promise 的 value 值作為參數(shù)傳入。5.當(dāng)調(diào)用 onRejected 函數(shù)時,會將當(dāng)前 Promise 的 reason 失敗原因作為參數(shù)傳入。6.then 函數(shù)的返回值為 Promise。7.then 可以被同一個 Promise 多次調(diào)用。
3.Promise 解決過程
Promise 的解決過程是一個抽象操作,接收一個 Promise 和一個值 x。
針對 x 的不同值處理以下幾種情況:
1.x 等于 Promise
拋出 TypeError 錯誤,拒絕 Promise。
2.x 是 Promise 的實(shí)例
如果 x 處于待定狀態(tài),那么 Promise 繼續(xù)等待直到 x 兌現(xiàn)或拒絕,否則根據(jù) x 的狀態(tài)兌現(xiàn)/拒絕 Promise。
3.x 是對象或函數(shù)
取出 x.then 并調(diào)用,調(diào)用時將 this 指向 x。將 then 回調(diào)函數(shù)中得到的結(jié)果 y 傳入新的 Promise 解決過程中,遞歸調(diào)用。
如果執(zhí)行報(bào)錯,則將以對應(yīng)的失敗原因拒絕 Promise。
這種情況就是處理擁有 then() 函數(shù)的對象或函數(shù),我們也叫它 thenable。
4.如果 x 不是對象或函數(shù)
以 x 作為值執(zhí)行 Promise。
手寫 Promise
1.首先定義 Promise 的三個狀態(tài)
var PENDING = 'pending';
var FULFILLED = 'fulfilled';
var REJECTED = 'rejected';
2.我們再來搞定 Promise 的構(gòu)造函數(shù)
創(chuàng)建 Promise 時需要傳入 execute 回調(diào)函數(shù),接收兩個參數(shù),這兩個參數(shù)分別用來兌現(xiàn)和拒絕當(dāng)前 Promise。
所以我們需要定義 resolve() 和 reject() 函數(shù)。
初始狀態(tài)為 PENDING,在執(zhí)行時可能會有返回值 value,在拒絕時會有拒絕原因 reason。
同時需要注意,Promise 內(nèi)部的異常不能直接拋出,需要進(jìn)行異常捕獲。
function Promise(execute) {
var that = this;
that.state = PENDING;
function resolve(value) {
if (that.state === PENDING) {
that.state = FULFILLED;
that.value = value;
}
}
function reject(reason) {
if (that.state === PENDING) {
that.state = REJECTED;
that.reason = reason;
}
}
try {
execute(resolve, reject);
} catch (e) {
reject(e);
}
}
3. 實(shí)現(xiàn) then() 方法
then 方法用來注冊當(dāng)前 Promise 狀態(tài)落定后的回調(diào),每個 Promise 實(shí)例都需要有它,顯然要寫到 Promise 的原型 prototype 上,并且 then() 函數(shù)接收兩個回調(diào)函數(shù)作為參數(shù),分別是 onFulfilled 和 onRejected。
Promise.prototype.then = function(onFulfilled, onRejected) {}
根據(jù)上面第 2 條規(guī)則,如果可選參數(shù)不為函數(shù)時應(yīng)該被忽略,我們要對參數(shù)進(jìn)行如下判斷。
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(x) { return x; }
onRejected = typeof onRejected === 'function' ? onRejected : function(e) { throw e; }
根據(jù)第 3 條規(guī)則,需要使用 setTimeout 延遲執(zhí)行,模擬異步。
根據(jù)第 4 條、第 5 條規(guī)則,需要根據(jù) Promise 的狀態(tài)來執(zhí)行對應(yīng)的回調(diào)函數(shù)。
在 PENDING 狀態(tài)時,需要等到狀態(tài)落定才能調(diào)用。我們可以將 onFulfilled 和 onRejected 函數(shù)存到 Promise 的屬性 onFulfilledFn 和 onRejectedFn 中,
當(dāng)狀態(tài)改變時分別調(diào)用它們。
var that = this;
var promise;
if (that.state === FULFILLED) {
setTimeout(function() {
onFulfilled(that.value);
});
}
if (that.state === REJECTED) {
setTimeout(function() {
onRejected(that.reason);
});
}
if (that.state === PENDING) {
that.onFulfilledFn = function() {
onFulfilled(that.value);
}
that.onRejectedFn = function() {
onRejected(that.reason);
}
}
根據(jù)第 6 條規(guī)則,then 函數(shù)的返回值為 Promise,我們分別給每個邏輯添加并返回一個 Promise。
同時,then 支持鏈?zhǔn)秸{(diào)用,我們需要將 onFulfilledFn 和 onRejectedFn 改成數(shù)組。
var that = this;
var promise;
if (that.state === FULFILLED) {
promise = new Promise(function(resolve, reject) {
setTimeout(function() {
try {
onFulfilled(that.value);
} catch (reason) {
reject(reason);
}
});
});
}
if (that.state === REJECTED) {
promise = new Promise(function(resolve, reject) {
setTimeout(function() {
try {
onRejected(that.reason);
} catch (reason) {
reject(reason);
}
});
});
}
if (that.state === PENDING) {
promise = new Promise(function(resolve, reject) {
that.onFulfilledFn.push(function() {
try {
onFulfilled(that.value);
} catch (reason) {
reject(reason);
}
})
that.onRejectedFn.push(function() {
try {
onRejected(that.reason);
} catch (reason) {
reject(reason);
}
});
});
}
與上面相對應(yīng)的,再將 Promise 的構(gòu)造函數(shù)相應(yīng)的進(jìn)行改造。
1.添加 onFulFilledFn 和 onRejectedFn 數(shù)組。2.resolve() 和 reject() 函數(shù)改變狀態(tài)時,需要異步調(diào)用數(shù)組中的函數(shù),同樣使用 setTimeout 來模擬異步。
function Promise(execute) {
var that = this;
that.state = PENDING;
that.onFulfilledFn = [];
that.onRejectedFn = [];
function resolve(value) {
setTimeout(function() {
if (that.state === PENDING) {
that.state = FULFILLED;
that.value = value;
that.onFulfilledFn.forEach(function(fn) {
fn(that.value);
})
}
})
}
function reject(reason) {
setTimeout(function() {
if (that.state === PENDING) {
that.state = REJECTED;
that.reason = reason;
that.onRejectedFn.forEach(function(fn) {
fn(that.reason);
})
}
})
}
try {
execute(resolve, reject);
} catch (e) {
reject(e);
}
}
4.Promise 解決過程 resolvePromise()
Promise 解決過程分為以下幾種情況,我們需要分別進(jìn)行處理:
1.x 等于 Promise TypeError 錯誤
此時相當(dāng)于 Promise.then 之后 return 了自己,因?yàn)?then 會等待 return 后的 Promise,導(dǎo)致自己等待自己,一直處于等待。。
function resolvePromise(promise, x) {
if (promise === x) {
return reject(new TypeError('x 不能等于 promise'));
}
}
2.x 是 Promise 的實(shí)例
如果 x 處于待定狀態(tài),Promise 會繼續(xù)等待直到 x 兌現(xiàn)或拒絕,否則根據(jù) x 的狀態(tài)兌現(xiàn)/拒絕 Promise。
我們需要調(diào)用 Promise 在構(gòu)造時的函數(shù) resolve() 和 reject() 來改變 Promise 的狀態(tài)。
function resolvePromise(promise, x, resolve, reject) {
// ...
if (x instanceof Promise) {
if (x.state === FULFILLED) {
resolve(x.value);
} else if (x.state === REJECTED) {
reject(x.reason);
} else {
x.then(function(y) {
resolvePromise(promise, y, resolve, reject);
}, reject);
}
}
}
3.x 是對象或函數(shù)
取出 x.then 并調(diào)用,調(diào)用時將 this 指向 x,將 then 回調(diào)函數(shù)中得到的結(jié)果 y 傳入新的 Promise 解決過程中,遞歸調(diào)用。
如果執(zhí)行報(bào)錯,則將以對應(yīng)的失敗原因拒絕 Promise。
x 可能是一個 thenable 而非真正的 Promise。
需要設(shè)置一個變量 executed 避免重復(fù)調(diào)用。
function resolvePromise(promise, x, resolve, reject) {
// ...
if ((x !== null) && ((typeof x === 'object' || (typeof x === 'function'))) {
var executed;
try {
var then = x.then;
if (typeof then === 'function') {
then.call(x, function(y) {
if (executed) return;
executed = true;
return resolvePromise(promise, y, resolve, reject);
}, function (e) {
if (executed) return;
executed = true;
reject(e);
})
} else {
resolve(x);
}
} catch (e) {
if (executed) return;
executed = true;
reject(e);
}
}
}
4.直接將 x 作為值執(zhí)行
function resolvePromise(promise, x, resolve, reject) {
// ...
resolve(x)
}
測試
// 為了支持測試,將模塊導(dǎo)出
module.exports = {
deferred() {
var resolve;
var reject;
var promise = new Promise(function (res, rej) {
resolve = res;
reject = rej;
})
return {
promise,
resolve,
reject
}
}
}
我們可以選用這款測試工具對我們寫的 Promise 進(jìn)行測試 Promise/A+ 測試工具: promises-aplus-tests[5]。
目前支持 827 個測試用例,我們只需要在導(dǎo)出模塊的時候遵循 CommonJS 規(guī)范,按照要求導(dǎo)出對應(yīng)的函數(shù)即可。

Promise.resolve
Promise.resolve() 可以實(shí)例化一個解決(fulfilled) 的 Promise。
Promise.resolve = function(value) {
if (value instanceof Promise) {
return value;
}
return new Promise(function(resolve, reject) {
resolve(value);
});
}
Promise.reject
Promise.reject() 可以實(shí)例化一個 rejected 的 Promise 并拋出一個異步錯誤(這個錯誤不能通過try/catch捕獲,只能通過拒絕處理程序捕獲)
Promise.reject = function(reason) {
return new Promise(function(resolve, reject) {
reject(reason);
});
}
Promise.prototype.catch
Promise.prototype.catch() 方法用于給 Promise 添加拒絕時的回調(diào)函數(shù)。
Promise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
}
Promise.prototype.finally
Promise.prototype.finally() 方法用于給 Promise 添加一個不管最終狀態(tài)如何都會執(zhí)行的操作。
Promise.prototype.finally = function(fn) {
return this.then(function(value) {
return Promise.resolve(value).then(function() {
return value;
});
}, function(error) {
return Promise.resolve(reason).then(function() {
throw error;
});
});
}
Promise.all
Promise.all() 方法會將多個 Promise 實(shí)例組合成一個新的 Promise 實(shí)例。
組合后的 Promise 實(shí)例只有當(dāng)每個包含的 Promise 實(shí)例都解決(fulfilled)后才解決(fulfilled),如果有一個包含的 Promise 實(shí)例拒絕(rejected)了,則合成的 Promise 也會拒絕(rejected)。
兩個注意點(diǎn):
傳入的是可迭代對象,用 for...of 遍歷 Iterable 更安全 傳入的每個實(shí)例不一定是 Promise,需要用 Promise.resolve() 包裝
Promise.all = function(promiseArr) {
return new Promise(function(resolve, reject) {
const length = promiseArr.length;
const result = [];
let count = 0;
if (length === 0) {
return resolve(result);
}
for (let item of promiseArr) {
Promise.resolve(item).then(function(data) {
result[count++] = data;
if (count === length) {
resolve(result);
}
}, function(reason) {
reject(reason);
});
}
});
}
Promise.race
Promise.race() 同樣返回一個合成的 Promise 實(shí)例,其會返回這一組中最先解決(fulfilled)或拒絕(rejected)的 Promise 實(shí)例的返回值。
Promise.race = function(promiseArr) {
return new Promise(function(resolve, reject) {
const length = promiseArr.length;
if (length === 0) {
return resolve();
}
for (let item of promiseArr) {
Promise.resolve(item).then(function(value) {
return resolve(value);
}, function(reason) {
return reject(reason);
});
}
});
}
Promise.any
Promise.any() 相當(dāng)于 Promise.all() 的反向操作,同樣返回一個合成的 Promise 實(shí)例,只要其中包含的任何一個 Promise 實(shí)例解決(fulfilled)了,合成的 Promise 就解決(fulfilled)。
只有當(dāng)每個包含的 Promise 都拒絕(rejected)了,合成的 Promise 才拒絕(rejected)。

Promise.any = function(promiseArr) {
return new Promise(function(resolve, reject) {
const length = promiseArr.length;
const result = [];
let count = 0;
if (length === 0) {
return resolve(result);
}
for (let item of promiseArr) {
Promise.resolve(item).then((value) => {
return resolve(value);
}, (reason) => {
result[count++] = reason;
if (count === length) {
reject(result);
}
});
}
});
}
Promise.allSettled
Promise.allSettled() 方法也是返回一個合成的 Promise,不過只有等到所有包含的每個 Promise 實(shí)例都返回結(jié)果落定時,不管是解決(fulfilled)還是拒絕(rejected),合成的 Promise 才會結(jié)束。一旦結(jié)束,狀態(tài)總是 fulfilled。
其返回的是一個對象數(shù)組,每個對象表示對應(yīng)的 Promise 結(jié)果。
對于每個結(jié)果對象,都有一個 status 字符串。如果它的值為 fulfilled,則結(jié)果對象上存在一個 value 。如果值為 rejected,則存在一個 reason 。
Promise.allSettled = function(promiseArr) {
return new Promise(function(resolve) {
const length = promiseArr.length;
const result = [];
let count = 0;
if (length === 0) {
return resolve(result);
} else {
for (let item of promiseArr) {
Promise.resolve(item).then((value) => {
result[count++] = { status: 'fulfilled', value: value };
if (count === length) {
return resolve(result);
}
}, (reason) => {
result[count++] = { status: 'rejected', reason: reason };
if (count === length) {
return resolve(result);
}
});
}
}
});
}
// 使用 Promise.finally 實(shí)現(xiàn)
Promise.allSettled = function(promises) {
// 也可以使用擴(kuò)展運(yùn)算符將 Iterator 轉(zhuǎn)換成數(shù)組
// const promiseArr = [...promises]
const promiseArr = Array.from(promises)
return new Promise(resolve => {
const result = []
const len = promiseArr.length;
let count = len;
if (len === 0) {
return resolve(result);
}
for (let i = 0; i < len; i++) {
promiseArr[i].then((value) => {
result[i] = { status: 'fulfilled', value: value };
}, (reason) => {
result[i] = { status: 'rejected', reason: reason };
}).finally(() => {
if (!--count) {
resolve(result);
}
});
}
});
}
// 使用 Promise.all 實(shí)現(xiàn)
Promise.allSettled = function(promises) {
// 也可以使用擴(kuò)展運(yùn)算符將 Iterator 轉(zhuǎn)換成數(shù)組
// const promiseArr = [...promises]
const promiseArr = Array.from(promises)
return Promise.all(promiseArr.map(p => Promise.resolve(p).then(res => {
return { status: 'fulfilled', value: res }
}, error => {
return { status: 'rejected', reason: error }
})));
};
完整代碼倉庫[6]
手寫 Generator 函數(shù)
先來簡單回顧下 Generator 的使用:
function* webCanteenGenerator() {
yield '店小二兒,給我切兩斤牛肉來';
yield '再來十八碗酒';
return '好酒!這酒有力氣!';
}
var canteen = webCanteenGenerator();
canteen.next();
canteen.next();
canteen.next();
canteen.next();
// {value: "店小二兒,給我切兩斤牛肉來", done: false}
// {value: "再來十八碗酒", done: false}
// {value: "好酒!這酒有力氣!", done: true}
// {value: undefined, done: true}

// 簡易版
// 定義生成器函數(shù),入?yún)⑹侨我饧?/span>
function webCanteenGenerator(list) {
var index = 0;
var len = list.length;
return {
// 定義 next 方法
// 記錄每次遍歷位置,實(shí)現(xiàn)閉包,借助自由變量做迭代過程中的“游標(biāo)”
next: function() {
var done = index >= len; // 如果索引還沒有超出集合長度,done 為 false
var value = !done ? list[index++] : undefined; // 如果 done 為 false,則可以繼續(xù)取值
// 返回遍歷是否完畢的狀態(tài)和當(dāng)前值
return {
done: done,
value: value
}
}
}
}
var canteen = webCanteenGenerator(['道路千萬條', '安全第一條', '行車不規(guī)范']);
canteen.next();
canteen.next();
canteen.next();
// {done: false, value: "道路千萬條"}
// {done: false, value: "安全第一條"}
// {done: false, value: "行車不規(guī)范"}
// {done: true, value: undefined}
完整代碼地址[7]

手寫 async/await
Generator 缺陷:
1.函數(shù)外部無法捕獲異常 2.多個 yield 會導(dǎo)致調(diào)試?yán)щy
async 函數(shù)對 Generator 函數(shù)改進(jìn)如下:
1.內(nèi)置執(zhí)行器 2.更好的語義 3.更廣的適用性 4.返回值是 Promise
async/await 做的事情就是將 Generator 函數(shù)轉(zhuǎn)換成 Promise,說白了,async 函數(shù)就是 Generator 函數(shù)的語法糖,await 命令就是內(nèi)部 then 命令的語法糖。
const fetchData = (data) => new Promise((resolve) => setTimeout(resolve, 1000, data + 1))
const fetchResult = async function () {
var result1 = await fetchData(1);
var result2 = await fetchData(result1);
var result3 = await fetchData(result2);
console.log(result3);
}
fetchResult();
可以嘗試通過 Babel[8] 官網(wǎng)轉(zhuǎn)換一下上述代碼,可以看到其核心就是 _asyncToGenerator 方法。
我們下面來實(shí)現(xiàn)它。
function asyncToGenerator(generatorFn) {
// 將 Generator 函數(shù)包裝成了一個新的匿名函數(shù),調(diào)用這個匿名函數(shù)時返回一個 Promise
return function() {
// 生成迭代器,相當(dāng)于執(zhí)行 Generator 函數(shù)
// 如上面三碗不過崗例子中的 var canteen = webCanteenGenerator()
var gen = generatorFn.apply(this, arguments);
return new Promise(function(resolve, reject) {
// 利用 Generator 分割代碼片段,每一個 yield 用 Promise 包裹起來
// 遞歸調(diào)用 Generator 函數(shù)對應(yīng)的迭代器,當(dāng)?shù)鲌?zhí)行完成時執(zhí)行當(dāng)前的 Promise,失敗時則拒絕 Promise
function step(key, arg) {
try {
var info = gen[key](arg "key");
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
// 遞歸終止條件,完成了就 resolve
resolve(value);
} else {
return Promise.resolve(value).then(function(value) {
step('next', value);
}, function(err) {
step('throw', err);
});
}
}
return step('next');
});
}
}
完整代碼地址[9]
好了,本文到這里就告一段落,如果上述代碼你發(fā)現(xiàn)有問題的地方,可以在評論區(qū)留言,一起探討學(xué)習(xí)。
也歡迎來前端食堂年終總結(jié)的評論區(qū)蓋樓,聽說食堂的小伙伴們都來了。
你遠(yuǎn)道而來這世間,想必也是因?yàn)闊釔郯?| 掘金年度征文[10]
一些參考資源
前端高手進(jìn)階(朱德龍) JavaScript 設(shè)計(jì)模式核心原理與應(yīng)用實(shí)踐(修言) ES6 系列之 Babel 將 Async 編譯成了什么樣子[11] 通俗淺顯的理解Promise中的then[12] 手寫async await的最簡實(shí)現(xiàn)(20行)[13]
參考資料
github.com/Geekhyt: https://github.com/Geekhyt/front-end-canteen
[2]這些手寫代碼會了嗎?少年: https://juejin.cn/post/6856419501777846279#comment
[3]你遠(yuǎn)道而來這世間,想必也是因?yàn)闊釔郯?| 掘金年度征文: https://juejin.cn/post/6905737176776966152#comment
[4]Promise/A+ 規(guī)范: https://github.com/promises-aplus/promises-spec
[5]Promise/A+ 測試工具: promises-aplus-tests: https://github.com/promises-aplus/promises-tests
[6]完整代碼倉庫: https://github.com/Geekhyt/fe-interview/issues/1
[7]完整代碼地址: https://github.com/Geekhyt/fe-interview/issues/2
[8]Babel: https://babeljs.io/repl
[9]完整代碼地址: https://github.com/Geekhyt/fe-interview/issues/2
[10]你遠(yuǎn)道而來這世間,想必也是因?yàn)闊釔郯?| 掘金年度征文: https://juejin.cn/post/6905737176776966152#comment
[11]ES6 系列之 Babel 將 Async 編譯成了什么樣子: https://juejin.cn/post/6844903702457925640
[12]通俗淺顯的理解Promise中的then: https://segmentfault.com/a/1190000010420744
[13]手寫async await的最簡實(shí)現(xiàn)(20行): https://juejin.cn/post/6844904102053281806
[14]github.com/Geekhyt: https://github.com/Geekhyt/front-end-canteen

