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