大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以點此加我微信ruochuan12?進群參與,每周大家一起學習200行左右的源碼,共同進步。已進行4個月了,很多小伙伴表示收獲頗豐。
以下問題你是不是在哪里聽過?
你知道什么是 Promise 嗎?它是干什么用的呢?
那你知道 Promise 有哪些方法嗎?如何使用呢?
Promise 的 then 方法(或者 catch 方法)是怎么實現的呢?
能手寫一個 Promise 嗎?
Promise 和 async/await 的區別是什么?
Promise 中有異步任務(例如 setTimeout 等)的執行順序是什么樣的呢?
為什么面試過程中 Promise 出現的頻率這么高呢?
異步編程是 JavaScript 中的一個核心概念,與其他腳本編程語言相比,異步編程是一項讓 JavaScript 速度更快的特性。JavaScript 是單線程的,這意味著它逐行執行程序。它也是異步的,這意味著如果我們的程序執行到達一個必須等待結果的代碼塊,它將繼續經過這個正在等待的代碼塊,因此程序不會凍結執行,并且一旦該異步任務完成,我們的代碼將通過使用回調來處理它正在等待的結果。如果回調太多,嵌套太深,Promise 確實可以解決這一痛點。其實上面的問題如果動手寫過一次源碼,基本就是都清楚了
接下來就根據 Promise 的特性來實現一下
大體結構如下:

