Promise原理探究及实现
前言
作為ES6處理異步操作的新規(guī)范,Promise一經(jīng)出現(xiàn)就廣受歡迎。面試中也是如此,當(dāng)然此時(shí)對前端的要求就不僅僅局限會(huì)用這個(gè)階段了。下面就一起看下Promise相關(guān)的內(nèi)容。
Promise用法及實(shí)現(xiàn)
在開始之前,還是簡單回顧下Promise是什么以及怎么用,直接上來談實(shí)現(xiàn)有點(diǎn)空中花園的感覺。(下面示例參考自阮大佬es6 Promis,)
定義
Promise 是異步編程的一種解決方案,可以認(rèn)為是一個(gè)對象,可以從中獲取異步操作的信息。以替代傳統(tǒng)的回調(diào)事件。
常見用法
Promise的創(chuàng)建
es6規(guī)范中,Promise是個(gè)構(gòu)造函數(shù),所以創(chuàng)建如下:
const promise = new Promise((resolve, reject) => {
setTimeout(resolve, 200, 'resolve');
// 可以為同步,如下操作
return resolve('resolve')
})
注意resolve或者reject 一旦執(zhí)行,后續(xù)的代碼可以執(zhí)行但就不會(huì)再更新狀態(tài)(否則這狀態(tài)回調(diào)就無法控制了)。
舉個(gè)例子:
var a = new Promise((resolve,reject)=>{
resolve(1)
console.log('執(zhí)行代碼,改變狀態(tài)')
throw new Error('ss')
})
a.then((res)=>{
console.log('resolved >>>',res)
},(err)=>{
console.log('rejected>>>',err)
})
// 輸出
// 執(zhí)行代碼,改變狀態(tài)
// resolved >>> 1
因此,狀態(tài)更新函數(shù)之后的再次改變狀態(tài)的操作都是無效的,例如異常之類的也不會(huì)被catch。
邏輯代碼推薦在狀態(tài)更新之前執(zhí)行。
構(gòu)造函數(shù)
構(gòu)造函數(shù)接收一個(gè)函數(shù),該函數(shù)會(huì)同步執(zhí)行,即我們的邏輯處理函數(shù),何時(shí)執(zhí)行對應(yīng)的回調(diào),這部分邏輯還是要自己管理的。
至于如何執(zhí)行回調(diào),就和入?yún)⒂嘘P(guān)系了。
兩個(gè)入?yún)esolve和reject,分別更新不同狀態(tài),以觸發(fā)對應(yīng)處理函數(shù)。
觸發(fā)操作由Promise內(nèi)部實(shí)現(xiàn),我們只關(guān)注觸發(fā)時(shí)機(jī)即可
構(gòu)造函數(shù)實(shí)現(xiàn)
那么要實(shí)現(xiàn)一個(gè)Promise,其構(gòu)造函數(shù)應(yīng)該是這么個(gè)樣子:
// 三種狀態(tài)
const STATUS = {
PENDING: 'pending',
RESOLVED:'resolved',
REJECTED:'rejected'
}
class Promise{
constructor(fn){
// 初始化狀態(tài)
this.status = STATUS.PENDING
// resolve事件隊(duì)列
this.resolves = []
// reject事件隊(duì)列
this.rejects = []
// resolve和reject是內(nèi)部提供的,用以改變狀態(tài)。
const resovle = (val)=>{
// 顯然這里應(yīng)該是改變狀態(tài)觸發(fā)回調(diào)
this.triggerResolve(val)
}
const reject = (val)=>{
// 顯然這里應(yīng)該是改變狀態(tài)觸發(fā)回調(diào)
this.triggerReject(val)
}
// 執(zhí)行fn
try{
fn(resolve,reject)
}catch(err){
// 運(yùn)行異常要觸發(fā)reject,就需要在這里catch了
this.triggerReject(err)
}
}
then(){
}
}
觸發(fā)回調(diào)的triggerReject/triggerResolve 做的事情主要兩個(gè):
更新當(dāng)前狀態(tài)
執(zhí)行回調(diào)隊(duì)列中的事件
// 觸發(fā) reject回調(diào)
triggerReject(val){
// 保存當(dāng)前值,以供后面調(diào)用
this.value = val
// promise狀態(tài)一經(jīng)變化就不再更新,所以對于非pending狀態(tài),不再操作
if (this.status === STATUS.PENDING) {
// 更新狀態(tài)
this.status = STATUS.REJECTED
// 循環(huán)執(zhí)行回調(diào)隊(duì)列中事件
this.rejects.forEach((it) => {
it(val)
})
}
}
// resolve 功能類似
// 觸發(fā) resolve回調(diào)
triggerResolve(val) {
this.value = val
if(this.status === STATUS.PENDING){
this.status = STATUS.RESOLVED
this.resolves.forEach((it,i)=>{
it(val)
})
}
}
此時(shí)執(zhí)行的話還是不能達(dá)到目的的,因?yàn)閠his.resolves/ this.rejects的回調(diào)隊(duì)列里面還是空呢。
下面就看如何會(huì)用then往回調(diào)隊(duì)列中增加監(jiān)聽事件。
then用法
該方法為Promise實(shí)例上的方法,作用是為Promise實(shí)例增加狀態(tài)改變時(shí)的回調(diào)函數(shù)。
接受兩個(gè)參數(shù),resolve和reject即我們所謂成功和失敗回調(diào),其中reject可選
then方法返回的是一個(gè)新的實(shí)例(也就是新建了一個(gè)Promise實(shí)例),可實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用。
new Promise((resolve, reject) => {
return resolve(1)
}).then(function(res) {
// ...
}).then(function(res) {
// ...
});
前面的結(jié)果為后邊then的參數(shù),這樣可以實(shí)現(xiàn)次序調(diào)用。
若前面返回一個(gè)promise,則后面的then會(huì)依舊遵循promise的狀態(tài)變化機(jī)制進(jìn)行調(diào)用。
then 實(shí)現(xiàn)
看起來也簡單,then是往事件隊(duì)列中push事件。那么很容易得出下面的代碼:
// 兩個(gè)入?yún)⒑瘮?shù)
then(onResolved,onRejected){
const resolvehandle=(val)=>{
return onResolved(val)
},rejecthandle =(val)=>{
return onRejected(val)
}
// rejecthandle
this.resolves.push(resolvehandle)
this.rejects.push(rejecthandle)
}
此時(shí)執(zhí)行示例代碼,可以得到結(jié)果了。
new Promise((resolve, reject) => {
setTimeout(resolve, 200, 'done');
}).then((res)=>{
console.log(res)
}) // done
不過這里太簡陋了,而且then還有個(gè)特點(diǎn)是支持鏈?zhǔn)秸{(diào)用其實(shí)返回的也是promise 對象。
我們來改進(jìn)一下。
then支持鏈?zhǔn)秸{(diào)用
then(onResolved,onRejected){
// 返回promise 保證鏈?zhǔn)秸{(diào)用,注意這里每次then都新建了promise
return new Promise((resolve,reject)=>{
const resolvehandle = (val)=>{
// 對于值,回調(diào)方法存在就直接執(zhí)行,否則不變傳遞下去。
let res = onResolved ? onResolved(val) : val
if(Promise.isPromise(res)){
// 如果onResolved 是promise,那么就增加then
return res.then((val)=>{
resolve(val)
})
}else {
// 更新狀態(tài),執(zhí)行完了,后面的隨便
return resolve(val)
}
},
rejecthandle = (val)=>{
var res = onRejected ? onRejected(val) : val;
if (Promise.isPromise(res)) {
res.then(function (val) {
reject(val);
})
} else {
reject(val);
}
}
// 正常加入隊(duì)列
this.resolves.push(resolvehandle)
this.rejects.push(rejecthandle)
})
}
此時(shí)鏈?zhǔn)秸{(diào)用和promise 的回調(diào)也已經(jīng)支持了,可以用如下代碼測試。
new Promise((resolve, reject) => {
setTimeout(resolve, 200, 'done');
}).then((res)=>{
return new Promise((resolve)=>{
console.log(res)
setTimeout(resolve, 200, 'done2');
})
}).then((res)=>{
console.log('second then>>', res)
})
同步resolve的實(shí)現(xiàn)
不過此時(shí)對于同步的執(zhí)行,還是有些問題。
因?yàn)閠hen中的實(shí)現(xiàn),只是將回調(diào)事件假如回調(diào)隊(duì)列。
對于同步的狀態(tài),then執(zhí)行在構(gòu)造函數(shù)之后,
此時(shí)事件隊(duì)列為空,而狀態(tài)已經(jīng)為resolved,
所以這種狀態(tài)下需要加個(gè)判斷,如果非pending狀態(tài)直接執(zhí)行回調(diào)。
then(onResolved,onRejected){
/**省略**/
// 剛執(zhí)行then 狀態(tài)就更新,那么直接執(zhí)行回調(diào)
if(this.status === STATUS.RESOLVED){
return resolvehandle(this.value)
}
if (this.status === STATUS.REJECTED){
return rejecthandle(this.value)
}
})
}
這樣就能解決同步執(zhí)行的問題。
new Promise((resolve, reject) => {
resolve('done')
}).then((res)=>{
console.log(res)
})
// done
catch
catch方法是.then(null, rejection)或.then(undefined, rejection)的別名,用于指定發(fā)生錯(cuò)誤時(shí)的回調(diào)函數(shù)。
直接看例子比較簡單:
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 處理 getJSON 和 前一個(gè)回調(diào)函數(shù)運(yùn)行時(shí)發(fā)生的錯(cuò)誤
console.log('發(fā)生錯(cuò)誤!', error);
});
此時(shí)catch是是getJSON和第一個(gè)then運(yùn)行時(shí)的異常,如果只是在then中指定reject函數(shù),那么then中執(zhí)行的異常無法捕獲。
因?yàn)閠hen返回了一個(gè)新的promise,同級的reject回調(diào),不會(huì)被觸發(fā)。
舉個(gè)例子:
var a = new Promise((resolve,reject)=>{
resolve(1)
})
a.then((res)=>{
console.log(res)
throw new Error('then')
},(err)=>{
console.log('catch err>>>',err) // 不能catch
})
該catch只能捕獲構(gòu)造函數(shù)中的異常,對于then中的error就不能捕獲了。
var a = new Promise((resolve,reject)=>{
resolve(1)
})
a.then((res)=>{
console.log(res)
throw new Error('then')
}).catch((err)=>{
console.log('catch err>>>',err) // catch err>>> Error: then at <anonymous>:6:11
})
推薦每個(gè)then之后都跟catch來捕獲所有異常。
catch 的實(shí)現(xiàn)
基于catch方法是.then(null, rejection)或.then(undefined, rejection)的別名這句話,其實(shí)實(shí)現(xiàn)就比較簡單了。
其內(nèi)部實(shí)現(xiàn)調(diào)用then就可以了。
catch(onRejected){
return this.then(null, onRejected)
}
Promise.resolve/Promise.reject
該方法為獲取一個(gè)指定狀態(tài)的Promise對象的快捷操作。
直接看例子比較清晰:
Promise.resolve(1);
// 等價(jià)于
new Promise((resolve) => resolve(1));
Promise.reject(1);
// 等價(jià)于
new Promise((resolve,reject) => reject(1));
既然是Promise的自身屬性,那么可以用es6的static來實(shí)現(xiàn):
Promise.reject與其類似,就不再實(shí)現(xiàn)了。
// 轉(zhuǎn)為promise resolve 狀態(tài)
static resolve(obj){
if (Promise.isPromise(obj)) {
return obj;
}
// 非promise 轉(zhuǎn)為promise
return new Promise(function (resolve, reject) {
resolve(obj);
})
}
結(jié)束語
參考文章
阮一峰e(cuò)s6入門
https://promisesaplus.com/
http://liubin.org/promises-book/
本想把常見的promise面試題一起加上的,后面就寫成了promise的實(shí)現(xiàn),手動(dòng)Promise都可以實(shí)現(xiàn)的話,相關(guān)面試題應(yīng)該問題不大。這里附一個(gè)JavaScript | Promises interiew 大家可以看看。完整代碼請戳
總結(jié)
以上是生活随笔為你收集整理的Promise原理探究及实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中兴MiFavor UI 3.0图文刷机
- 下一篇: CUDA简要理解