【小小前端】手寫一個很簡單的非同步程式設計解決方案Promise及其鏈式呼叫

.Ping發表於2020-01-20

前端非同步一直是個熱門話題,作為新一代非同步程式設計解決方案的Promise,相比傳統非同步程式設計,不僅解決了恐怖的回撥地獄,在寫法上也是相當方便簡潔。如此牛*的功能讓人不禁地想一探究竟,在各大網路公司面試中,手寫Promise也是常見的筆試題了,就讓我們一起來簡單探討一下Promise的實現吧!

【小小前端】手寫一個很簡單的非同步程式設計解決方案Promise及其鏈式呼叫

引子


什麼是Promise?

Promise 是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函式和事件——更合理和更強大。它由社群最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise物件。

Promise簡單用法

// 定義一個Promise物件
let promise = new Promise((resolve,reject) => {
    true&&resolve(true) || reject(false)
});

promise.then(res=>{
    console.log(res)
}).catch(err=>{
    throw err
})
複製程式碼

如此一個低階簡單的Promise就完成了。

正文


思考一下,需要實現哪些功能

  1. 定義一個Promise
  2. 在Promise內部,需要一個建構函式(constructor),建構函式有兩個引數:resolvereject,建構函式內部需要一個狀態變數state(pending、resolved、rejected),一個非同步成功的回參res
  3. 既然是Promise那必須得有then方法,有兩個引數分別是onFulfilledonRejected
  4. 有了then,自然有catchfinally等一系列。。。。。。

宣告一個Promise

既然是手寫,那麼我們直接將原來的Promise物件覆蓋,這裡採用ES6的class寫法。

宣告建構函式

  • Promise存在三個狀態(state)pending、fulfilled、rejected
  • pending(等待態)為初始態,並可以轉化為fulfilled(成功態)和rejected(失敗態)
  • 不管成功還是失敗,最終得到的res或者reason都無法改變
class Promise {
    // 建構函式
    constructor(executor){
        // 狀態控制state,預設值值為pending、成功resolved、失敗rejected
        this.state = 'pending';
        // 成功返回值
        this.res = '';
        // 失敗返回原因
        this.reason = '';
        // resolve方法
        let resolve = (res) => {
            // 如果狀態state為pending,將狀態變為resolved,並將返回值賦給res
            this.state === 'pending' && (this.state = 'resolved') && (this.res = res);
        };
        
        let reject = (reason) => {
            // 如果狀態state為pending,將狀態變為rejected,並返回錯誤原因
            this.state === 'pending' && (this.state = 'rejected') && (this.reason = reason);
        };
        
        // 如果executor報錯的話,直接執行reject
        try {
            executor(resolve,reject)
        } catch (error) {
            reject(error)
        }
    }
}
複製程式碼

實現then方法

then(onFulfilled,onRejected){
    // state為resolved,執行onFulfilled
    this.state === 'resolved' && (()=>{
        onFulfilled(this.res)
    })();
    // state為rejected,執行onRejected
    this.state === 'rejected' && (()=>{
        onRjected(this.reason)
    })();
}
複製程式碼

牛刀小試

此時,一個最基本最簡單最低階的Promise就完成了,讓我們先測試一下結果,隨機一個數,如果大於0.5則成功,如果小於則失敗。

let promise = new Promise((resolve,reject)=>{
    let random = Math.random();
    if(random>0.5){
        resolve(random)
    } else{
        reject(random)
    }
});

promise.then(res=>{
    console.log('成功:' + res)
},err=>{
    console.log('失敗:' + err)
})
複製程式碼

重新整理控制檯:

【小小前端】手寫一個很簡單的非同步程式設計解決方案Promise及其鏈式呼叫

【小小前端】手寫一個很簡單的非同步程式設計解決方案Promise及其鏈式呼叫

看起來沒什麼毛病,不過如果你認為這就結束了?

還是這該死的非同步

有人問,Promise不就是解決非同步的,怎麼又說到非同步,這裡要說的非同步就是指定時器了,比如setTimeout,為什麼這樣說,這就涉及到js的事件迴圈機制(eventLoop)了,這裡暫且不提。

先看一個例子:

let promise = new Promise((resolve,reject)=>{
    let random = Math.random();
    if(random>0.5){
        setTimeout(() => {
            resolve(random)
        }, 1000);
    } else{
        setTimeout(() => {
            reject(random)
        }, 1000);
    }
});

promise.then(res=>{
    console.log('成功:' + res)
},err=>{
    console.log('失敗:' + err)
})

複製程式碼

【小小前端】手寫一個很簡單的非同步程式設計解決方案Promise及其鏈式呼叫

此時,不管怎麼重新整理,控制檯就是不列印結果,不管正確還是錯誤,這是為什麼呢?

首先我們定義了一個Promise物件的例項,在例項中定義了一個定時器,一秒後返回,當執行到定時器之後,定時器掛起,繼續執行下面的promise.then,而此時因為定時器的緣故,不管是resolve還是reject都沒有執行,也就是說state的狀態任然沒有改變,所以在then方法中,沒有對應的項,如果再次執行then方法,同理依然無法執行,那麼問題來了,如何解決因為定時器導致的狀態未變化和多次執行then方法呢?

改造建構函式

首先為了多次執行then方法,我們定義兩個空陣列,分別存放成功或者失敗方法,然後在then方法中,如果狀態依然是pending,則將成功或者失敗的函式存入對應的陣列,當定時器時間過了之後,在resolve或者reject中迴圈對應陣列並執行函式。

