面試官:能不能手寫一個 Promise?

大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以點此加我微信ruochuan12?進群參與,每周大家一起學習200行左右的源碼,共同進步。已進行4個月了,很多小伙伴表示收獲頗豐。

d2cb0a1998fab92ad2a1655fc49a4002.png以下問題你是不是在哪里聽過?

  1. 你知道什么是 Promise 嗎?它是干什么用的呢?

  2. 那你知道 Promise 有哪些方法嗎?如何使用呢?

  3. Promise 的 then 方法(或者 catch 方法)是怎么實現的呢?

  4. 能手寫一個 Promise 嗎?

  5. Promise 和 async/await 的區別是什么?

  6. Promise 中有異步任務(例如 setTimeout 等)的執行順序是什么樣的呢?

為什么面試過程中 Promise 出現的頻率這么高呢?

異步編程是 JavaScript 中的一個核心概念,與其他腳本編程語言相比,異步編程是一項讓 JavaScript 速度更快的特性。JavaScript 是單線程的,這意味著它逐行執行程序。它也是異步的,這意味著如果我們的程序執行到達一個必須等待結果的代碼塊,它將繼續經過這個正在等待的代碼塊,因此程序不會凍結執行,并且一旦該異步任務完成,我們的代碼將通過使用回調來處理它正在等待的結果。如果回調太多,嵌套太深,Promise 確實可以解決這一痛點。其實上面的問題如果動手寫過一次源碼,基本就是都清楚了

接下來就根據 Promise 的特性來實現一下

大體結構如下:

8afea7c0769f32aa3c31ca380bf17e9d.png
Promise1

第一步是需要根據使用實現構造函數;

第二步是實現原型方法 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 都已經fulfilledrejected后的 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 狀態的三種方式:

  1. resolve()

  2. reject()

  3. 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?

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

8ad3f5d7a92b07ef1d622da1731fd836.gif

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

你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》10余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助1000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。

b5320ecb88f6a72ad769aab9538966a0.png

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

今日話題

略。分享、收藏、點贊、在看我的文章就是對我最大的支持~

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

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

相關文章

設計圖像素和開發像素_游戲開發的像素藝術設計

設計圖像素和開發像素Pixel art is a large part of the legacy of game development. Every Pokemon game up until their X/Y series was rendered entirely with pixel art, Ragnarok Online (2000) was one of the first commercial works to feature 3D rendering along s…

CF1100F Ivan and Burgers

CF1100F Ivan and Burgers 靜態區間&#xff0c;選取任意個數使得它們的異或和最大 \(n,\ m\leq5\times10^5,\ a_i\in[0,\ 10^6]\) lxl ST表&#xff0c;線性基 如果暴力維護線性基&#xff0c;線段樹時間復雜度為 \(O(n\log^2n)-O(\log^3n)\) 由于重復元素對答案沒有影響&…

深入淺出Nintex——更新PeopleandGroup類型的Field

轉載于:https://www.cnblogs.com/mingle/archive/2011/11/25/2263199.html

從 vue-cli 源碼中,我發現了27行讀取 json 文件有趣的 npm 包

1. 前言大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。已進行四個月了&#xff0c;很多小伙伴表示收獲頗豐。想學源碼&#xff0c;極力推薦訂閱我寫…

自定義view示例_自定義404頁的10個示例(從最佳到最差)

自定義view示例自定義404頁面 (Custom 404 pages) To customize or not to customize your 404 page? I hope by now you know the answer is that, yes, under essentially all circumstances you should customize your 404 page. 404 errors occur when someone attempts t…

BTF:實踐指南

本文地址&#xff1a;BTF&#xff1a;實踐指南 | 深入淺出 eBPF 1. BPF 的常見限制 1.1 調試限制1.2 可移植性2. BTF 是什么&#xff1f;3. BTF 快速入門 3.1 BPF 快速入門3.1 BTF 和 CO-RE4. 結論 BPF 是 Linux 內核中基于寄存器的虛擬機&#xff0c;可安全、高效和事件驅動…

python 混入類MixIn

寫在前面 能把一件事情說的那么清楚明白&#xff0c;感謝廖雪峰的官方網站。 1.為什么要用混入類&#xff1f;&#xff08;小白入門&#xff09; 繼承是面向對象編程的一個重要的方式&#xff0c;因為通過繼承&#xff0c;子類就可以擴展父類的功能。 step1: 回憶一下Animal類層…