第一步是需要根據使用實現構造函數;
第二步是實現原型方法 then,then 是核心邏輯,其他的方法都是對 then 方法的使用和完善;
下面我們就來一步步看看這個 Promise 的實現。
一、介紹 Promise
Promise 是 ES6 中進行異步編程的新解決方案(相對于單純使用回調函數),具有三種狀態:pending、rejected、resolved,狀態的修改只能是 pending 到 rejected 或者 pending 到 resolved,且狀態是不可逆的。它的使用這里就不多說啦,大致結構如下:
const?p?=?new?Promise((resolve,?reject)?=>?{resolve("success");
});
p.then((value)?=>?{console.log("成功",?value);},(reason)?=>?{console.log("失敗",?reason);}
).catch((error)?=>?{console.log("錯誤",?error);
});
then 方法中有成功和失敗的回調,catch 是捕獲整個過程中產生的錯誤。
在這里需要注意一個問題,如果resolve("success");
?是在一個異步中,例如定時器,then 方法并不是在定時器結束才綁定,而是直接綁定的,只不過成功和失敗的回調是在狀態修改以后才調用的,這個很重要,封裝 then 方法的時候需要實現這一邏輯。
它的方法分為原型方法和構造函數方法,then 和 catch 為原型上的方法,即實例上可調用的方法,其它為構造函數的方法。現有的方法和解釋給大家都列出來啦!
Promise.prototype.then 方法: (onResolved, onRejected) => {} (1) onResolved 函數: 成功的回調函數 (value) => {} (2) onRejected 函數: 失敗的回調函數 (reason) => {} 說明: 指定用于得到成功 value 的成功回調和用于得到失敗 reason 的失敗回調 返回一個新的 promise 對象
Promise.prototype.catch 方法: (onRejected) => {} (1) onRejected 函數: 失敗的回調函數 (reason) => {} 說明: then()的語法糖, 相當于: then(undefined, onRejected)
Promise.resolve 方法: (value) => {} (1) value: 成功的數據或 promise 對象 說明: 返回一個成功/失敗的 promise 對象
Promise.reject 方法: (reason) => {} (1) reason: 失敗的原因 說明: 返回一個失敗的 promise 對象
Promise.all 方法: (promises) => {} (1) promises: 包含 n 個 promise 的數組 說明: 返回一個新的 promise, 接收一個 Promise 對象的集合,只有所有的 promise 都成功才成功, 只要有一個失敗了就 直接失敗
Promise.race 方法: (promises) => {} (1) promises: 包含 n 個 promise 的數組 說明: 返回一個新的 promise, 接收一個 Promise 對象的集合,第一個完成的 promise 的結果狀態就是最終的結果狀態
Promise.any 方法: (promises) => {} (1) promises: 包含 n 個 promise 的數組 說明: 返回一個新的 promise, 接收一個 Promise 對象的集合,當其中的一個 promise 成功,就返回那個成功的 promise 的值,如果可迭代對象中沒有一個 promise 成功(即所有的 promises 都失敗/拒絕),就返回一個失敗的 promise 和 AggregateError 類型的實例。
Promise.allSettled 方法: (promises) => {} (1) promises: 包含 n 個 promise 的數組 說明:方法返回一個在所有給定的 promise 都已經
fulfilled
或rejected
后的 promise,并帶有一個對象數組,每個對象表示對應的 promise 結果。當您有多個彼此不依賴的異步任務成功完成時,或者您總是想知道每個promise
的結果時,通常使用它。該方法為 ES2020 新增的特性,它能夠返回所有任務的結果。
二、封裝 Promise
根據 Promise 的使用可以確定需要封裝的整體結構如下:
//?構造函數
function?Promise(executor)?{function?resolve(data)?{}function?reject(data)?{}executor(resolve,?reject);
}//then方法
Promise.prototype.then?=?function?(onResolved,?onRejected)?{};//catch方法
Promise.prototype.catch?=?function?()?{};Promise.resolve?=?function?()?{};
Promise.reject?=?function?()?{};
Promise.race?=?function?()?{};
Promise.any?=?function?()?{};
Promise.all?=?function?()?{};
Promise.allSettled?=?function?()?{};
new Promise()
有一個回調函數需要實現,且回調函數需要有兩個參數,所以構造函數需要有一個參數executor
Promise 構造方法的實現如下:
//?構造函數
function?Promise(executor)?{this.promiseState?=?"pending";this.primiseResult?=?null;//?保存then的回調函數,使用數組主要是為了鏈式調用的場景,多個then方法的回調this.callbacks?=?[];const?self?=?this;/***?改變狀態的三種方式*?1、resolve*?2、reject*?3、throw*/function?resolve(data)?{//?保證狀態只能修改一次if?(self.promiseState?!==?"pending")?return;//?修改對象狀態self.promiseState?=?"fulfilled";//?設置對象結果值self.primiseResult?=?data;//?then方法的回調函數異步執行setTimeout(()?=>?{//?狀態改變觸發回調函數的執行self.callbacks.forEach((item)?=>?{item.onResolved(data);});});}function?reject(data)?{//?保證狀態只能修改一次if?(self.promiseState?!==?"pending")?return;//?修改對象狀態self.promiseState?=?"rejected";//?設置對象結果值self.primiseResult?=?data;//?then方法的回調函數異步執行setTimeout(()?=>?{//?狀態改變觸發回調函數的執行self.callbacks.forEach((item)?=>?{item.onRejected(data);});});}//?throw要改變狀態?通過try...catch...try?{executor(resolve,?reject);}?catch?(e)?{//catch方法的實現reject(e);}
}
改變 Promise 狀態的三種方式:
resolve()
reject()
throw() 通過 try...catch...實現
上面的代碼中兼容了對上面三種方法的處理,Promise 狀態只能修改一次且不可逆,如果調用了 resolve(),然后再調用 reject(),只會執行前者,后者不執行;那么如何實現狀態的不可逆修改呢?通過判斷狀態if(self.promiseState !== 'pending') return;
?即保證每次都是從 pending 修改狀態到失敗或者成功。
new 完以后需要通過實例方法調用 then 和 catch 方法,所以下面是這兩個方法的實現:
//then方法
Promise.prototype.then?=?function?(onResolved,?onRejected)?{const?self?=?this;//?【異常穿透】如果沒有寫失敗的回調,這里需要補充上,并拋出一個錯誤if?(typeof?onRejected?!==?"function")?{onRejected?=?(reason)?=>?{throw?reason;};}//?【值傳遞】if?(typeof?onResolved?!==?"function")?{onResolved?=?(value)?=>?value;}return?new?Promise((resolve,?reject)?=>?{function?callback(type)?{//?獲取then回調函數的執行結果try?{const?result?=?type(self.primiseResult);if?(result?instanceof?Promise)?{//?返回結果是Promiseresult.then((v)?=>?{resolve(v);},(r)?=>?{reject(r);});}?else?{resolve(result);}}?catch?(e)?{reject(e);}}if?(this.promiseState?===?"fulfilled")?{//?then方法的回調函數異步執行setTimeout(()?=>?{callback(onResolved);});}if?(this.promiseState?===?"rejected")?{//?then方法的回調函數異步執行setTimeout(()?=>?{callback(onRejected);});}//?異步處理,狀態沒有變更if?(this.promiseState?===?"pending")?{this.callbacks.push({onResolved:?function?()?{callback(onResolved);},onRejected:?function?()?{callback(onRejected);},});}});
};
then 方法中需要判斷 pending 的情況,主要是因為狀態變更有異步的可能,需要先存儲 then 的回調函數,方便狀態修改以后調用,將所有的異步回調存儲到callbacks
,由于會有多個 then 方法鏈式調用,所以 callbacks 是數組,用于保存多個回調,且 then 方法的回調函數不是同步執行的,所以需要通過 setTimeout 放入另一個隊列;
鏈式調用,涉及到 then 方法的返回,返回值必須是個 Promise 才能實現鏈式調用;成功的回調函數返回的結果也可能是 Promise;成功的回調函數返回的結果 考慮到 throw 的情況,還是要使用 try...catch...;中斷 promise,返回一個 pending 狀態的 promise;
//?catch方法
//?需要處理異常穿透
Promise.prototype.catch?=?function?(onRejected)?{return?this.then(undefined,?onRejected);
};
catch 方法及異常穿透 catch 方法的功能 then 已經實現了,直接使用就可以,只是沒有成功的處理函數;then 方法中沒有寫失敗的回調函數,會默認添加一個失敗的回調函數并拋出異常,最后統一由 catch 處理異常。
值傳遞:第一個回調函數不傳也可以,我們會在 then 方法處理這種情況,如果檢測到沒有這個方法,就自動添加這個方法。
接下來是對構造函數的實現,之所以在 then 方法后面現實是因為下面這些方法的實現是基于上面的實現。resolve 方法快速創建 promise 對象的實現,所以可以直接調用封裝好的 Promise,以下的方法基本都是對上面方法的使用
// resolve方法?作用:?快速創建promise對象
Promise.resolve?=?function?(value)?{return?new?Promise((resolve,?reject)?=>?{if?(value?instanceof?Promise)?{value.then((v)?=>?{resolve(v);},(r)?=>?{reject(r);});}?else?{resolve(value);}});
};
接下來是 reject 方法的實現,傳入什么都是返回失敗,也是調用現有的方法,直接返回將狀態修改為失敗:
Promise.reject?=?function?(value)?{return?new?Promise((resolve,?reject)?=>?{reject(value);});
};
race 方法無論成功失敗,只要最先返回的結果,只要有結果就返回:
Promise.race?=?function?(promises)?{return?new?Promise((resolve,?reject)?=>?{for?(let?i?=?0;?i?<?promises.length;?i++)?{promises[i].then((v)?=>?{//?最先返回的改變狀態resolve(v);},(r)?=>?{reject(r);});}});
};
all 方法的實現:其中一個 Promise 成功的時候不可以改變狀態,只有全部成功才能改變狀態;實現是使用一個計數器,當數量和promises
數量相同,且都成功了,就返回所有結果,失敗直接改變狀態結
//?all方法
Promise.all?=?function?(promises)?{return?new?Promise((resolve,?reject)?=>?{let?count?=?0;let?arr?=?[];for?(let?i?=?0;?i?<?promises.length;?i++)?{promises[i].then((v)?=>?{//?根據all的定義,不可以直接改變狀態count++;//不用push是為了保證輸出的順序正常一一對應arr[i]?=?v;if?(count?===?promises.length)?{resolve(arr);}},(r)?=>?{reject(r);});}});
};
any 方法實現:其中的一個 promise 成功,就返回那個成功的 promise 的值,失敗返回一個AggregateError
類型的錯誤new AggregateError('AggregateError: All promises were rejected')
//?any方法?其中的一個?promise?成功,就返回那個成功的promise的值,失敗返回一個AggregateError類型的錯誤
Promise.any?=?function?(promises)?{let?count?=?0;return?new?Promise((resolve,?reject)?=>?{for?(let?i?=?0;?i?<?promises.length;?i++)?{promises[i].then((v)?=>?{resolve(v);},(r)?=>?{count++;if?(count?===?promises.length)?{reject(new?AggregateError("AggregateError:?All?promises?were?rejected"));}});}});
};
allSettled 方法是比較少知道的方法,有時候會在面試者被問到你如何將所有的成功失敗結構都返回,下面就是答案:
//?allSettled方法?所有結果都返回后顯示每個結果的返回值
Promise.allSettled?=?function?(promises)?{return?new?Promise((resolve,?reject)?=>?{let?arr?=?[];for?(let?i?=?0;?i?<?promises.length;?i++)?{promises[i].then((v)?=>?{arr[i]?=?{?status:?"fulfilled",?value:?v?};if?(arr.length?===?promises.length)?{resolve(arr);}},(r)?=>?{arr[i]?=?{?status:?"rejected",?reason:?r?};if?(arr.length?===?promises.length)?{resolve(arr);}});}});
};
實現了上面的構造方法以后,可以發現,提供的方法如果在你的邏輯中不適用,也可以類比上面的方法實現自己想要的方法。
三、總結 Promise
一步一步實現下來,發現邏輯都是環環相扣的
由于需要狀態的管理并且不可逆,所以需要有個變量來保存狀態;
由于構造函數的參數(回調函數)可以改變狀態,所以需要添加對應的方法來處理狀態的修改;
又由于狀態的可能是異步修改的,所以需要添加一個變量來保存 then 方法的回調函數;
由于 then 可以存在多個,所以保存回調函數的變量得是一個數組;
由于 then 可以鏈式調用,所以 then 方法必須返回一個 promise 對象;
其他方法也可以調用 then 方法,所以也需要返回一個 promise 對象;
由于 throw 也可以改變狀態,所以處理需要使用 try...catch...實現狀態的改變;
由于可能會存在 then 方法沒有失敗回調函數的情況,所以異常需要統一由 catch 方法收口;
由于 catch 方法可以再多個 then 方法之后,所以需要考慮異常穿透,將失敗回調函數補充上并拋出異常;
其他方法的實現主要是在上面的基礎上保證在特定的時期改變返回的 promise 的狀態,有的是在第一次成功的時候返回成功(比如 any 方法);有的是在所有都成功的時候返回成功(比如 all 方法);有的是在第一結果返回的時候就返回,無論成功失敗(比如 race 方法);有的是在所有結果都返回了以后就返回結果,無論成功失敗(比如 allSettled 方法)。
四、擴展
async/await 也是異步編程的一種解決方案,他遵循的是 Generator 函數的語法糖,他擁有內置執行器,不需要額外的調用直接會自動執行并輸出結果,它返回的是一個 Promise 對象。在涉及到比較復雜的業務場景,then 方法的調用會顯得不太美觀,但是 async/await 看起來就好很多,這一句代碼執行完,才會執行下一句。
以上就是我對 promise 的學習和理解,如果有什么問題請大家指正。
最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信 江西?拉你進群。
推薦閱讀
整整4個月了,盡全力組織了源碼共讀活動~
我歷時3年才寫了10余篇源碼文章,但收獲了100w+閱讀
老姚淺談:怎么學JavaScript?
我在阿里招前端,該怎么幫你(可進面試群)
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》10余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助1000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。分享、收藏、點贊、在看我的文章就是對我最大的支持~