本文假設你有必定的Promise基礎知識,不涉及api的講解,可是對你深刻理解Promise有必定益處。git
在公司頂過幾天面試官,一道手寫Promise就卡主了很多人(受困於這道題的人別打我。。。我是不會告訴你我就任的公司的),其實這道題的主要目的是考察對Promise的理解,順便的纔是考察js邏輯,寫出來是加分項,能表達出你對Promise的理解纔是最重要的。github
可是現實狀況是有挺多人直接白卷,把加分項變成了減分項。寫不寫是態度問題,寫出幾個點已經足以讓面試官高看你一眼。下面我就把面試官但願看到幾個點拆解出來,以面試題的方式去理解Promise,但願對大家有所幫助。面試
不想看長篇大論的能夠直接查看總結:總結api
var p1 = new Promise(function(resolve, reject) {
throw Error('sync error')
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
複製代碼
2.請寫出下列代碼的輸出promise
var p1 = new Promise(function(resolve, reject) {
setTimeout(() => {
throw Error('async error')
})
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
複製代碼
var p1 = new Promise(function(resolve, reject) {
resolve()
})
.then(res => {
throw Error('sync error')
})
複製代碼
錯誤三連,你知道正確答案嗎😏?瀏覽器
正確答案是:bash
這裏考查的主要是Promise的錯誤捕獲,其實仔細想一想js中能用的錯誤捕獲也只能是try catch了,而try catch只能捕獲同步錯誤,而且在沒有傳入錯誤監聽的時候會將捕獲到的錯誤拋出。app
因此在手寫promise中,你至少要寫出try catch包裹回調代調異步
function Promise(fn) {
...
doResolve(fn, this)
}
function doResolve(fn, self) {
try {
fn(function(value) {
...
},
function(reason) {
...
})
} catch(err) {
reject(self, err)
}
}
Promise.prototype.then = function(onFulfilled, onRejected) {
try {
...
onFulfilled(value)
} catch(err) {
reject(err)
}
};
function reject(self, newValue) {
...
if (!self._handled) {
Promise._unhandledRejectionFn(self._value);
}
}
複製代碼
把上面的面試題改寫一下:async
var p1 = new Promise(function(resolve, reject) {
resolve(1)
throw Error('sync error')
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
複製代碼
var p1 = new Promise(function(resolve, reject) {
reject(2)
resolve(1)
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
複製代碼
var p1 = new Promise(function(resolve, reject) {
resolve(1)
})
.then(res => {
throw Error('sync error')
console.log(res)
})
.catch(err => {
console.log(err)
})
複製代碼
正確答案是:
Promise是一個有狀態的容器,當狀態被凝固了,後面的resolve或reject就不會被觸發。簡單的說就是同一個Promise只能觸發一個狀態監聽(onFulfilled或onRejected)。因此在手寫Promise中須要有一個狀態標記:
function Promise(fn) {
...
this._state = 0 // 狀態標記
doResolve(fn, this)
}
function doResolve(fn, self) {
var done = false // 保證只執行一個監聽
try {
fn(function(value) {
if (done) return
done = true
resolve(self, value)
},
function(reason) {
if (done) return;
done = true
reject(self, value)
})
} catch(err) {
if (done) return
done = true
reject(self, err)
}
}
function resolve(self, newValue) {
try {
self._state = 1;
...
}
catch(err) {
reject(self, err)
}
}
function reject(self, newValue) {
self._state = 2;
...
if (!self._handled) {
Promise._unhandledRejectionFn(self._value);
}
}
複製代碼
var p1 = new Promise(function(resolve, reject) {
resolve()
setTimeout(() => {
console.log(1)
})
console.log(2)
})
.then(res => {
console.log(3)
})
console.log(4)
複製代碼
正確答案是:
依次輸出:
2
4
3
1
複製代碼
或
2
4
1
3
複製代碼
首先 promise 中then、catch、finally中的回調都是異步執行的,因此前面輸出2 4
的同步代碼是沒有疑問的。
那爲何兩種答案都認爲是對的呢,實際上是由於polyfill的鍋。正確的Promise輸出應該是 2 4 3 1,緣由在於Promise.then是微任務執行的,微任務優先於宏任務執行(setTimeout就是宏任務)。
可是在polyfill中,瀏覽器環境是無法主動註冊微任務的,因此一樣是使用setTimeout調用then中的fn,一樣是宏任務的狀況下就只是隊列的先進先出原則了,那麼在promise-polyfill環境中輸出 2 4 1 3也認爲是正確的。
那麼手寫Promise中,應該將resolve,reject回調設爲異步:
function handle(self, deferred) {
...
setTimeout(function() {
var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
if (cb === null) {
(self._state === 1 ? resolve : reject)(deferred.promise, self._value);
return;
}
var ret;
try {
ret = cb(self._value);
} catch (e) {
reject(deferred.promise, e);
return;
}
resolve(deferred.promise, ret);
}, 0)
...
}
複製代碼
var p1 = new Promise(function(resolve, reject) {
reject(1)
})
.catch(err => {
console.log(err)
return 2
})
setTimeout(() => {
p1
.then(res => console.log(res))
}, 1000)
複製代碼
正確答案是:
先輸出 1
1秒後輸出 2
Promise會將最後的值存儲起來,若是在下次使用promise方法的時候回直接返回該值的promise。
因此手寫一個Promise,你應該保存返回值:
function Promise(fn) {
...
this._state = 0 // 狀態標記
this._value = undefined; // 存儲返回值
doResolve(fn, this)
}
function resolve(self, newValue) {
try {
...
if (newValue instanceof Promise) {
self._state = 3;
self._value = newValue;
finale(self);
return;
} else if (typeof then === 'function') {
doResolve(bind(then, newValue), self);
return;
}
self._state = 1;
self._value = newValue;
...
}
catch(err) {
reject(self, err)
}
}
function reject(self, newValue) {
self._state = 2;
self._value = newValue;
...
if (!self._handled) {
Promise._unhandledRejectionFn(self._value);
}
}
複製代碼
var p1 = new Promise(function(resolve, reject) {
reject(1)
})
.then(
res => {
console.log(res)
return 2
},
err => {
console.log(err)
return 3
}
)
.catch(err => {
console.log(err)
return 4
})
.finally(res => {
console.log(res)
return 5
})
.then(
res => console.log(res),
err => console.log(err)
)
複製代碼
正確答案是:
依次輸出:
1
undefined
3
複製代碼
Promise可以鏈式調用的緣由是它的每個方法都返回新的promise,哪怕是finally方法,特殊的是finlly會返回上一個promise的值包裝成的新promise,而且finally也不接收參數,由於不管Promise是reject仍是fulfill它都會被調用。
因此你須要在promise方法中返回新的promise:
function bind(fn, thisArg) {
return function() {
fn.apply(thisArg, arguments);
};
}
function resolve(self, newValue) {
...
try {
if (newValue instanceof Promise) {
self._state = 3;
self._value = newValue;
finale(self);
return;
} else if (typeof then === 'function') {
doResolve(bind(then, newValue), self);
return;
}
self._state = 1;
...
} catch (e) {
reject(self, e);
}
}
複製代碼
上述總共表達了五個Promise知識點:
文中案例皆取自 promise-polyfill,有美玉在前,做者就不亮出本身的板磚了,同時也提醒各位面試者多看優秀做品的源碼,何須看那些不太正規的第三方的實現。
畢竟公司的目標不是造重複的輪子,若是你已經能清晰明瞭地表述出上述部分知識,咱們就能相信你已是一個可以正確並靈活使用Promise的開發者了,及格分雙手奉上(以咱們公司的招聘目標爲例,相信大部分公司要求也是如此)。
最後:
立刻快到2019年了,祝你們都能找到趁心如意的工做!🎉🎉🎉
-- The End