關于字符串流的學習(c++)

/* 字符串流 在字符數組中可以存放字符,也可以存放整數、浮點數以及其他類型的數據。在向字符數組存入數據之前,要先將數據從二進制形式轉換為ASCII代碼,然后存放在緩沖區,再從緩沖區送到字符數組。從字符數組讀數據時,先將字符數組中的數據送到緩沖區,在賦給變量前要先將ASCII…

估計很多前端都沒學過單元測試~

大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。已進行四個月了&#xff0c;很多小伙伴表示收獲頗豐。想學源碼&#xff0c;極力推薦訂閱我寫的《學習…

xd可以用ui動效效果嗎_通過動畫使UI設計栩栩如生:Adobe XD和After Effects

xd可以用ui動效效果嗎Note — If you don’t fancy splashing out on an Adobe license, you can trial their products for 14 days each. That should give you more than enough time to play, check it out.注意—如果您不愿意花錢購買Adobe許可證&#xff0c;則可以分別試…

BookMarklet:瑞士軍刀你用了嗎?

Bookmarklet 是一段隱藏在鏈接后面的js代碼&#xff0c;可以收藏在收藏夾。通過這段代碼&#xff0c;我們可以跨瀏覽器&#xff08;當然&#xff0c;也跨平臺&#xff09;實現一些工具。比起瀏覽器插件來說&#xff0c;使用更加方便。典型的&#xff0c;dict.cn 網站的工具和有…

第十二周編程總結

這個作業屬于那個課程C語言程序設計II這個作業要求在哪里https://pintia.cn/problem-sets/1127748174659035136/problems/1127749414029729792我在這個課程的目標是更好的學習函數這個作業在那個具體方面幫助我實現目鍛煉了我的編程能力參考文獻c語言程序設計26-1 計算最長的字…

可能是全網首個前端源碼共讀活動,誠邀加入學習

大家好&#xff0c;我是若川。從8月份到現在11月結束了。每周一期&#xff0c;一起讀200行左右的源碼&#xff0c;撰寫輔助文章&#xff0c;截止到現在整整4個月了。由寫有《學習源碼整體架構系列》20余篇的若川【若川視野公眾號號主】傾力組織&#xff0c;召集了各大廠對于源碼…

現代游戲中的UX趨勢

ux設計中的各種地圖游戲UX (GAMES UX) Even though websites and games have matured side-by-side over the past few decades, games have a long and detailed history of user experience. Sure, it was scrappy and fairly rudimentary initially, but the only way you c…

SQL Server 2008 安裝過程中遇到“性能計數器注冊表”..

Windows 2008 系統 SQL Server 2008 性能計數器注冊表作者&#xff1a; 來源&#xff1a; 時間&#xff1a;2010-6-13 完美集成、增強 KindEditor HTML 編輯器今天跟隨部門老大去現場學習&#xff0c;安裝 Windows208 下 SQL Server2008&#xff0c…

你提交代碼前沒有校驗?巧用gitHooks解決

大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。已進行四個月了&#xff0c;很多小伙伴表示收獲頗豐。想學源碼&#xff0c;極力推薦訂閱我寫的《學習…

Linux下自動化測試環境的搭建

1.安裝Linux虛擬機&#xff0c;詳情參考 https://blog.csdn.net/qq_22770715/article/details/78558374 https://www.cnblogs.com/Q277227/p/8176564.html 1.1 需要確定IP &#xff0c;使用 ifconfig 1.2 linux的用戶名跟密碼&#xff1b; 1.3 確定可以遠程ssh登錄&…

code craft_以Craft.io為先—關于我們行業的實踐職業道路的系列

code craft重點 (Top highlight)For the past two decades, digital product design / UX has been shifting to become a more strategic discipline within organizations. Partially because business leaders have started to pay attention to how design-driven companie…

Nginx+httpd反代實現動靜分離

什么是動靜分離為了提高網站的響應速度&#xff0c;減輕程序服務器&#xff08;apachephp&#xff0c;nginxphp等&#xff09;的負載&#xff0c;對于靜態資源比如圖片&#xff0c;js&#xff0c;css&#xff0c;html等靜態文件&#xff0c;我們可以在反向代理服務器中設置&…

(建議收藏)前端面試必問的十六條HTTP網絡知識體系

大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。已進行四個月了&#xff0c;很多小伙伴表示收獲頗豐。想學源碼&#xff0c;極力推薦訂閱我寫的《學習…