摘要:傳入的回調函數也不是一個函數類型,那怎么辦規范中說忽略它就好了。因此需要判斷一下回調函數的類型,如果明確是個函數再執行它。
Promise是什么
所謂Promise,簡單說就是一個容器,里面保存著某個未來才會結束的事件(通常是一個異步操作)的結果。從語法上說,Promise 是一個對象,從它可以獲取異步操作的消息。Promise 提供統一的 API,各種異步操作都可以用同樣的方法進行處理。
Promise是處理異步編碼的一個解決方案,在Promise出現以前,異步代碼的編寫都是通過回調函數來處理的,回調函數本身沒有任何問題,只是當多次異步回調有邏輯關系時就會變得復雜:
const fs = require("fs"); fs.readFile("1.txt", (err,data) => { fs.readFile("2.txt", (err,data) => { fs.readFile("3.txt", (err,data) => { //可能還有后續代碼 }); }); });
上面讀取了3個文件,它們是層層遞進的關系,可以看到多個異步代碼套在一起不是縱向發展的,而是橫向,不論是從語法上還是從排錯上都不好,于是Promise的出現可以解決這一痛點。
上述代碼如果改寫成Promise版是這樣:
const util = require("util"); const fs = require("fs"); const readFile = util.promisify(fs.readFile); readFile("1.txt") .then(data => { return readFile("2.txt"); }).then(data => { return readFile("3.txt"); }).then(data => { //... });
可以看到,代碼是從上至下縱向發展了,更加符合人們的邏輯。
下面手寫一個Promise,按照Promises/A+規范,可以參照規范原文:
Promises/A+規范
手寫實現Promise是一道前端經典的面試題,比如美團的面試就是必考題,Promise的邏輯還是比較復雜的,考慮的邏輯也比較多,下面總結手寫Promise的關鍵點,和怎樣使用代碼來實現它。
Promise代碼基本結構實例化Promise對象時傳入一個函數作為執行器,有兩個參數(resolve和reject)分別將結果變為成功態和失敗態。我們可以寫出基本結構
function Promise(executor) { this.state = "pending"; //狀態 this.value = undefined; //成功結果 this.reason = undefined; //失敗原因 function resolve(value) { } function reject(reason) { } } module.exports = Promise;
其中state屬性保存了Promise對象的狀態,規范中指明,一個Promise對象只有三種狀態:等待態(pending)成功態(resolved)和失敗態(rejected)。
當一個Promise對象執行成功了要有一個結果,它使用value屬性保存;也有可能由于某種原因失敗了,這個失敗原因放在reason屬性中保存。
每一個Promise實例都有一個then方法,它用來處理異步返回的結果,它是定義在原型上的方法,我們先寫一個空方法做好準備:
Promise.prototype.then = function (onFulfilled, onRejected) { };當實例化Promise時會立即執行
當我們自己實例化一個Promise時,其執行器函數(executor)會立即執行,這是一定的:
let p = new Promise((resolve, reject) => { console.log("執行了"); });
運行結果:
執行了
因此,當實例化Promise時,構造函數中就要馬上調用傳入的executor函數執行
function Promise(executor) { var _this = this; this.state = "pending"; this.value = undefined; this.reason = undefined; executor(resolve, reject); //馬上執行 function resolve(value) {} function reject(reason) {} }已經是成功態或是失敗態不可再更新狀態
規范中規定,當Promise對象已經由pending狀態改變為了成功態(resolved)或是失敗態(rejected)就不能再次更改狀態了。因此我們在更新狀態時要判斷,如果當前狀態是pending(等待態)才可更新:
function resolve(value) { //當狀態為pending時再做更新 if (_this.state === "pending") { _this.value = value;//保存成功結果 _this.state = "resolved"; } } function reject(reason) { //當狀態為pending時再做更新 if (_this.state === "pending") { _this.reason = reason;//保存失敗原因 _this.state = "rejected"; } }
以上可以看到,在resolve和reject函數中分別加入了判斷,只有當前狀態是pending才可進行操作,同時將成功的結果和失敗的原因都保存到對應的屬性上。之后將state屬性置為更新后的狀態。
then方法的基本實現當Promise的狀態發生了改變,不論是成功或是失敗都會調用then方法,所以,then方法的實現也很簡單,根據state狀態來調用不同的回調函數即可:
Promise.prototype.then = function (onFulfilled, onRejected) { if (this.state === "resolved") { //判斷參數類型,是函數執行之 if (typeof onFulfilled === "function") { onFulfilled(this.value); } } if (this.state === "rejected") { if (typeof onRejected === "function") { onRejected(this.reason); } } };
需要一點注意,規范中說明了,onFulfilled 和 onRejected 都是可選參數,也就是說可以傳也可以不傳。傳入的回調函數也不是一個函數類型,那怎么辦?規范中說忽略它就好了。因此需要判斷一下回調函數的類型,如果明確是個函數再執行它。
讓Promise支持異步代碼寫到這里似乎基本功能都實現了,可是還有一個很大的問題,目前此Promise還不支持異步代碼,如果Promise中封裝的是異步操作,then方法無能為力:
let p = new Promise((resolve, reject) => { setTimeout(() => { resolve(1); },500); }); p.then(data => console.log(data)); //沒有任何結果
運行以上代碼發現沒有任何結果,本意是等500毫秒后執行then方法,哪里有問題呢?原因是setTimeout函數使得resolve是異步執行的,有延遲,當調用then方法的時候,此時此刻的狀態還是等待態(pending),因此then方法即沒有調用onFulfilled也沒有調用onRejected。
這個問題如何解決?我們可以參照發布訂閱模式,在執行then方法時如果還在等待態(pending),就把回調函數臨時寄存到一個數組里,當狀態發生改變時依次從數組中取出執行就好了,清楚這個思路我們實現它,首先在類上新增兩個Array類型的數組,用于存放回調函數:
function Promise(executor) { var _this = this; this.state = "pending"; this.value = undefined; this.reason = undefined; this.onFulfilledFunc = [];//保存成功回調 this.onRejectedFunc = [];//保存失敗回調 //其它代碼略... }
這樣當then方法執行時,若狀態還在等待態(pending),將回調函數依次放入數組中:
Promise.prototype.then = function (onFulfilled, onRejected) { //等待態,此時異步代碼還沒有走完 if (this.state === "pending") { if (typeof onFulfilled === "function") { this.onFulfilledFunc.push(onFulfilled);//保存回調 } if (typeof onRejected === "function") { this.onRejectedFunc.push(onRejected);//保存回調 } } //其它代碼略... };
寄存好了回調,接下來就是當狀態改變時執行就好了:
function resolve(value) { if (_this.state === "pending") { _this.value = value; //依次執行成功回調 _this.onFulfilledFunc.forEach(fn => fn(value)); _this.state = "resolved"; } } function reject(reason) { if (_this.state === "pending") { _this.reason = reason; //依次執行失敗回調 _this.onRejectedFunc.forEach(fn => fn(reason)); _this.state = "rejected"; } }
至此,Promise已經支持了異步操作,setTimeout延遲后也可正確執行then方法返回結果。
鏈式調用Promise處理異步代碼最強大的地方就是支持鏈式調用,這塊也是最復雜的,我們先梳理一下規范中是怎么定義的:
每個then方法都返回一個新的Promise對象(原理的核心)
如果then方法中顯示地返回了一個Promise對象就以此對象為準,返回它的結果
如果then方法中返回的是一個普通值(如Number、String等)就使用此值包裝成一個新的Promise對象返回。
如果then方法中沒有return語句,就視為返回一個用Undefined包裝的Promise對象
若then方法中出現異常,則調用失敗態方法(reject)跳轉到下一個then的onRejected
如果then方法沒有傳入任何回調,則繼續向下傳遞(值的傳遞特性)。
規范中說的很抽像,我們可以把不好理解的點使用代碼演示一下。
其中第3項,如果返回是個普通值就使用它包裝成Promise,我們用代碼來演示:
let p =new Promise((resolve,reject)=>{ resolve(1); }); p.then(data=>{ return 2; //返回一個普通值 }).then(data=>{ console.log(data); //輸出2 });
可見,當then返回了一個普通的值時,下一個then的成功態回調中即可取到上一個then的返回結果,說明了上一個then正是使用2來包裝成的Promise,這符合規范中說的。
第4項,如果then方法中沒有return語句,就視為返回一個用Undefined包裝的Promise對象
let p = new Promise((resolve, reject) => { resolve(1); }); p.then(data => { //沒有return語句 }).then(data => { console.log(data); //undefined });
可以看到,當沒有返回任何值時不會報錯,沒有任何語句時實際上就是return undefined;即將undefined包裝成Promise對象傳給下一個then的成功態。
第6項,如果then方法沒有傳入任何回調,則繼續向下傳遞,這是什么意思呢?這就是Promise中值的穿透,還是用代碼演示一下:
let p = new Promise((resolve, reject) => { resolve(1); }); p.then(data => 2) .then() .then() .then(data => { console.log(data); //2 });
以上代碼,在第一個then方法之后連續調用了兩個空的then方法 ,沒有傳入任何回調函數,也沒有返回值,此時Promise會將值一直向下傳遞,直到你接收處理它,這就是所謂的值的穿透。
現在可以明白鏈式調用的原理,不論是何種情況then方法都會返回一個Promise對象,這樣才會有下個then方法。
搞清楚了這些點,我們就可以動手實現then方法的鏈式調用,一起來完善它:
Promise.prototype.then = function (onFulfilled, onRejected) { var promise2 = new Promise((resolve, reject) => { //代碼略... } return promise2; };
首先,不論何種情況then都返回Promise對象,我們就實例化一個新promise2并返回。
接下來就處理根據上一個then方法的返回值來生成新Promise對象,由于這塊邏輯較復雜且有很多處調用,我們抽離出一個方法來操作,這也是規范中說明的:
/** * 解析then返回值與新Promise對象 * @param {Object} promise2 新的Promise對象 * @param {*} x 上一個then的返回值 * @param {Function} resolve promise2的resolve * @param {Function} reject promise2的reject */ function resolvePromise(promise2, x, resolve, reject) { //... }
resolvePromise方法用來封裝鏈式調用產生的結果,下面我們分別一個個情況的寫出它的邏輯,首先規范中說明,如果promise2和 x 指向同一對象,就使用TypeError作為原因轉為失敗。原文如下:
If promise and x refer to the same object, reject promise with a TypeError as the reason.
這是什么意思?其實就是循環引用,當then的返回值與新生成的Promise對象為同一個(引用地址相同),則會拋出TypeError錯誤:
let promise2 = p.then(data => { return promise2; });
運行結果:
TypeError: Chaining cycle detected for promise #
很顯然,如果返回了自己的Promise對象,狀態永遠為等待態(pending),再也無法成為resolved或是rejected,程序會死掉,因此首先要處理它:
function resolvePromise(promise2, x, resolve, reject) { if (promise2 === x) { reject(new TypeError("Promise發生了循環引用")); } }
接下來就是分各種情況處理。當x就是一個Promise,那么就執行它,成功即成功,失敗即失敗。若x是一個對象或是函數,再進一步處理它,否則就是一個普通值:
function resolvePromise(promise2, x, resolve, reject) { if (promise2 === x) { reject(new TypeError("Promise發生了循環引用")); } if (x !== null && (typeof x === "object" || typeof x === "function")) { //可能是個對象或是函數 } else { //否則是個普通值 resolve(x); } }
此時規范中說明,若是個對象,則嘗試將對象上的then方法取出來,此時如果報錯,那就將promise2轉為失敗態。原文:
If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
function resolvePromise(promise2, x, resolve, reject) { //代碼略... if (x !== null && (typeof x === "object" || typeof x === "function")) { //可能是個對象或是函數 try { let then = x.then;//取出then方法引用 } catch (e) { reject(e); } } else { //否則是個普通值 resolve(x); } }
多說幾句,為什么取對象上的屬性有報錯的可能?Promise有很多實現(bluebird,Q等),Promises/A+只是一個規范,大家都按此規范來實現Promise才有可能通用,因此所有出錯的可能都要考慮到,假設另一個人實現的Promise對象使用Object.defineProperty()惡意的在取值時拋錯,我們可以防止代碼出現Bug。
此時,如果對象中有then,且then是函數類型,就可以認為是一個Promise對象,之后,使用x作為this來調用then方法。
If then is a function, call it with x as this
//其他代碼略... if (x !== null && (typeof x === "object" || typeof x === "function")) { //可能是個對象或是函數 try { let then = x.then; if (typeof then === "function") { //then是function,那么執行Promise then.call(x, (y) => { resolve(y); }, (r) => { reject(r); }); } else { resolve(x); } } catch (e) { reject(e); } } else { //否則是個普通值 resolve(x); }
這樣鏈式寫法就基本完成了。但是還有一種極端的情況,如果Promise對象轉為成功態或是失敗時傳入的還是一個Promise對象,此時應該繼續執行,直到最后的Promise執行完。
p.then(data => { return new Promise((resolve,reject)=>{ //resolve傳入的還是Promise resolve(new Promise((resolve,reject)=>{ resolve(2); })); }); })
此時就要使用遞歸操作了。
規范中原文如下:
If a promise is resolved with a thenable that participates in a circular thenable chain, such that the recursive nature of [[Resolve]](promise, thenable) eventually causes [[Resolve]](promise, thenable) to be called again, following the above algorithm will lead to infinite recursion. Implementations are encouraged, but not required, to detect such recursion and reject promise with an informative TypeError as the reason.
很簡單,把調用resolve改寫成遞歸執行resolvePromise方法即可,這樣直到解析Promise成一個普通值才會終止,即完成此規范:
//其他代碼略... if (x !== null && (typeof x === "object" || typeof x === "function")) { //可能是個對象或是函數 try { let then = x.then; if (typeof then === "function") { let y = then.call(x, (y) => { //遞歸調用,傳入y若是Promise對象,繼續循環 resolvePromise(promise2, y, resolve, reject); }, (r) => { reject(r); }); } else { resolve(x); } } catch (e) { reject(e); } } else { //是個普通值,最終結束遞歸 resolve(x); }
到此,鏈式調用的代碼已全部完畢。在相應的地方調用resolvePromise方法即可。
最后的最后其實,寫到此處Promise的真正源碼已經寫完了,但是距離100分還差一分,是什么呢?
規范中說明,Promise的then方法是異步執行的。
onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
ES6的原生Promise對象已經實現了這一點,但是我們自己的代碼是同步執行,不相信可以試一下,那么如何將同步代碼變成異步執行呢?可以使用setTimeout函數來模擬一下:
setTimeout(()=>{ //此處的代碼會異步執行 },0);
利用此技巧,將代碼then執行處的所有地方使用setTimeout變為異步即可,舉個栗子:
setTimeout(() => { try { let x = onFulfilled(value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } },0);
好了,現在已經是滿分的Promise源碼了。
滿分的測試好不容易寫好的Promise源碼,最終是否真的符合Promises/A+規范,開源社區提供了一個包用于測試我們的代碼:promises-aplus-tests
這個包的使用方法不在詳述,此包可以一項項的檢查我們寫的代碼是否合規,如果有任一項不符就會給我們報出來,如果檢查你的代碼一路都是綠色,那恭喜,你的Proimse已經合法了,可以上線提供給別人使用了:
872項測試通過!
現在源碼都會寫,終于可以自信的回答面試官的問題了。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/97213.html
摘要:如果實現滿足所有要求,則實現可能允許。本條款允許使用特定于實現的方法來采用已知一致承諾的狀態。接下來根據規范進行手寫實現注釋偷懶就將對應的規范標注出來,其實基本上就是對著規范實現。 如果要手寫實現promise,那么先看看promise/A+規范,再來實現,將會事半功倍。那么我先翻譯一下Promise/A+規范中的內容。 術語 1.1 promise 是一個帶有符合此規范的the...
摘要:今天我們來自己手寫一個符合規范的庫。是異步編程的一種解決方案,比傳統的解決方案回調函數和事件更合理和更強大。我們可以看到,其實就是一個構造函數。所以說我們的數組里存的是一個一個的的回調函數,也就是一個一個。 今天我們來自己手寫一個符合PromiseA+規范的Promise庫。大家是不是很激動呢?? showImg(https://segmentfault.com/img/bV6t4Z?...
摘要:手寫一款符合規范的長篇預警有點長,可以選擇性觀看。初始狀態是,狀態可以有或者不能從轉換為或者從轉換成即只要由狀態轉換為其他狀態后,狀態就不可變更。 手寫一款符合Promise/A+規范的Promise 長篇預警!有點長,可以選擇性觀看。如果對Promise源碼不是很清楚,還是推薦從頭看,相信你認真從頭看到尾,并且去實際操作了,肯定會有收獲的。主要是代碼部分有點多,不過好多都是重復的,不...
摘要:本文同時也發布在我的博客上,歡迎之前也手寫過簡單的,這次則是為了通過官方的測試集,借鑒了一些下載量較多的,改了幾遍,終于是通過了規范的個測試用例如何測試測試庫地址在這,大家在寫完自己的后,不妨也去測試一下,檢驗自己的是否符合規范。 本文同時也發布在我的github博客上,歡迎star~ 之前也手寫過簡單的promise,這次則是為了通過官方的Promise A+測試集,借鑒了一些下載量...
摘要:使用及原理分析通過關鍵字創建實例接受一個參數方法返回兩個方法可用通過在方法中通過調用使成功或調用使失敗來控制狀態中可以執行同步代碼也可以執行異步代碼原型對象上有方法供實例調用方法接受兩個參數默認為一個函數默認為一個函數當狀態為時執行用戶傳入 promise使用及原理分析: 通過new關鍵字創建promise實例, 接受一個executor參數, executor方法返回兩個方法 res...
閱讀 2572·2021-09-23 11:21
閱讀 1882·2021-09-22 15:15
閱讀 970·2021-09-10 11:27
閱讀 3440·2019-08-30 15:54
閱讀 653·2019-08-30 15:52
閱讀 1335·2019-08-30 15:44
閱讀 2349·2019-08-29 15:06
閱讀 2972·2019-08-28 18:21