constructor(executor){
    // 原先程式碼不變
    // ......
    // 新增陣列
    // 成功列表
    this.resolvedList = [];
    // 失敗列表
    this.rejectedList = [];
    // 改造一下resolve和reject
    let resolve = (res) => {
        this.state === 'pending' && (this.state = 'resolved') && (this.res = res);
        this.state === 'resolved' && this.resolvedList.forEach(fn => {
            fn();
        })
    };
    // 失敗
    let reject = (reason) => {
        this.state === 'pending' && (this.state = 'rejected') && (this.reason = reason);
        this.state === 'rejected' && this.rejectedList.forEach(fn => {
            fn();
        })
    };
}
複製程式碼

改造then方法

由於定時器的原因,造成先執行then,那麼在then中將對應函式存起:

then(onFulfilled,onRejected){
    this.state === 'pending' && (() =>{
        this.resolvedList.push(()=>{
            console.log('resolvedList.push')
            onFulfilled(this.res);
        });
        this.rejectedList.push(()=>{
            onRejected(this.reason)
        });
        return
    })()
    // state為resolved,執行onFulfilled
    this.state === 'resolved' && (()=>{
        onFulfilled(this.res)
    })();
    // state為rejected,執行onRejected
    this.state === 'rejected' && (()=>{
        onRjected(this.reason)
    })();
}
複製程式碼

呼叫then方法

promise.then(
    res => {
        console.log("成功1:" + res);
    },
    err => {
        console.log("失敗1:" + err);
    }
);
promise.then(
    res => {
        console.log("成功2:" + res);
    },
    err => {
        console.log("失敗2:" + err);
    }
);
複製程式碼

重新整理控制檯:

【小小前端】手寫一個很簡單的非同步程式設計解決方案Promise及其鏈式呼叫

此時控制檯已經可以正常列印。

then方法的鏈式呼叫

為了解決回撥地獄,在Promise中,我們是這樣的寫法:new Promise().then().then(),道理其實很簡單,第一個then方法返回的值也是一個Promise物件,那麼就能繼續使用then方法。

當我們在第一個then中return了一個引數(引數未知,需判斷)。這個return出來的新的promise就是onFulfilled()或onRejected()的值:

  • 返回值是普通值如:數值、字串等,直接將其作為callBackPromise成功的結果
  • 如果是一個Promise,則取其結果作為callBackPromise成功的結果

一個判斷返回值的函式callBackPromise

  • res 不能是null
  • 宣告瞭then,如果取then報錯,則走reject()
function callBackFunction(callBackPromise,res, resolve, reject){
    // 判斷callBackPromise === res ,如果等於會造成死迴圈
    if(callBackPromise === res){
        return reject(new TypeError('注意死迴圈了!'));
    }
    // 防止重複呼叫
    let reCall;
    if(res !== null && (typeof res === 'Object' || typeof res === 'function')){
        try {
            // 鏈式,呼叫上一次結果的then
            let then = res.then;
            typeof then === 'function' && then.call(res, nextRes => {
                if(reCall){
                    return 
                }
                reCall = true;
                callBackPromise(callBackPromise,nextRes,resolve,reject)
            },err => {
                if(reCall)
                return;
                reject(err);
            });
            resolve(res)
        }catch(e){
            if(reCall)
            return;
            reject(e)
        }
    }else{
        reject(res)
        // resolve(res)
    }
}
複製程式碼

繼續改造then方法

then(onFulfilled,onRejected){
    // 注意此處用到的var,如果用let會造成callBackFunction報錯,原因是let沒有變數提升
    var callBackPromise = new Promise((resolve,reject) => {
        this.state === 'pending' && (() =>{
            this.resolvedList.push(()=>{
                let res = onFulfilled(this.res);
                callBackFunction(callBackPromise,res,resolve,reject)
            });
            this.rejectedList.push(()=>{
                let res = onRejected(this.reason)
                callBackFunction(callBackPromise,res,resolve,reject)
            });
            return
        })()
        this.state === 'resolved' && (()=>{
            let res = onFulfilled(this.res);
            callBackFunction(callBackPromise,res,resolve,reject)
        })();
        this.state === 'rejected' && (()=>{
            let res = onRejected(this.reason);
            callBackFunction(callBackPromise,res,resolve,reject)
        })();
    })
    return callBackPromise;
}
複製程式碼

測試一下,如果列印為空多重新整理幾次,因為沒有實現catch方法:

promise.then(res => {
    console.log('第一次回撥',res)
    return '第一次返回'
})
.then(res2=>{
    console.log('第二次回撥',res2)
    return '第二次回撥'
})
.then(res3=>{
    console.log('第三次回撥',res3)
})
複製程式碼

【小小前端】手寫一個很簡單的非同步程式設計解決方案Promise及其鏈式呼叫

可以看到此時已經可以進行鏈式操作。

後記

到此,一個基本的鏈式呼叫已經完成,然而Promise的博大精深遠不及此,諸如onFulfilled,onRejected的可選性,catch、finally、all等方法的實現都還沒有完成,需要不斷完善,如果上述程式碼中有錯誤的還請各位大佬指出,輕噴。

【小小前端】手寫一個很簡單的非同步程式設計解決方案Promise及其鏈式呼叫

參考連結

相關文章