Promise是前端面試中的高頻問題,我作為面試官的時候,問Promise的概率超過90%,據我所知,大多數公司,都會問一些關於Promise的問題。如果你能根據PromiseA+的規範,寫出符合規範的原始碼,那麼我想,對於面試中的Promise相關的問題,都能夠給出比較完美的答案。
我的建議是,對照規範多寫幾次實現,也許第一遍的時候,是改了多次,才能通過測試,那麼需要反覆的寫,我已經將Promise的原始碼實現寫了不下七遍。
Promise的原始碼實現
/**
* 1. new Promise時,需要傳遞一個 executor 執行器,執行器立刻執行
* 2. executor 接受兩個引數,分別是 resolve 和 reject
* 3. promise 只能從 pending 到 rejected, 或者從 pending 到 fulfilled
* 4. promise 的狀態一旦確認,就不會再改變
* 5. promise 都有 then 方法,then 接收兩個引數,分別是 promise 成功的回撥 onFulfilled,
* 和 promise 失敗的回撥 onRejected
* 6. 如果呼叫 then 時,promise已經成功,則執行 onFulfilled,並將promise的值作為引數傳遞進去。
* 如果promise已經失敗,那麼執行 onRejected, 並將 promise 失敗的原因作為引數傳遞進去。
* 如果promise的狀態是pending,需要將onFulfilled和onRejected函式存放起來,等待狀態確定後,再依次將對應的函式執行(釋出訂閱)
* 7. then 的引數 onFulfilled 和 onRejected 可以預設
* 8. promise 可以then多次,promise 的then 方法返回一個 promise
* 9. 如果 then 返回的是一個結果,那麼就會把這個結果作為引數,傳遞給下一個then的成功的回撥(onFulfilled)
* 10. 如果 then 中丟擲了異常,那麼就會把這個異常作為引數,傳遞給下一個then的失敗的回撥(onRejected)
* 11.如果 then 返回的是一個promise,那麼需要等這個promise,那麼會等這個promise執行完,promise如果成功,
* 就走下一個then的成功,如果失敗,就走下一個then的失敗
*/
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
function Promise(executor) {
let self = this;
self.status = PENDING;
self.onFulfilled = [];//成功的回撥
self.onRejected = []; //失敗的回撥
//PromiseA+ 2.1
function resolve(value) {
if (self.status === PENDING) {
self.status = FULFILLED;
self.value = value;
self.onFulfilled.forEach(fn => fn());//PromiseA+ 2.2.6.1
}
}
function reject(reason) {
if (self.status === PENDING) {
self.status = REJECTED;
self.reason = reason;
self.onRejected.forEach(fn => fn());//PromiseA+ 2.2.6.2
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
Promise.prototype.then = function (onFulfilled, onRejected) {
//PromiseA+ 2.2.1 / PromiseA+ 2.2.5 / PromiseA+ 2.2.7.3 / PromiseA+ 2.2.7.4
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
let self = this;
//PromiseA+ 2.2.7
let promise2 = new Promise((resolve, reject) => {
if (self.status === FULFILLED) {
//PromiseA+ 2.2.2
//PromiseA+ 2.2.4 --- setTimeout
setTimeout(() => {
try {
//PromiseA+ 2.2.7.1
let x = onFulfilled(self.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
//PromiseA+ 2.2.7.2
reject(e);
}
});
} else if (self.status === REJECTED) {
//PromiseA+ 2.2.3
setTimeout(() => {
try {
let x = onRejected(self.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
} else if (self.status === PENDING) {
self.onFulfilled.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(self.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
self.onRejected.push(() => {
setTimeout(() => {
try {
let x = onRejected(self.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
});
return promise2;
}
function resolvePromise(promise2, x, resolve, reject) {
let self = this;
//PromiseA+ 2.3.1
if (promise2 === x) {
reject(new TypeError('Chaining cycle'));
}
if (x && typeof x === 'object' || typeof x === 'function') {
let used; //PromiseA+2.3.3.3.3 只能呼叫一次
try {
let then = x.then;
if (typeof then === 'function') {
//PromiseA+2.3.3
then.call(x, (y) => {
//PromiseA+2.3.3.1
if (used) return;
used = true;
resolvePromise(promise2, y, resolve, reject);
}, (r) => {
//PromiseA+2.3.3.2
if (used) return;
used = true;
reject(r);
});
}else{
//PromiseA+2.3.3.4
if (used) return;
used = true;
resolve(x);
}
} catch (e) {
//PromiseA+ 2.3.3.2
if (used) return;
used = true;
reject(e);
}
} else {
//PromiseA+ 2.3.3.4
resolve(x);
}
}
module.exports = Promise;
複製程式碼
有專門的測試指令碼可以測試所編寫的程式碼是否符合PromiseA+的規範。
首先,在promise實現的程式碼中,增加以下程式碼:
Promise.defer = Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
複製程式碼
安裝測試指令碼:
npm install -g promises-aplus-tests
複製程式碼
如果當前的promise原始碼的檔名為promise.js
那麼在對應的目錄執行以下命令:
promises-aplus-tests promise.js
複製程式碼
promises-aplus-tests中共有872條測試用例。以上程式碼,可以完美通過所有用例。
對上面的程式碼實現做一點簡要說明(其它一些內容註釋中已經寫得很清楚):
-
onFulfilled 和 onFulfilled的呼叫需要放在setTimeout,因為規範中表示: onFulfilled or onRejected must not be called until the execution context stack contains only platform code。使用setTimeout只是模擬非同步,原生Promise並非是這樣實現的。
-
在 resolvePromise 的函式中,為何需要usedd這個flag,同樣是因為規範中明確表示: If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored. 因此我們需要這樣的flag來確保只會執行一次。
-
self.onFulfilled 和 self.onRejected 中儲存了成功的回撥和失敗的回撥,根據規範2.6顯示,當promise從pending態改變的時候,需要按照順序去指定then對應的回撥。
PromiseA+的規範(翻譯版)
PS: 下面是我翻譯的規範,供參考
術語
- promise 是一個有then方法的物件或者是函式,行為遵循本規範
- thenable 是一個有then方法的物件或者是函式
- value 是promise狀態成功時的值,包括 undefined/thenable或者是 promise
- exception 是一個使用throw丟擲的異常值
- reason 是promise狀態失敗時的值
要求
2.1 Promise States
Promise 必須處於以下三個狀態之一: pending, fulfilled 或者是 rejected
2.1.1 如果promise在pending狀態
2.1.1.1 可以變成 fulfilled 或者是 rejected
複製程式碼
2.1.2 如果promise在fulfilled狀態
2.1.2.1 不會變成其它狀態
2.1.2.2 必須有一個value值
複製程式碼
2.1.3 如果promise在rejected狀態
2.1.3.1 不會變成其它狀態
2.1.3.2 必須有一個promise被reject的reason
複製程式碼
概括即是:promise的狀態只能從pending變成fulfilled,或者從pending變成rejected.promise成功,有成功的value.promise失敗的話,有失敗的原因
2.2 then方法
promise必須提供一個then方法,來訪問最終的結果
promise的then方法接收兩個引數
promise.then(onFulfilled, onRejected)
複製程式碼
2.2.1 onFulfilled 和 onRejected 都是可選引數
2.2.1.1 onFulfilled 必須是函式型別
2.2.1.2 onRejected 必須是函式型別
複製程式碼
2.2.2 如果 onFulfilled 是函式:
2.2.2.1 必須在promise變成 fulfilled 時,呼叫 onFulfilled,引數是promise的value
2.2.2.2 在promise的狀態不是 fulfilled 之前,不能呼叫
2.2.2.3 onFulfilled 只能被呼叫一次
複製程式碼
2.2.3 如果 onRejected 是函式:
2.2.3.1 必須在promise變成 rejected 時,呼叫 onRejected,引數是promise的reason
2.2.3.2 在promise的狀態不是 rejected 之前,不能呼叫
2.2.3.3 onRejected 只能被呼叫一次
複製程式碼
2.2.4 onFulfilled 和 onRejected 應該是微任務
2.2.5 onFulfilled 和 onRejected 必須作為函式被呼叫
2.2.6 then方法可能被多次呼叫
2.2.6.1 如果promise變成了 fulfilled態,所有的onFulfilled回撥都需要按照then的順序執行
2.2.6.2 如果promise變成了 rejected態,所有的onRejected回撥都需要按照then的順序執行
複製程式碼
2.2.7 then必須返回一個promise
promise2 = promise1.then(onFulfilled, onRejected);
複製程式碼
2.2.7.1 onFulfilled 或 onRejected 執行的結果為x,呼叫 resolvePromise
2.2.7.2 如果 onFulfilled 或者 onRejected 執行時丟擲異常e,promise2需要被reject
2.2.7.3 如果 onFulfilled 不是一個函式,promise2 以promise1的值fulfilled
2.2.7.4 如果 onRejected 不是一個函式,promise2 以promise1的reason rejected
複製程式碼
2.3 resolvePromise
resolvePromise(promise2, x, resolve, reject)
2.3.1 如果 promise2 和 x 相等,那麼 reject promise with a TypeError
2.3.2 如果 x 是一個 promsie
2.3.2.1 如果x是pending態,那麼promise必須要在pending,直到 x 變成 fulfilled or rejected.
2.3.2.2 如果 x 被 fulfilled, fulfill promise with the same value.
2.3.2.3 如果 x 被 rejected, reject promise with the same reason.
複製程式碼
2.3.3 如果 x 是一個 object 或者 是一個 function
2.3.3.1 let then = x.then.
2.3.3.2 如果 x.then 這步出錯,那麼 reject promise with e as the reason..
2.3.3.3 如果 then 是一個函式,then.call(x, resolvePromiseFn, rejectPromise)
2.3.3.3.1 resolvePromiseFn 的 入參是 y, 執行 resolvePromise(promise2, y, resolve, reject);
2.3.3.3.2 rejectPromise 的 入參是 r, reject promise with r.
2.3.3.3.3 如果 resolvePromise 和 rejectPromise 都呼叫了,那麼第一個呼叫優先,後面的呼叫忽略。
2.3.3.3.4 如果呼叫then丟擲異常e
2.3.3.3.4.1 如果 resolvePromise 或 rejectPromise 已經被呼叫,那麼忽略
2.3.3.3.4.3 否則,reject promise with e as the reason
2.3.3.4 如果 then 不是一個function. fulfill promise with x.
複製程式碼
2.3.4 如果 x 不是一個 object 或者 function,fulfill promise with x.
Promise的其他方法
雖然上述的promise原始碼已經符合PromiseA+的規範,但是原生的Promise還提供了一些其他方法,如:
- Promise.resolve()
- Promise.reject()
- Promise.prototype.catch()
- Promise.prototype.finally()
- Promise.all()
- Promise.race()
下面具體說一下每個方法的實現:
Promise.resolve
Promise.resolve(value) 返回一個以給定值解析後的Promise 物件.
- 如果 value 是個 thenable 物件,返回的promise會“跟隨”這個thenable的物件,採用它的最終狀態
- 如果傳入的value本身就是promise物件,那麼Promise.resolve將不做任何修改、原封不動地返回這個promise物件。
- 其他情況,直接返回以該值為成功狀態的promise物件。
Promise.resolve = function (param) {
if (param instanceof Promise) {
return param;
}
return new Promise((resolve, reject) => {
if (param && param.then && typeof param.then === 'function') {
setTimeout(() => {
param.then(resolve, reject);
});
} else {
resolve(param);
}
});
}
複製程式碼
thenable物件的執行加 setTimeout的原因是根據原生Promise物件執行的結果推斷的,如下的測試程式碼,原生的執行結果為: 20 400 30;為了同樣的執行順序,增加了setTimeout延時。
測試程式碼:
let p = Promise.resolve(20);
p.then((data) => {
console.log(data);
});
let p2 = Promise.resolve({
then: function(resolve, reject) {
resolve(30);
}
});
p2.then((data)=> {
console.log(data)
});
let p3 = Promise.resolve(new Promise((resolve, reject) => {
resolve(400)
}));
p3.then((data) => {
console.log(data)
});
複製程式碼
Promise.reject
Promise.reject方法和Promise.resolve不同,Promise.reject()方法的引數,會原封不動地作為reject的理由,變成後續方法的引數。
Promise.reject = function (reason) {
return new Promise((resolve, reject) => {
reject(reason);
});
}
複製程式碼
Promise.prototype.catch
Promise.prototype.catch 用於指定出錯時的回撥,是特殊的then方法,catch之後,可以繼續 .then
Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
}
複製程式碼
Promise.prototype.finally
不管成功還是失敗,都會走到finally中,並且finally之後,還可以繼續then。並且會將值原封不動的傳遞給後面的then.
Promise.prototype.finally = function (callback) {
return this.then((value) => {
return Promise.resolve(callback()).then(() => {
return value;
});
}, (err) => {
return Promise.resolve(callback()).then(() => {
throw err;
});
});
}
複製程式碼
Promise.all
Promise.all(promises) 返回一個promise物件
- 如果傳入的引數是一個空的可迭代物件,那麼此promise物件回撥完成(resolve),只有此情況,是同步執行的,其它都是非同步返回的。
- 如果傳入的引數不包含任何 promise,則返回一個非同步完成.
- promises 中所有的promise都promise都“完成”時或引數中不包含 promise 時回撥完成。
- 如果引數中有一個promise失敗,那麼Promise.all返回的promise物件失敗
- 在任何情況下,Promise.all 返回的 promise 的完成狀態的結果都是一個陣列
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
let index = 0;
let result = [];
if (promises.length === 0) {
resolve(result);
} else {
function processValue(i, data) {
result[i] = data;
if (++index === promises.length) {
resolve(result);
}
}
for (let i = 0; i < promises.length; i++) {
//promises[i] 可能是普通值
Promise.resolve(promises[i]).then((data) => {
processValue(i, data);
}, (err) => {
reject(err);
return;
});
}
}
});
}
複製程式碼
測試程式碼:
var promise1 = new Promise((resolve, reject) => {
resolve(3);
})
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then(function(values) {
console.log(values); //[3, 42, 'foo']
},(err)=>{
console.log(err)
});
var p = Promise.all([]); // will be immediately resolved
var p2 = Promise.all([1337, "hi"]); // non-promise values will be ignored, but the evaluation will be done asynchronously
console.log(p);
console.log(p2)
setTimeout(function(){
console.log('the stack is now empty');
console.log(p2);
});
複製程式碼
Promise.race
Promise.race函式返回一個 Promise,它將與第一個傳遞的 promise 相同的完成方式被完成。它可以是完成( resolves),也可以是失敗(rejects),這要取決於第一個完成的方式是兩個中的哪個。
如果傳的引數陣列是空,則返回的 promise 將永遠等待。
如果迭代包含一個或多個非承諾值和/或已解決/拒絕的承諾,則 Promise.race 將解析為迭代中找到的第一個值。
Promise.race = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
return;
} else {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then((data) => {
resolve(data);
return;
}, (err) => {
reject(err);
return;
});
}
}
});
}
複製程式碼
測試程式碼:
Promise.race([
new Promise((resolve, reject) => { setTimeout(() => { resolve(100) }, 1000) }),
undefined,
new Promise((resolve, reject) => { setTimeout(() => { reject(100) }, 100) })
]).then((data) => {
console.log('success ', data);
}, (err) => {
console.log('err ',err);
});
Promise.race([
new Promise((resolve, reject) => { setTimeout(() => { resolve(100) }, 1000) }),
new Promise((resolve, reject) => { setTimeout(() => { resolve(200) }, 200) }),
new Promise((resolve, reject) => { setTimeout(() => { reject(100) }, 100) })
]).then((data) => {
console.log(data);
}, (err) => {
console.log(err);
});
複製程式碼
歡迎關注小姐姐的微信公眾號:前端宇宙。