大廠面試題手寫Promise源碼


手寫Promise源碼幾乎是每個大廠面試要求必會的一個考點,每次聽到源碼,總有一種讓人上頭的感覺,因為自己從來沒有實現過,總感覺這東西很難實現,最近再為跳槽做准備,從頭寫學了一下基礎知識,手寫了call源碼、apply源碼、Promise源碼,感覺還挺有意思,不是想想的那么難。就是一個js的簡答實現。只要優點js基礎的人都能手寫出來,所以不要一聽“源碼”二字就給嚇到。自己動手實現一遍,比看別人的幾十遍的效果更好。

本篇文章從實際應用角度思考Promise是怎樣的一個實現過程,會先從簡單的應用出發,然后一點一點去完善整個代碼。

先來一個簡單的例子:

//剛開始是等待態,pending
let promise = new Promise( (resolve,reject) =>{if(err) return reject(err) //失敗了返回失敗信息 失敗態
        resolve(data) //成功了返回數據 成功態 }) //狀態改變了調用
promise.then(data=>{ //成功了調用
 console.log(data) },err=>{             //失敗了調用
 console.log(err) })

這是一個promise實例,它有三種狀態,pending(等待態)、fullfilled(成功態)、rejected(失敗態),resolve為成功時調用,狀態由等待態變為成功態,reject為失敗,狀態由等待態變為失敗態。

當狀態改變的了的時候會執行then方法,如果是成功就輸出值成功的值,如果是失敗就返回失敗的原因。但是他們兩個只能調用一個,不可能有即成功由失敗的情況。

根據這個實例我們就可以實現一版簡單的功能。

第一版:定義整個流程,實現同步功能:

這一版我們主要實現流程能走通,能執行同步任務

class MyPromise{ constructor(executorCallback){ //executorCallback執行的回調函數
        let _this = this; _this.status = 'pending'; //記錄狀態
        _this.value = ''; //記錄返回的數據
        function resolveFn(result){ if(_this.status === 'pending'){ //狀態只能是從pending到成功
                _this.status = 'resolved' _this.value = result } } function rejectFn(reason){ if(_this.status === 'pending'){ //狀態只能是從pending到失敗
                _this.status = 'rejected' _this.value= reason } }
 executorCallback(resolveFn,rejectFn) } //判斷狀態成功還是失敗,成功了執行onFullfiled,失敗了執行onRejected
 then(onFullfiled,onRejected) { if(this.status === 'resolved'){ onFullfiled(this.data) } if(this.status === 'rejected'){ onRejected(this.err) } } }
1.定義_this保證每次都能獲取到正確的this
2.定義一個常量status用來記錄promise的狀態
3.定義一個常量value用來保存執行結果的返回值
4.executorCallback是立即執行函數,需要我們手動傳入兩個參數,這兩個參數代表成功和失敗時候調用,需要我們在內部定義好。
5.resolveFn()和rejectedFn()函數,在執行前先判斷一下狀態,如果狀態為pending就執行,並且讓狀態變為相應的成功態或者失敗態,這樣每次就只能執行一個,要么是resolveFn(),
要么是rejectedFn(),並且把相應的返回值賦值給value。
6.狀態改變了之后就會調用Promise.then()方法,如果狀態是成功態就執行onFullfilled(),如果狀態是失敗態,就執行onRejected()
到此基本流程通了,我們寫個小案例測一下
可以看到,雖然我寫了兩個函數,但是它只執行了第一個,就說明當狀態pending變為成功態之后不會再去執行失敗的函數,同理當狀態變為失敗態之后也不會再去執行成功的函數,
現在雖然實現了可以執行同步任務,但是對於異步任務還是執行不了。例如

 可以看到控制台沒有輸出任何東西,現在我們就來解決一下如何實現異步任務。

第二版:實現異步功能

我們先來分析一下上一版為什么實現不了異步功能,當代碼執行到setTimeout時,不會立即執行里面的函數,而是先放到一個異步調用棧里面,等到同步代碼執行完了在執行里面的resolveFn函數,這個時候then已經執行完了,不會再執行,所以就不會輸出任何東西,我們可以在resolveFn執行之前就將then的回調函數先保存起來,等到resolveFn執行的時候再去一個一個執行這些回調函數,這個時候就可以實現異步功能。

在原來的基礎上修改

class myPromise{ constructor(executorCallback){ var _this = this _this.status = 'pending' _this.value  _this.onFullfilledCallback = [] //存放成功時的回調函數 _this.onRejectedCallback = [] //存放時的失敗的回調函數
        function resolveFn (result) { let timer = setTimeout( () =>{ //異步任務 clearTimeout(timer) // console.log('chengg')
                if(_this.status === 'pending'){ _this.status = 'resolved' _this.value = result _this.onFullfilledCallback .forEach(item =>item(_this.value)); } }) } function rejectFn (err) { let timer = setTimeout( () =>{ //異步任務 clearTimeout(timer) if(_this.pending === 'pending'){ _this.status = 'rejected' _this.value = err _this.onRejectedCallback.forEach(item =>item(_this.value)) } }) } executorCallback(resolveFn,rejectFn) } then(onFullfiled,onRejected){ if(this.status === 'pending') { this.onFullfilledCallback .push(onFullfiled) this.onRejectedCallback.push(onRejected) } } }

我將修改了的部分用紅色字體標出。

1.onResolvedCallback和onRejectedCallback用來存放成功和失敗時候的回調函數,想想為什么是一個數組呢?我們前面分析過,第一個原因是同一個Promise實例可以調用多次then,
需要把這些方法都放在同一個數組里,例如
let p1 = new Promise( (resolve,reject) =>{ setTimeout(function(){ resolve('ok') },1000) }) p1.then(result =>{ console.log('result1:'+result) },reason =>{ console.log(reason) }) p1.then(result =>{ console.log('result2:'+result) },reason =>{ console.log(reason) })

 第二個原因是當立即執行完 Promise 時,讓它的狀態還是pending的時候,應該把 then 中的回調保存起來,當執行成功或者失敗,狀態改變時再執行

 
        

 

2.resolveFn()和rejected()這里為什么要用setTimeout將它變為異步執行呢?因為如果不用setTimeou這種方式的話,若Promise里面的代碼是同步代碼,在執行到reject或者resolve的時候,還沒有執行then,所以數組里還沒有值,這個時候調用的話不會報錯但是不會輸出任何結果,用setTimeout轉為異步的話,會先去執行then方法,將回調收集到數組里,然后再去執行異步任務,這個時候就有值了。舉例子:

 此時紅色方框內的是同步代碼,會先執行,不會輸出任何東西,當然如果把紅色方框內的變為異步代碼就不會有這個問題了。但是我們要同時兼顧同步和異步都存在的情況。

3.then方法:

then(onFullfiled,onRejected){ if(this.status === 'pending') { this.onFullfilledCallback .push(onFullfiled) this.onRejectedCallback.push(onRejected) } }

then很簡單,就是在狀態為pending的時候將回調函數收集到數組里面,到此異步功能就差不多了,我們寫個例子試一下。

let p1 = new myPromise((resolve,reject) =>{ setTimeout(function(){ resolve('第一次成功') },1000) }) p1.then(result =>{ console.log('result:'+result) },reason =>{ console.log('reason:'+reason) })

 完美。

但是這個第二版還不能實現鏈式調用,在工作中我們經常通過promis.then().then()這樣的方式來解決回調地獄,如下圖。

 接下來我們就實現鏈式調用

第三版:實現鏈式調用

首先分析,Promise為什么可以實現鏈式調用,因為Promise.then()方法它返回的是一個新的Promise實例,將這個新的Promise實例的返回值傳遞到下一個then中,作為下次onFullfilled()或者onRejected()的值。那這個新的Promise的返回值會有哪幾種情況呢?我們來分析一下

 

1.如果返回的是一個普通值就直接執行成功,resolve(x)
2.如果返回的是一個promise實例,就繼續new
3.如果出錯了,就執行失敗reject(x)
4.如果參數是null,就會輸出undefined
所以要對返回值進行解析。
改寫then方法。
then(onFulfiled,onRejected){ // 聲明返回的promise2 
        let promise2 = new myPromise((resolveFn, rejectFn)=>{ if (this.status === 'fulfilled') { let x = onFulfiled(this.value); // resolvePromise函數,處理自己return的promise和默認的promise2的關系 
 resolvePromise(promise2, x, resolveFn, rejectFn); }; if (this.status === 'rejected') { let x = onRejected(this.reason); resolvePromise(promise2, x, resolveFn, rejectFn); }; if (this.status === 'pending') { this.onFullfilledCallback.push(()=>{ let x = onFulfiled(this.value); resolvePromise(promise2, x, resolveFn, rejectFn); }) this.onRejectedCallback.push(()=>{ let x = onRejected(this.reason); resolvePromise(promise2, x, resolveFn, rejectFn); }) } }); // 返回promise,完成鏈式 
        return promise2; }

我們首先定義一個新的Promise實例,在這個實例內部需要判斷狀態,當狀態不是等待態時,就去執行相對應的函數。如果狀態是等待態的話,就往回調函數中 push 函數。將then回調函數的返回值記為x,由於這個返回值可能有多種情況,所以需要對各種情況進行解析。所以另外封裝一個方法resolvePromise(),接下來我們就來封裝一下這個函數。

function resolvePromise(promise2, x, resolve, reject){ // 循環引用報錯 
                if(x === promise2){ // reject報錯 
                    return reject(new TypeError('Chaining cycle detected for promise')); } let called; //控制調用次數
                // x不是null 且x是對象或者函數 
                if (x !== null && (typeof x === 'object' || typeof x === 'function')) { try { // PromiseA+規定,聲明then 等於 x的then方法 
                        let then = x.then; // 如果then是函數,就默認是promise了 
                        if (typeof then === 'function') { // 就讓then執行 第一個參數是this, 后面是成功的回調 和 失敗的回調 
                            then.call(x, y => { // 成功和失敗只能調用一個 
                                if (called) return; called = true; // resolve的結果依舊是promise 那就繼續解析 
 resolvePromise(promise2, y, resolve, reject); }, err => { // 成功和失敗只能調用一個 
                                if (called) return; called = true; reject(err); }) } else { //如果不是函數,是普通對象直接resolve
                            resolve(x); // 直接成功即可 
 } } catch (e) { // 如果在執行的過程中報錯了,就被then的失敗捕獲 
                        if (called) return; called = true;       // 取then出錯了那就不要在繼續執行了 
 reject(e); } } else { //如果是普通值 
 resolve(x); } }
1.PromiseA+規定 x 不能與 promise2 相等,這樣會發生循環引用的問題
2.定義一個called來控制調用次數,成功和失敗只能調用一個,一旦調用完就將called = true,防止下一個再調用。
3.接着判斷x的類型,如果不是對象或者函數,那就是普通值,直接resolve(x)
4.如果是對象或者函數,將x.then賦值給then,如果then是函數,就讓then執行回調函數。如果下一次的執行結果還是一個Promise,就接着處理。如果then是個普通對象,就直接執行
resolve方法
5.再執行這段代碼的過程中可能會發生異常,我們用try catch去捕獲錯誤,如有錯誤,就直接執行reject方法。
來測試一下是否可以實現鏈式調用了:

 沒毛病。

到此,Promise的核心功能都已經完成了,Promise還有一些其他的方法,all、race、resolve、reject,相信理解了上面的封裝流程,大概就知道怎么封裝了,當然前提是要知道這些方法的用法。接下來就看一下這些方法的實現。

Promise其他方法的實現

簡單說一下,all方法接收一個數組,等到數組里面的所有Promise實例的狀態都變成成功態之后才成功,但只要有一個失敗了就返回失敗。race方法是哪個先成功就先返回哪個,resolve和reject是分別執行成功和是失敗。代碼如下

 

//all 獲取所有的promise,都執行then,把結果放到數組,一起返回
    static all(promiseArr =[]) { return new Promise((resolve,reject) =>{ let index = 0; let arr = [] for(let i =0; i<promiseArr.length; i++){ promiseArr[i].then(result =>{ index++ arr[i] = result if(index === arr.length){ resolve(arr) } },reason =>{ reject(reason) }) } }) } //誰先執行先返回誰
 static race (promises){ return new Promise((resolve,reject)=>{ for(let i=0;i<promises.length;i++){ promises[i].then(resolve,reject) }; }) } //resolve方法 
 static resolve(result){ return new Promise((resolve,reject)=>{ resolve(result) }); } //reject方法 
 static reject (reason){ return new Promise((resolve,reject)=>{ reject(reason) }); } 

 

測試:

          

 

 Promise源碼就全部已經完成。閱讀源碼對我們思維能力會有很大的提升的,希望大家不要都可以動手實現一下,相信會有不少收獲。

 

 

 

 


































 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM