站住,你這個Promise!

前排佔樓

個人開源項目 — Vchat 正式上線了,歡迎各位小哥哥小姐姐體驗。如果覺得還行的話,記得給個star喲 ^_^。

言歸正傳,你經歷過絕望嗎?

衆所周知,js是單線程異步機制的。這樣就會導致很多異步處理會嵌套很多的回調函數,最爲常見的就是ajax請求,我們需要等請求結果返回後再進行某些操作。如:
    function success(data, status) {
        console.log(data)
    }
    function fail(err, status) {
        console.log(err)
    }
    ajax({
        url: myUrl,
        type: 'get',
        dataType: 'json',
        timeout: 1000,
        success: success(data, status),
        fail: fail(err, status)
    })
乍一看還行啊,不夠絕望啊,讓絕望來的更猛烈一些吧!那麼試想一下,如果還有多個請求依賴於上一個請求的返回值呢?五個?六個?代碼就會變得非常冗餘和不易維護。這種現象,我們一般親切地稱它爲‘回調地獄’。現在解決回調地獄的手段有很多,比如非常方便的async/await、Promise等。

我們現在要講的是Promise。在如今的前端面試中,Promise簡直是考點般的存在啊,十個有九個會問。那麼我們如何真正的弄懂Promise呢?俗話說的好,‘想要了解它,先要接近它,再慢慢地實現它’。自己實現一個Promise,不就什麼都懂了。

其實網絡上關於Promise的文章有很多,我也查閱了一些相關文章,文末有給出相關原文鏈接。所以本文側重點是我在實現Promise過程中的思路以及個人的一些理解,有感興趣的小夥伴可以一起交流。

如果用promise實現上面的ajax,大概是這個效果:

    ajax().success().fail();

何爲 Promise

那麼什麼是Promise呢?
  1. Promise是爲了解決異步編程的弊端,使你的代碼更有條理、更清晰、更易維護。
  2. Promise是一個構造函數(或者類),接受一個函數作爲參數,該函數接受resolve,reject兩個參數。
  3. 它的內部有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗),其中pending可以轉化爲fulfilled或者和rejected,但是不能逆向轉化,成功和失敗也不能相互轉化。
  4. value、reason成功的參數和失敗的錯誤信息。
  5. then方法,實現鏈式調用,類似於jq。

基本用法:

    let getInfo = new Promise((resolve, reject) => {
        setTimeout(_ => {
            let ran = Math.random();
            console.log(ran);
            if (ran > 0.5) {
                resolve('success');
            } else {
                reject('fail');
            }
        }, 200);
    });
    getInfo.then(r => {
        return r + ' ----> Vchat';
    }).then(r => {
        console.log(r);
    }).catch(err => {
        console.log(err);
    })
    // ran > 0.5輸出 success ----> Vchat
    // ran <= 0.5輸出 fail

先定個小目標,然後一步步實現它。

