網上不少手寫Promise。根據本身的理解,也寫了一份,曬出來但願能被你們指正。也給本身一個梳理的過程。初學者要辯證的看這個文檔,你須要對原生Promise很熟悉。html
代碼其實很是簡單,不到100行代碼,主要是充分了解原生Promise都能作什麼,有什麼特徵。在根據這些功能特徵列出模擬Promise的需求,問題就解決了一大半了。這裏會一步步的列出原生Promise的功能特徵。再一步步的添加代碼。每一步代碼都是在上一步的代碼基礎上添加或修改得來的,在代碼中會標識出哪裏作了修改和增長,這樣就不用翻來翻去看上一步的代碼了,保證思路連貫性面試
說明一下,這裏研究原生的Promise只看表象,不深刻分析。文中代碼的運行和測試都是在chrome【版本 81.0.4044.129(正式版本) (64 位)】中進行chrome
文中代碼雖然已經都測試過了,可是不能保證在複製粘貼過程當中沒錯。因此最好理解以後本身敲一遍promise
還有更重要的是:這裏用setTimeout來模擬微任務,會和原生的Promise在程序中執行順序有所不一樣。這點必定要了解。 關於事件循環寫了 一個簡述文章,有興趣的能夠看一下:簡述JavaScript事件循環EventLoopbash
正文看起來有點長,其實都是重複的代碼佔的位置,內容很簡單異步
首先是模擬Promise須要實現的最基本的需求函數
promise這個容器,用來存儲異步或同步執行的結果。oop
還要有狀態,來反映同步或者異步處理的階段。有三種狀態post
在Promise對象外部不能直接訪問到Promise的狀態和值測試
還須要有個執行同步或異步的函數(執行函數),以Promise參數的形式,在Promise構造函數中執行
上代碼:
/*'↓↓↓定義了MyPromise函數,參數executor(執行函數),數據類型是Funciotn,用來執行同步或異步操做↓↓↓'*/
/*'↓↓↓之因此用函數不用class,是爲了方便定義對象私有變量和私有方法↓↓↓'*/
function MyPromise(executor){
/*'↓↓↓定義了三個狀態PENDING、FULFILLED、REJECTED↓↓↓'*/
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
/*'↓↓↓value變量存儲異步結果(Promise的值),pending狀態下是undefined,fulfilled狀態下是執行結果,rejected狀態下是錯誤緣由↓↓↓'*/
let value;
/*'↓↓↓state變量用來存儲Promise狀態,初始狀態是pending↓↓↓'*/
let state = PENDING;
/*'↓↓↓都定義好了,再運行執行函數↓↓↓'*/
executor()
}
複製代碼
最基本的部分完成
上面定義了容器中的狀態和須要存儲值的變量,並且運行了用戶本身定義的執行方法,可是當執行函數有告終果,怎麼改變容器的狀態和存儲結果呢?這就須要定義操做狀態和值方法來處理。需求以下
繼續完善MyPromise ↓↓↓↓↓
function MyPromise(executor){
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
let value;
let state = PENDING;
/*'↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓這裏是新加的改變狀態和值的方法↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓'*/
function change(newState, newValue){ //'用來修改狀態和相應的值的方法'
if (state === PENDING) {//'限制了只能在狀態pending下改變,這樣就保證狀態和值只能改一次'
value = newState;
state = newValue;
}
}
let resolve = change.bind(this, FULFILLED);// '定義了resolve函數,只能把狀態改爲fulfilled'
let reject = change.bind(this, REJECTED);// '定義了reject函數,只能把狀態改爲fulfilled'
//'resolve和reject都是change的偏函數,其實綁定this沒啥意義,就是寫着方便,修改方便,只關注change就好了,也便於閱讀代碼'
/*'↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑這裏是新加的改變狀態和值的方法↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑'*/
executor(resolve, reject)// ←←←'經過executor參數,傳遞到Promise外部使用'
}
複製代碼
能存儲狀態和值了,也能修改狀態和值了,可是還須要在相應狀態下處理值的方法的呢,因此就須要註冊回調函數了。
Promise對象中有兩個方法then和catch註冊回調函數
let p = new Promise((resolve, reject) => {})
p.then(value => {})
p.then(value => {})
p.catch(value => {})
/*'↑↑↑↑↑↑上面的代碼說的是分別註冊不少回調↑↑↑↑↑↑↑'*/
/*'各個回調是獨立的,返回的新Promise也不是同一個,這至關於Promise狀態傳遞出現了分支,這個後面再展開'*/
/*'↓↓↓↓↓↓下面是鏈式調用,這裏說的不是這種狀況↓↓↓↓↓↓↓↓↓↓↓↓↓'*/
p.then(value => {}).then(value => {}).then(value => {}).then(value => {})
複製代碼
根據上面的需求先吧這兩個用來註冊回調的方法加上
function MyPromise(executor){
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
let value;
let state = PENDING;
function change(newState, newValue){
if (state === PENDING) {
value = newState;
state = newValue;
}
}
let resolve = change.bind(this, FULFILLED);
let reject = change.bind(this, REJECTED);
/*'↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓這裏是註冊回調的方法↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓'*/
var onQueue = []; //'→→→→→爲了能註冊多個回調,定義了onQueue,存儲註冊的回調,等待狀態改變後調用'
function register(onFulfilled, onRejected) {//'→→→→→用來註冊回調的方法'
let nextPromise = new MyPromise((nextResolve, nextReject) => {
/*'↓↓↓↓↓↓↓↓↓向onQueue中添加註冊的方法,用這種對象結構保存是爲了以後調用方便↓↓↓↓↓↓↓↓↓'*/
/*'↓↓↓↓↓↓↓↓↓至於爲何在Promise裏寫,後面會的內容會詳細提到↓↓↓↓↓↓↓↓↓'*/
onQueue.push({
[FULFILLED]: { on: onFulfilled},
[REJECTED]: { on: onRejected},
});
})
return nextPromise;
}
this.then = register.bind(this); //'→→→→→定義了Promise對象的then方法,註冊處理返回值的回調方法'
this.catch = register.bind(this, undefined);//'→→→→→定義了Promise對象的catch方法,是then的語法糖'
/*'↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑這裏是註冊回調的方法↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑'*/
executor(resolve, reject)
}
複製代碼
當狀態不是pending的時候,就須要執行註冊的回調,那麼就須要有一個處理回調的機制。這裏添加一個run方法處理這個功能
在加功能以前仍是定需求,看看原生Promise都幹了些什麼
首先須要說明一下:這裏說的執行註冊的回調函數,並非說直接執行,要遵循js的event loop事件循環機制。原生的Promise是把回調放入微任務等待,等這次宏任務執行完畢,再執行當前微任務
這裏咱們用setTimeout這個宏任務來模擬微任務
再看看何時開始處理回調:
不要着急,還有別的,看看鏈式調用都發生了什麼,上例子:
new Promise((resolve, reject) => {
// resolve('p ok')
reject('p err')
}).then(value => {
console.log("成功1 "+value)
return "p1"
}, error => {
console.log("失敗1 "+error)//←←←←←←輸出這裏
return "p1 err"
}).catch(err => {
console.log("失敗2 "+error)
return "p2 err"
}).then(value => {
console.log("成功3 "+value)//←←←←←←輸出這裏
return new Promise((resolve, reject) => {
reject("新的Promise失敗了")
})
}, error => {
console.log("失敗3 "+error)
return "p3 err"
}).then(value => {
console.log("成功4 "+value)
}, error => {
console.log("失敗4 "+error)//←←←←←←輸出這裏
})
//輸出:
//失敗1 p err
//test.html:56 成功3 p1 err
//test.html:66 失敗4 新的Promise失敗了
複製代碼
整了一個好長的鏈,簡單說明一下狀況,爲了說的清楚,用一個圖來解釋一下:
根據上述原生Promise特徵,繼續增長MyPromise功能,加一個run方法用來實現處理註冊的回調函數的機制
function MyPromise(executor){
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
let value;
let state = PENDING;
function change(newState, newValue){
if (state === PENDING) {
value = newState;
state = newValue;
run()//'→→→→→這裏執行run方法。在狀態改變的時候嘗試處理一下回調函數 } } let resolve = change.bind(this, FULFILLED); let reject = change.bind(this, REJECTED); /*'↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓這裏處理註冊的回調函數↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓'*/ function run(){ if (state === PENDING) { return; } while (onQueue.length) { let onObj = onQueue.shift();//'←←←從註冊的回調中拿出一份,放入下面模擬的微任務中' setTimeout(() => { //'←←←用setTimeout模擬微任務,把回調放入,等待執行' if (onObj[state].on) { //'←←←判斷當前狀態下,是否註冊了回調函數' let returnvalue = onObj[state].on(value);//'←←←有就運行回調函數,獲得返回值' if (returnvalue instanceof MyPromise) { //'←←←判斷返回值是否是MyPromise類型' /*'↓↓↓返回值是MyPromise類型,用這個MyPromise對象的then方法,能獲得返回MyPromise對象的狀態和值
↓↓↓再利用nextPromise的resolve和reject方法做爲參數獲得狀態和值,這樣就實現了繼承狀態和值'*/ returnvalue.then(onObj[FULFILLED].next, onObj[REJECTED].next); } else { //'↓↓↓返回值不是MyPromise類型,直接改變nextPromise狀態爲fulfilled,值爲回調函數的返回值' onObj[FULFILLED].next(returnvalue); } } else { /*'↓↓↓當前狀態沒有註冊回調函數,
↓↓↓則利用保存的nextPromise對象的resolve或reject,改變nextPromise對象的狀態
↓↓↓這就至關於傳遞了狀態和值'*/ onObj[state].next(value); } }, 0); } } /*'↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑這裏處理註冊的回調函數↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑'*/ var onQueue = []; function register(onFulfilled, onRejected) { let nextPromise = new MyPromise((nextResolve, nextReject) => { onQueue.push({ /*"↓↓↓這裏添加了next屬性,爲何呢 ↓↓↓上面的需求中提到了,狀態和值會有向下傳遞的狀況,還會有繼承回調函數返回Promise對象狀態和值的狀況 ↓↓↓要想改變下一個Promise對象(nextPromise)狀態,只能經過它的resolve和reject方法 ↓↓↓因此這裏就把這倆方法提保存起來以便傳遞和繼承狀態使用↓↓↓↓"*/ [FULFILLED]: { on: onFulfilled, next: nextResolve}, [REJECTED]: { on: onRejected, next: nextReject},//'←←←之因此分開存,就是由於在方便向下傳遞狀態' }); }) run() //'→→→→→這裏執行run方法。用來處理註冊的回調函數,run方法裏有判斷,爲pending狀態不處理回調函數' return nextPromise; } this.then = register.bind(this); this.catch = register.bind(this, undefined); executor(resolve, reject) } 複製代碼
仍是先上幾個例子,看看原生Promise特色,總結一下需求。
先來一個小實驗
new Promise(() => {
xxx;// 用一個未定義的變量拋錯
})// 控制檯輸出了錯誤
console.log('我是後面的代碼')// '可是也輸出了這句,沒有被阻塞,說明沒有拋出到Promise外面'
複製代碼
上面的例子錯誤輸出了,後面的代碼也執行了。錯誤只是簡單的輸出,說明沒有拋出到Promise外面。 可是若是Promise的參數是否是function類型,會發生什麼
new Promise("hahaha") //Uncaught TypeError: Promise resolver hahaha is not a function
console.log('我是後面的代碼')//'這裏沒有輸出輸出了'
複製代碼
上面例子錯誤輸出了,後面的代碼沒有輸出。說明錯誤拋出來了,因此:
再來個例子,看看Promise是怎麼處理各個部分報錯的。
new Promise((resolve, reject) => {
xxx;
// resolve("ok")
// reject('no')
// xxx;
}).then(value => {
//xxx;
console.log(value)//輸出位置0
},err => {
//xxx;
console.log("err1",err)//輸出位置1
}).catch(err => {
console.log("err2",err)//輸出位置2
})
當Prosmise的執行函數有錯,輸出位置1輸出:err1 ReferenceError: xxx is not defined
當then第一個參數有錯誤,輸出位置2輸出:err1 ReferenceError: xxx is not defined
當then第二個參數有錯誤,輸出位置2輸出:err2 ReferenceError: xxx is not defined
這個例子把then的第二個參數去掉
當Prosmise的執行函數和then第一個參數有錯,都在輸出位置2輸出:err2 ReferenceError: xxx is not defined
複製代碼
你們能夠再變換一些方式測試錯誤處理的方式。根據以上例子能夠總結
function MyPromise(executor){
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
let value;
let state = PENDING;
function change(newState, newValue){
if (state === PENDING) {
value = newState;
state = newValue;
run()
}
}
let resolve = change.bind(this, FULFILLED);
let reject = change.bind(this, REJECTED);
function run(){
if (state === PENDING) {
return;
}
while (onQueue.length) {
let onObj = onQueue.shift();
setTimeout(() => {
if (onObj[state].on) {
try{//'在加了回調函數運行時抓取錯誤'
let returnvalue = onObj[state].on(value);
if (returnvalue instanceof MyPromise) {
returnvalue.then(onObj[FULFILLED].next, onObj[REJECTED].next);
} else {
onObj[FULFILLED].next(returnvalue);
}
}catch(error){
onObj[REJECTED].next(error);//'←←←若是回調函數報錯則,以rejected的狀態向下傳遞'
}
} else {
onObj[state].next(value);
}
}, 0);
}
}
var onQueue = [];
function register(onFulfilled, onRejected) {
let nextPromise = new MyPromise((nextResolve, nextReject) => {
onQueue.push({
[FULFILLED]: { on: onFulfilled, next: nextResolve},
[REJECTED]: { on: onRejected, next: nextReject},
});
})
run()
return nextPromise;
}
this.then = register.bind(this);
this.catch = register.bind(this, undefined);
//'↓↓↓加了判斷,若是executor不是函數類型,就向外拋錯'
if (!(executor instanceof Function)) {
throw new TypeError(executor + " 不是個函數。親!MyPromise參數得是個函數的呢");
}
//'↓↓↓這裏又加了try爲執行函數運行時抓取錯誤'
//'↓↓↓可是有個問題就是若是executor不是函數,光加個try就不會向外拋出錯誤了,因此在這前邊再加個判斷'
try {
executor(resolve, reject);
} catch (error) {
/*'↓↓↓若是執行函數報錯,改變自身狀態爲rejected。 ↓↓↓若是在狀態改變以後報錯,也會執行這裏,可是前面已經限制了狀態只能改變一次。 ↓↓↓在這裏調用reject方法就沒有用了。達到了狀態改變以後報錯不處理的效果。'*/
reject(error);
}
}
複製代碼
寫到這裏promise最基本的功能就實現了。可是有那麼一點點特徵尚未,是錦上添花的功能,是啥呢。上例子
new Promise((resolve, reject) => {
// xxx;
resolve("ok")
// reject('no')
})
//resolve時,控制檯沒輸出
//reject時,控制檯輸出:Uncaught (in promise) no。意思就是錯誤沒有處理
//在狀態改變以前的錯誤,報錯輸出在控制檯,可是也只是輸出而已不是拋出錯誤。由於不會阻塞代碼
複製代碼
上面例子說明報錯和rejected狀態沒有被處理,雖然不會拋出,也不會影響程序。可是會在控制檯有紅字提示。因此最後把這個提示功能加上,下面的代碼調整了一下順秩序,看着更順眼點,再添加提示的功能
function MyPromise(executor){
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
let value;
let state = PENDING;
let tipTask;//'爲了能移除提示任務用的'
function change(newState, newValue){
if (state === PENDING) {
value = newState;
state = newValue;
if (!onQueue.length && state === REJECTED) {
/* '當狀態爲rejected,並且沒有註冊回調函數,則在任務中放入一個提示任務。 若是在MyPromise運行的這個宏任務中註冊了回調,則在run中提示任務被移除 直到最後,確定會有沒有註冊回調函數的MyPromise對象。這個對象就會執行這個提示任務了 例如一個鏈式調用p.then().then()....then()不可能無窮的,總會有最後一個。 這最後一個返回的promise就不會被註冊回調。因此這裏添加的提示任務就會被執行了 ' */
tipTask = setTimeout(() => {//'爲了能移除這個任務。把變量放在MyPromise函數做用域下'
console.error("在MyPromise裏,須要註冊一個處理錯誤的回調 \n" + (value || ''));
}, 0);
}
run()
}
}
function run(){
if (state === PENDING) {
return;
}
while (onQueue.length) {
clearTimeout(tipTask);//'若是註冊了回調。這裏把change方法里加的提示任務移除掉'
let onObj = onQueue.shift();
setTimeout(() => {
if (onObj[state].on) {
try{
let returnvalue = onObj[state].on(value);
if (returnvalue instanceof MyPromise) {
returnvalue.then(onObj[FULFILLED].next, onObj[REJECTED].next);
} else {
onObj[FULFILLED].next(returnvalue);
}
}catch(error){
onObj[REJECTED].next(error);
}
} else {
onObj[state].next(value);
}
}, 0);
}
}
var onQueue = [];
function register(onFulfilled, onRejected) {
let nextPromise = new MyPromise((nextResolve, nextReject) => {
onQueue.push({
[FULFILLED]: { on: onFulfilled, next: nextResolve},
[REJECTED]: { on: onRejected, next: nextReject},
});
})
run()
return nextPromise;
}
let resolve = change.bind(this, FULFILLED);
let reject = change.bind(this, REJECTED);
this.then = register.bind(this);
this.catch = register.bind(this, undefined);
if (!(executor instanceof Function)) {
throw new TypeError(executor + " 不是個函數。親!MyPromise參數得是個函數的呢");
}
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
複製代碼
好了這就獲得一個相對完美的模擬Promise的手寫代碼了。
其實Uncaught (in promise) no或者內部報錯沒處理在控制檯輸出,實際上在chrome中是在全部當前存在的宏任務隊列任務(不是全部,是當前,也就是在運行這個Promise的宏任務運行時,所存在的宏任務)都執行完畢以後再輸出(我的懷疑這個提示用的是否是setTimeout0呀哈哈哈)。
這裏用setTimeout模擬的微任務,在js事件循環(event loop)循序上會和原生Promise有區別。
本文但願能給你們幫助,也但願能獲得指點。文中使用的詞語都是相對口語化的,若是您在正式場合使用,好比面試中,請使用更高大上的專業用語。謝謝