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