構建Promise

  • 基礎構造

    首先需要了解一下基本原理。我第一次接觸Promise的時候,還很懵懂(捂臉)。那會只知道這麼寫,不知道到底是個什麼流程走向。下面,我們來看看最基本的實現:
        function Promise(Fn){
            let resolveCall = function() {console.log('我是默認的');}; // 定義爲函數是爲了防止沒有then方法時報錯
            this.then = (onFulfilled) => {
                resolveCall = onFulfilled;
            };
            function resolve(v){ // 將resolve的參數傳給then中的回調
                resolveCall(v);
            }
            Fn(resolve);
        }
        new Promise((resolve, reject) => {
            setTimeout(_ => {
                resolve('success');
            }, 200)
        }).then(r => {
            console.log(r);
        });
        // success

    這裏需要注意的是,當我們new Promise 的時候Promise裏的函數會直接執行。所以如果你想定義一個Promise以待後用,比如axios封裝,需要用函數包裝。比如這樣:

        function myPromise() {
            return new Promise((resolve, reject) => {
                setTimeout(_ => {
                    resolve('success');
                }, 200)
            })
        }
        // myPromise().then()

    再回到上面,在new Promise 的時候會立即執行fn,遇到異步方法,於是先執行then中的方法,將 onFulfilled 存儲到 resolveCall 中。異步時間到了後,執行 resolve,從而執行 resolveCall即儲存的then方法。這是輸出的是我們傳入的‘success’

    這裏會有一個問題,如果 Promise 接受的方法不是異步的,則會導致 resolve 比 then 方法先執行。而此時 resolveCall 還沒有被賦值,得不到我們想要的結果。所以要給resolve加上異步操作,從而保證then方法先執行。
        // 直接resolve
        new Promise((resolve, reject) => {
            resolve('success');
        }).then(r => {
            console.log(r); // 輸出爲 ‘我是默認的’,因爲此時then方法還沒有,then方法的回調沒有賦值給resolveCall,執行的是默認定義的function() {}。
        });
        // 加上異步處理,保證then方法先執行
        function resolve(v){
            setTimeout(_ => {
                resolveCall(v);
            })
        }
  • 增加鏈式調用

    鏈式調用是Promise非常重要的一個特徵,但是上面寫的那個函數顯然是不支持鏈式調用的,所以我們需要進行處理,在每一個then方法中return一下this。
        function Promise(Fn){
            this.resolves = []; // 方便存儲onFulfilled
            this.then = (onFulfilled) => {
                this.resolves.push(onFulfilled);
                return this;
            };
            let resolve = (value) =>{ // 改用箭頭函數,這樣不用擔心this指針問題
                setTimeout(_ => {
                    this.resolves.forEach(fn => fn(value));
                });
            };
            Fn(resolve);
        }

    可以看到,這裏將接收then回調的方法改爲了Promise的屬性resolves,而且是數組。這是因爲如果有多個then,依次push到數組的方式才能存儲,否則後面的then會將之前保存的覆蓋掉。這樣等到resolve被調用的時候,依次執行resolves中的函數就可以了。這樣可以進行簡單的鏈式調用。

        new Promise((resolve, reject) => {
            resolve('success');
        }).then(r => {
            console.log(r); // success
        }).then(r => {
            console.log(r); // success
        });

    但是我們會有這樣的需求, 某一個then鏈想自己return一個參數供後面的then使用,如:

        then(r => {
            console.log(r);
            return r + ' ---> Vchat';
        }).then();

    要做到這一步,需要再加一個處理。

        let resolve = (value) =>{
            setTimeout(_ => {
                // 每次執行then的回調時判斷一下是否有返回值,有的話更新value
                this.resolves.forEach(fn => value = fn(value) || value);
            });
        };
  • 增加狀態

    我們在文章開始說了Promise的三種狀態以及成功和失敗的參數,現在我們需要體現在自己寫的實例裏面。
        function Promise(Fn){
            this.resolves = [];
            this.status = 'PENDING'; // 初始爲'PENDING'狀態
            this.value;
            this.then = (onFulfilled) => {
                if (this.status === 'PENDING') { // 如果是'PENDING',則儲存到數組中
                    this.resolves.push(onFulfilled);
                } else if (this.status === 'FULFILLED') { // 如果是'FULFILLED',則立即執行回調
                    console.log('isFULFILLED');
                    onFulfilled(this.value);
                }
                return this;
            };
            let resolve = (value) =>{
                if (this.status === 'PENDING') { // 'PENDING' 狀態才執行resolve操作
                    setTimeout(_ => {
                        //狀態轉換爲FULFILLED
                        //執行then時保存到resolves裏的回調
                        //如果回調有返回值,更新當前value
                        this.status = 'FULFILLED';
                        this.resolves.forEach(fn => value = fn(value) || value);
                        this.value = value;
                    });
                }
            };
            Fn(resolve);
        }

    這裏可能會有同學覺得困惑,我們通過一個例子來說明增加的這些處理到底有什麼用。

        let getInfo = new Promise((resolve, reject) => {
            resolve('success');
        }).then(_ => {
            console.log('hahah');
        });
        setTimeout(_ => { 
            getInfor.then(r => {
                console.log(r); // success
            })
        }, 200);

    在resolve函數中,判斷了'PENDING' 狀態才執行setTimeout方法,並且在執行時更改了狀態爲'FULFILLED'。這時,如果運行這個例子,只會輸出一個‘success’,因爲接下來的異步方法調用時狀態已經被改爲‘FULFILLED’,所以不會再次執行。

    這種情況要想它可以執行,就需要用到then方法裏的判斷,如果狀態是'FULFILLED',則立即執行回調。此時的傳參是在resolve執行時保存的this.value。這樣就符合Promise的狀態原則,PENDING不可逆,FULFILLED和REJECTED不能相互轉化。

  • 增加失敗處理

    可能有同學發現我一直沒有處理reject,不用着急。reject和resolve流程是一樣的,需要一個reason做爲失敗的信息返回。在鏈式調用中,只要有一處出現了reject,後續的resolve都不應該執行,而是直接返回reject。
        this.reason;
        this.rejects = [];
         // 接收失敗的onRejected函數
        if (this.status === 'PENDING') {
            this.rejects.push(onRejected);
        }
         // 如果狀態是'REJECTED',則立即執行onRejected。
        if (this.status === 'REJECTED') {
            onRejected(this.reason);
        }
        // reject方法
        let reject = (reason) =>{
            if (this.status === 'PENDING') {
                setTimeout(_ => {
                    //狀態轉換爲REJECTED
                    //執行then時保存到rejects裏的回調
                    //如果回調有返回值,更新當前reason
                    this.status = 'REJECTED';
                    this.rejects.forEach(fn => reason = fn(reason) || reason);
                    this.reason = reason;
                });
            }
        };
        // 執行Fn出錯直接reject
        try {
            Fn(resolve, reject);
        }
        catch(err) {
            reject(err);
        }

    在執行儲存then中的回調函數那一步有一個細節一直沒有處理,那就是判斷是否有onFulfilled或者onRejected方法,因爲是允許不要其中一個的。現在如果then中缺少某個回調,會直接push進undefined,如果執行的話就會出錯,所以要先判斷一下是否是函數。

        this.then = (onFulfilled, onRejected) => {
            // 判斷是否是函數,是函數則執行
            function success (value) {
                return typeof onFulfilled === 'function' && onFulfilled(value) || value;
            }
            function erro (reason) {
                return typeof onRejected === 'function' && onRejected(reason) || reason;
            }
            // 下面的處理也要換成新定義的函數
            if (this.status === 'PENDING') {
                this.resolves.push(success);
                this.rejects.push(erro);
            } else if (this.status === 'FULFILLED') {
                success(this.value);
            } else if (this.status === 'REJECTED') {
                erro(this.reason);
            }
            return this;
        };

    因爲reject回調執行時和resolve基本一樣,所以稍微優化一下部分代碼。

        if(this.status === 'PENDING') {
            let transition = (status, val) => {
                setTimeout(_ => {
                    this.status = status;
                    let f = status === 'FULFILLED',
                        queue = this[f ? 'resolves' : 'rejects'];
                    queue.forEach(fn => val = fn(val) || val);
                    this[f ? 'value' : 'reason'] = val;
                });
            };
            function resolve(value) {
                transition('FULFILLED', value);
            }
            function reject(reason) {
                transition('REJECTED', reason);
            }
        }
  • 串行 Promise

    假設有多個ajax請求串聯調用,即下一個需要上一個的返回值作爲參數,並且要return一個新的Promise捕捉錯誤。這樣我們現在的寫法就不能實現了。

    我的理解是之前的then返回的一直是this,但是如果某一個then方法出錯了,就無法跳出循環、拋出異常。而且原則上一個Promise,只要狀態改變成‘FULFILLED’或者‘REJECTED’就不允許再次改變。

    之前的例子可以執行是因爲沒有在then中做異常的處理,即沒有reject,只是傳遞了數據。所以如果要做到每一步都可以獨立的拋出異常,從而終止後面的方法執行,還需要再次改造,我們需要每個then方法中return一個新的Promise。

        // 把then方法放到原型上,這樣在new一個新的Promise時會去引用prototype的then方法,而不是再複製一份。
        Promise.prototype.then = function(onFulfilled, onRejected) {
            let promise = this;
            return new Promise((resolve, reject) => {
                function success (value) {
                    let val = typeof onFulfilled === 'function' && onFulfilled(value) || value;
                    resolve(val); // 執行完這個then方法的onFulfilled以後,resolve下一個then方法
                }
                function erro (reason) {
                    let rea = typeof onRejected === 'function' && onRejected(reason) || reason;
                    reject(rea); // 同resolve
                }
                if (promise.status === 'PENDING') {
                    promise.resolves.push(success);
                    promise.rejects.push(erro);
                } else if (promise.status === 'FULFILLED') {
                    success(promise.value);
                } else if (promise.status === 'REJECTED') {
                    erro(promise.reason);
                }
            });
        };

    在成功的函數中還需要做一個處理,用以支持在then的回調函數(onFulfilled)中return的Promise。如果onFulfilled方法return的是一個Promise,則直接執行它的then方法。如果成功了,就繼續執行後面的then鏈,失敗了直接調用reject。

        function success(value) {
            let val = typeof onFulfilled === 'function' && onFulfilled(value) || value;
            if(val && typeof val['then'] === 'function'){ // 判斷是否有then方法
                val.then(function(value){ // 如果返回的是Promise 則直接執行得到結果後再調用後面的then方法
                    resolve(value);
                },function(reason){
                    reject(reason);
                });
            }else{
                resolve(val);
            }
        }

    找個例子測試一下

        function getInfo(success, fail) {
            return new Promise((resolve, reject) => {
                setTimeout(_ => {
                    let ran = Math.random();
                    console.log(success, ran);
                    if (ran > 0.5) {
                        resolve(success);
                    } else {
                        reject(fail);
                    }
                }, 200);
            })
        }
        getInfo('Vchat', 'fail').then(res => {
            console.log(res);
            return getInfo('可以線上預覽了', 'erro');
        }, rej => {
            console.log(rej);
        }).then(res => {
            console.log(res);
        }, rej => {
            console.log(rej);
        });
        // 輸出
        // Vchat 0.8914818954810422
        // Vchat
        // 可以線上預覽了 0.03702367800412443
        // erro

總結

到這裏,Promise的主要功能基本上都實現了。還有很多實用的擴展,我們也可以添加。
比如 catch可以看做then的一個語法糖,只有onRejected回調的then方法。其它Promise的方法,比如.all、.race 等等,感興趣的小夥伴可以自己實現一下。另外,文中如有不對之處,還請指出。
    Promise.prototype.catch = function(onRejected){
        return this.then(null, onRejected);
    }

相關文章

交流羣

本羣是Vchat前端交流羣,歡迎各種技術交流,期待你的加入

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章