面試官問:來實現一個Promise

大家好,我是若川。最近組織了源碼共讀活動《1個月,200+人,一起讀了4周源碼》,已經有超50+人提交了筆記,群里已經有超1500人,感興趣的可以點此鏈接掃碼加我微信?ruochuan12?參與,一起學習,共同進步。

前言

Promise 作為異步編程的一種解決方案,在 ES6 中被標準化,提供了 Promise 對象和一系列的 API。在事件循環、鏈式調用、調度器實現等面試場景中均有涉及。在本文中筆者將從零實現一個符合 Promise/A+ 標準的 Promise 主體代碼邏輯,并在后續系列文章中給出其他方法的實現以及常見的實際使用場景中的解法。

1、準備

本文按?Promise/A+[1]?(中文版【翻譯】Promises/A+規范[2]) 的標準實現,不熟悉的讀者可以先看一遍,了解一些術語做些準備知識。

2、Promise 實現

本節,我們將進入正題,從零實現一個我們自己的 Promise。PS:在下文中,本文約定大寫 Promise 指代我們實現的 MyPromise 函數對象,小寫 promise 指代一個實例對象。

現在,我們實現的 MyPromise 函數第一版定義如下

function MyPromise(executor) {// TODO}

2.1、狀態定義

Promise 只有三種狀態:pending、fulfilled 和 rejected, 其中后兩種是終態。

因此,我們可以先定義一個狀態集合:

const PRO_STATUS = {PENDING: 'pending',FULFILLED: 'fulfilled',REJECTED: 'rejected'
}

2.2、狀態轉換及方式

promise 對象內部就像一個狀態機,但是這個狀態機有一點自己的限制條件,即, 它的狀態變換路徑只有兩種:

pending-》fulfilled 
或者
pending -》rejected

并且,轉換之后是固定的。Ok,在談完狀態轉換的路徑后,我們來看一下狀態轉換的方式。

在初始化 promise 對象時需要向構造函數提供一個 executor 函數,該函數有兩個入參(函數類型):

?1、resolve,該函數接受一個參數,更改 promise 內部狀態 pending-》fullfilled?2、reject,該函數接受一個 Error 類型參數,更改 promise 內部狀態 pending -》rejected

至此,總結一下我們的 MyPromise 里應該有幾個東西:

當前狀態
fulfilled 狀態下的 value 值
rejected 狀態下的 reason 值
resolve 函數
reject 函數

那么,MyPromise 第二版現在是如下的樣子:

let count = 0
function MyPromise(executor) {const self = thisself.status = PRO_STATUS.PENDINGself.count = ++countself.fulfilledValue = undefinedself.rejectedReason = undefinedtry {executor(resolve, reject)} catch (error) {reject(error)}function resolve(rs) {//TODO}function reject(err){// TODO}
}

這里,我們為每個實例追加了一個 count 計數,讀者可以忽略。

實例化函數后,我們直接執行了 executor 函數,并傳入了兩個函數類型的參數。在 executor 函數內部,用戶可以通過 resolve 或者 reject 修改 promise 對象進入終態,并且只能進入一次,舉個例子:

new MyPromise((resolve, reject)=>{//balabala.......resolve(1) reject(new Error('error'))resolve(2)
})

這里寫了三行修改 promise 狀態的代碼,但是最后 promise 的狀態是 fulfilled,并且 fulfilledValue 是 1。這個我們在后面 resolve 和 reject 實現中說。

2.3、then 和 catch 方法

我們知道,Promise 對象實現了鏈式調用來解決回調地獄的問題。類似這樣:

new Promise(()=>{....
})
.then(rs=>{...
})
.then(rs=>{})
.catch(err=>{})

也就是說,我們可以在 then 或 catch 中拿到 promise 對象的終態數據并通過生成新的 promise 對象向下傳遞。

首先,我們來看看 then 方法。

then 方法接受兩個函數類型的參數:onfulfilled 和 onrejected。onfulfilled 接受 promise 的 fulfilledValue 作為入參并在 promise 為 fulfilled 狀態時被調用, onrejected 接受 promise 的 rejectedReason 作為入參并在 promise 為 rejected 狀態時被調用。

const onfulfilled = value =>{...}
const onrejected = reason =>{...}
promiseA.then(onfulfilled, onrejected)

此外,then 方法將返回一個新的 Promise 類型對象。

相比 then方法,catch?方法僅接受一個 onrejected 函數類型的參數。和 then 方法一樣將返回一個新的 Promise 類型對象。

const onrejected = reason =>{...}
promiseA.catch(onrejected)

實際上,then 和 catch 方法有幾個作用:

?為 promise 對象收集 onfulfilled 和 onrejected 回調函數,在終態后(resolve 和 reject 函數觸發)進行回調的調用?觸發 onfulfilled 和 onrejected 回調函數

其實第一個比較好理解,第二個可以用下面一個代碼去解釋。

let promise = new Promise((resolve)=>{setTimeout(()=>{resolve()promise.then(rs=>{console.log(2)}) // then2})
}).then(rs=>{console.log(1)}) // then1

rs=>{console.log(1)} 回調通過 then1 收集,在 resolve 調用后被觸發。此時 promise 對象進入終態, rs=>{console.log(2)} 回調通過 then2 收集并觸發執行。

并且,這些回調函數只會被調用一次。

綜上,我們可以總結如下:

?MyPromise 內部 resolve、reject 函數以及 then、catch 都可能會觸發回調函數執行,那么他們可能在代碼鏈路上交匯在某個執行點,也就是說他們調用了同一個處理函數,我們定義為 _handle 函數。?此外,Promise 函數內部有一個數據結構維護當前的回調函數,這里我們需要一個隊列。?最后,如果我們有 promise A 對象,promise A 對象的 then 和 catch 方法都會返回一個新的 promise B 實例,A 內部狀態是 fullfilled,它只調用 onfulfilled 方法。此外,promise A 進入終態才會使得 promise B 進入終態,關鍵點在于 A 持有 B 的 resolve、reject,A 進入終態后調用 B 的 resolve/reject,具體調用 resolve 還是 reject 以及入參要分情況區別。

Ok,有了上面的結論,我們繼續修改已有代碼:

const TYPES = {THEN: 'then',CATCH: 'catch',FINALLY: 'finally'
}
...
function MyPromise(executor) {const self = this...self.cbQueue = [] // 保存回調等數據...function resolve(rs) {self._handle()}function reject(err){self._handle()}
}MyPromise.prototype._handle = function(cb){// TODO
}/*** * @param {*} onfulfilled * @param {*} onrejected * @returns */
MyPromise.prototype.then = function(onfulfilled, onrejected) {return new Promise((resolve, reject) => {this._handle({type: TYPES.THEN,resolve,reject,onfulfilled,onrejected})})
}/*** * @param {*} onrejected * @returns */
MyPromise.prototype.catch = function(onrejected) {return new Promise((resolve, reject) => {this._handle({type: TYPES.CATCH,resolve,reject,onrejected})})
}

上面的代碼里,我們還定義了一個 TYPES 來指定回調函數是通過 then、catch還是 finally 方法收集的,以此來輔助我們在 _handle 函數中的處理。

現在關鍵來到了 _handle 函數。我們根據 Promise/A+ 的標準和實際代碼使用中,對于細節進行了歸結。

?1、A.resolve() 情況下,B 不管是通過 then 還是 catch 產生, 都要調用 B.resolve(),入參要看是否提供了 onfulfilled,具體如下:

????1)如果 B 是 A.then 生成,則 B.resolve(onfulfilled(A.fulfilledValue))

????2)如果 B 是 A.catch 生成,則 B.resolve(A.fulfilledValue),不會調用 A.catch 提供的 onrejected 方法 如果不提供 onfulfilled 則 B.resolve(A.fulfilledValue)

?2、注意,A.reject() 的情況下,如果有 onrejected 函數處理則狀態發生轉換,并且入參要看是否提供了 onrejected 函數進行包裝,具體如下:

????1)A 調用 reject,B 是 A.then 生成,則 B.reject(A.rejectedReason) 或者 B.resolve(onrejected(A.rejectedReason))

????2)A 調用 reject,B 是 A.catch 生成,則 B.resolve(onrejected(A.rejectedReason)) 如果不提供 onrejected 則 B.reject(A.rejectedReason)

?????????3、如果 A.fulfilledValue 是一個 Promise 類型,則要把 A.then() 這些收集到的回調給 A.fulfilledValue

針對 3,可以看如下示例代碼:

function delay(){new Promise((resolve, reject)=>{// promise 0// console.log('0')// resolve('resolve')reject(new Error('reject'))}).then(rs=>{return new Promise(resolve=>{setTimeout(()=>{console.log('1')resolve('inner rs')}, 2000)})}).catch(err=>{ // promise 1return new Promise(resolve=>{ // promise 2setTimeout(()=>{console.log('1')resolve('inner err')}, 2000)})}).then(rs=>{console.log('2')return 'then2'})
}
最后打印:
(注:先延遲2s)
1 
2

最后的 then 會被 promise1 收集到,因為 promise1 的 fulfilledValue 是一個 Promise 類型對象,即 promise2。要實現延遲 2s 打印 1 后再打印 2,需要把 promise1 收集到的回調賦給 promise2。

Ok,了解了處理邏輯,我們就可以直接上代碼了。

MyPromise.prototype._handle = function(cb){if(cb) {// then、catch、finally 方法處理this.cbQueue.push(cb)}else{// resolve、reject 處理}if(this.status === PRO_STATUS.PENDING){// nothing to do} else {for (let i = 0; i < this.cbQueue.length; i++) {const cb = this.cbQueue[i];const { type, resolve, reject, onfulfilled, onrejected, onfinally } = cb// finallyif(type === TYPES.FINALLY){onfinally()resolve()continue}if(this.status === PRO_STATUS.FULFILLED){//if(typeof resolve === 'function'){let fulfilledValue = this.fulfilledValuelet ans = fulfilledValueif(fulfilledValue instanceof MyPromise){// 收集的回調賦給 fulfilledValuefulfilledValue.cbQueue = this.cbQueuethis.cbQueue = []continue}if(typeof onfulfilled === 'function'){// 這里要處理一下數據ans = onfulfilled(fulfilledValue)if(ans instanceof MyPromise && ans.status !== PRO_STATUS.PENDING){if(ans.status === PRO_STATUS.FULFILLED){resolve(ans.fulfilledValue)}else{reject(ans.rejectedReason)}}else{resolve(ans)}}else{resolve(ans)}}}else{if(typeof resolve === 'function'){let ans = this.fulfilledValueif(typeof onrejected === 'function'){/*** 這個地方要注意下,上面 setTimeout模擬異步的地方,修改狀態的部分要放在 setTimeout 外面。* 否則到這里, status 還是 pending*/ans = onrejected(this.rejectedReason)if(ans instanceof MyPromise && ans.status !== PRO_STATUS.PENDING){if(ans.status === PRO_STATUS.FULFILLED){resolve(ans.fulfilledValue)}else{reject(ans.rejectedReason)}}else{resolve(ans)}}else{reject(this.rejectedReason)}}}// })}this.cbQueue = []}
}

_handle 函數先對進來的參數進行判斷,有的話就入隊列。然后看當前的狀態,是終態就處理上面的邏輯,最終清空隊列。否則就直接退出。PS:這里缺少了 finally 方法的處理代碼,我們在后面補上。此外還有就是關于異常的拋出問題,當 promise A 對象進入 rejected 狀態,此時,如果 promise.then 未提供 onrejected,則會拋出 error; 如果提供 onrejected,則不會,也就是有的資料中提到的 error 被“吃掉”了,這部分功能并未實現。

2.4、resolve 和 reject 實現

好了,到了這里基本上完成了大部分的工作了,但是還缺少了 resolve 和 reject 部分的代碼實現。無論是 resolve 還是 reject 函數,他們的功能都是兩個部分:

?修改狀態?觸發 onfulfilled/onrejected 回調(如果有的話)

我們都知道,Promise 屬于異步任務里的微任務,在構造函數里的代碼和 onfulfilled/onrejected 里的代碼都運行在主線程中。因此,我們需要模擬一個異步的過程,并且在定義多個 Promise 對象實例時保證一個時序,這里我們用 setTimeout,并在 setTimeout 中調用 _handle 函數。好的,我們來看 resolve 函數的具體實現。

function resolve(rs) {if(self.status === PRO_STATUS.PENDING) {/*** 在主線程修改狀態*/self.status = PRO_STATUS.FULFILLEDself.fulfilledValue = rssetTimeout(() => { /*** 模擬異步,但是這里有一個bug,setTimeout 并不準確,* 在 Promise.race 中有問題,主要是 setTimeout 執行間隔*/self._handle()})}
}

可以看到,首先判斷了當前狀態,確保只能第一個 resolve/reject 方法里面的代碼被執行去把 pending 狀態修改為終態,并且是在主線程中修改了狀態。另外注釋里標明了一個問題,我們使用了 setTimeout 去模擬異步,但是因為它本身延遲執行的特性,會帶來一些問題,比如下面的測試代碼:

const Promise = MyPromise
function race(){Promise.race([new Promise(resolve=>{setTimeout(()=>{resolve(1)}, 200)// 這里有一個bug?超時改成20看看}),new Promise((resolve, reject)=>{setTimeout(()=>{// resolve(2)reject(new Error('timeout'))}, 10)})]).then(res=>{log2('race res', {res})},err=>{log2('race err', {err})})
}

在注釋處修改為 20 會產生意外的效果。比照 resolve 函數的實現,我們可以很容易給出 reject 函數的代碼實現:

function reject(err){if(self.status === PRO_STATUS.PENDING) {self.status = PRO_STATUS.REJECTEDself.rejectedReason = errsetTimeout(() => {self._handle()})}
}

3、結語

通過本文的介紹,我們得到了一個 Promise 實現。下一節中,我們介紹一些 API 的實現。

References

[1]?Promise/A+:?https://promisesaplus.com/
[2]?【翻譯】Promises/A+規范:?https://www.ituring.com.cn/article/66566

最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信?江西?拉你進群。


推薦閱讀

1個月,200+人,一起讀了4周源碼
我歷時3年才寫了10余篇源碼文章,但收獲了100w+閱讀

老姚淺談:怎么學JavaScript?

我在阿里招前端,該怎么幫你(可進面試群)

fcce66a6d72f4df0ab5ffe4a9179fd5f.gif

·················?若川簡介?·················

你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動

ca26810f7a6572b0c20305fd065415d2.png

識別方二維碼加我微信、拉你進源碼共讀

今日話題

略。歡迎分享、收藏、點贊、在看我的公眾號文章~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/275339.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/275339.shtml
英文地址,請注明出處:http://en.pswp.cn/news/275339.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

奇跡暖暖服務器不穩定,閃耀暖暖用土豆當服務器?開服僅半小時就崩潰,無數玩家瘋狂吐槽...

大家好&#xff0c;這里是正驚游戲&#xff0c;我是你們的正驚小弟。繼奇跡暖暖之后&#xff0c;疊紙游戲的3D換裝類游戲《閃耀暖暖》于昨天正式開啟了全平臺公測。就在大家想要上游戲給女兒買好看的衣服時&#xff0c;發現游戲的服務器崩了&#xff0c;誰都登錄不上去&#xf…

D2 日報 2019年4月17日

? 新聞 ?? Is React Translated Yet? ¡S! Sim! はい&#xff01; react 文檔翻譯了多種語言reactjs.org? 開源項目 ?? formal/packages/formal-web at master kevinwolfcr/formal React Hooks 版本的 rc-form&#xff0c;集成了 React 表單組件通用的的非受控值緩…

nda協議_如何將NDA項目添加到您的投資組合

nda協議Being on the job hunt meant I needed to update my portfolio again. I had a new project to add, but it was under an NDA and I couldn’t say too much about it. Since I’ve never had to figure out how to display an NDA project on my portfolio before, I…

程序員一定會有35歲危機嗎?

大家好&#xff0c;我是若川。最近組織了源碼共讀活動《1個月&#xff0c;200人&#xff0c;一起讀了4周源碼》&#xff0c;已經有超50人提交了筆記&#xff0c;群里已經有超1500人&#xff0c;感興趣的可以點此鏈接掃碼加我微信 ruochuan12你好&#xff0c;我是黃老師。最近經…

hdu 2141 Can you find it? hdu1597 find the nth digit

hdu2141 唉&#xff0c;是我 想多了&#xff0c;用普通方法拼命剪枝&#xff0c;還是TLE 直接將前倆個數組的和求出來并保存&#xff0c;之后就是一個二分查找的過程了 二分的倆種寫法 第一種 #include<iostream>#include<algorithm>#include<string>using …

好程序員分享大勢所趨 HTML5成Web開發者最關心的技術

好程序員分享大勢所趨 HTML5成Web開發者最關心的技術&#xff0c;最近&#xff0c;在Stack Exchange上出現了一個比較熱門的問題&#xff1a;Web開發者最頭疼的問題是什么?結果并不是大家通常認為的兼容性問題&#xff0c;而是關于HTML5。  在所有與前端開發相關的技術中&am…

微軟bi 架構 服務器,微軟BI體系結構.

《微軟BI體系結構.》由會員分享&#xff0c;可在線閱讀&#xff0c;更多相關《微軟BI體系結構.(41頁珍藏版)》請在人人文庫網上搜索。1、Data Warehouse Data Access 前端報表用戶前端報表用戶 Data Sources Data Input Staging Area Data Marts 財務經理的視角財務經理的視角 …

網頁開發環境的重要性_少即是多:極簡方法在網頁設計中的重要性

網頁開發環境的重要性Written by Alan Smith由艾倫史密斯 ( Alan Smith)撰寫 Minimalism has been an increasingly popular trend in the web design world. Designers may be tempted by bolder, feature-rich design because it might seem like the best way to engage us…

聊聊前端八股文?

大家好&#xff0c;我是若川&#xff0c;點此加我微信進源碼群&#xff0c;一起學習源碼。同時可以進群免費看Vue專場直播&#xff0c;有尤雨溪分享「Vue3 生態現狀以及展望」前些天&#xff0c;我看到《劍指前端offer》一系列文章&#xff0c;被前言部分圖示和文章內容驚艷到。…

微服務神經元(Neural)

微服務架構中的神經組織&#xff0c;主要為分布式架構提供了集群容錯的三大利刃&#xff1a;限流、降級和熔斷。并同時提供了SPI、過濾器、JWT、重試機制、插件機制。此外還提供了很多小的黑科技(如&#xff1a;IP黑白名單、UUID加強版、Snowflake和大并發時間戳獲取等)。Featu…

flash跨域訪問解決辦法

今天一個客戶的flash程序突然無法訪問到數據&#xff0c;經過檢查發現當時做flash時&#xff0c;對訪問的數據使用了域名方式訪問&#xff0c;但是現在客戶又綁定了另一個域名&#xff0c;所以另一個域名訪問時就造成了跨域訪問&#xff0c;由于flash采用完全域匹配規則&#x…

服務器內存型號與頻率,一張圖看懂如何選擇DDR4內存的頻率和容量

Intel發布了代號為Skylake的第六代酷睿處理器&#xff0c;與此同時各大主板廠商也迅速推出基于100系列芯片組的各型號主板以迎接Skylake處理器&#xff0c;分別有Z170、H170及B150三個不同級別的芯片組。那針對著不同芯片組主板&#xff0c;如何選擇DDR4內存的頻率和容量&#…

Promise 到底是什么?看這個小故事

大家好&#xff0c;我是若川&#xff0c;點此加我微信進源碼群&#xff0c;一起學習源碼。還可以進《劍指前端offer》交流群。另外&#xff0c;可以進群免費看下周六Vue專場直播&#xff0c;有尤雨溪分享「Vue3 生態現狀以及展望」如果你還是一個 JavaScript 初學者&#xff0c…

docker 修改服務器,docker-修改容器掛載目錄的3種方法小結

本文關鍵詳細介紹了docker-修改容器初始化目錄的3種方式總結&#xff0c;具備非常好的實用價值&#xff0c;期待對大伙兒有一定的協助。一起追隨我回來瞧瞧吧方法一&#xff1a;修改配置文件(需停止docker服務)1、停止docker服務systemctl stop docker.service(重要&#xff0c…

什么是測試開發

aaa轉載于:https://www.cnblogs.com/Chamberlain/p/10730856.html

DropDownList 控件不能觸發SelectedIndexChanged 事件的另一個原因

相信DropDownList 控件不能觸發SelectedIndexChanged 事件已經不是什么新鮮事情了&#xff0c;原因也無外乎以下幾種&#xff1a; 1、DropDownList 控件的屬性 AutoPostBack"True" 沒有寫&#xff1b; 2、DropDownList 控件的數據綁定沒有放在if (!Page.IsPostBack) …

Vue 團隊公開快如閃電的全新腳手架工具,未來將替代 Vue-CLI,才300余行代碼,學它!...

1. 前言大家好&#xff0c;我是若川。歡迎關注我的公眾號若川視野源碼共讀活動ruochuan12想學源碼&#xff0c;極力推薦之前我寫的《學習源碼整體架構系列》jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue-next-release…

斑馬無線打印服務器,如何設置斑馬打印機無線WiFi

安裝Zebra Setup Utilities.exe&#xff0c;打開軟件(沒有該軟件的可以向客服索要)界面如果是英文請選擇options(選項)&#xff0c;選擇應用程序語言Simplified Chinese(簡體中文)點擊確定&#xff0c;關閉軟件&#xff0c;重新打開&#xff0c;界面就會顯示中文。點擊相應的打…

Python自然語言處理學習筆記(19):3.3 使用Unicode進行文字處理

3.3 Text Processing with Unicode 使用Unicode進行文字處理 Our programs will often need to deal with different languages, and different character sets. The concept of “plain text” is a fiction&#xff08;虛構&#xff09;. If you live in the English-speakin…

小程序卡片疊層切換卡片_現在,卡片和清單在哪里?

小程序卡片疊層切換卡片重點 (Top highlight)介紹 (Intro) I was recently tasked to redesign the results of the following filters:我最近受命重新設計以下過濾器的結果&#xff1a; Filtered results for users (creatives) 用戶的篩選結果(創意) 2. Filtered results fo…