JavaScript——Promise進階

未標題-1.png

最近新型冠狀病毒嚴重,在家也沒啥事,寫寫文章,Promise 前端開發或多或少都有了解或使用到,我也常常用到。 但願閱讀本章能幫助瞭解Promise究竟是怎麼實現的,或多或少能幫你^_^

爲何出現Promise

在javascript開發過程當中,代碼是單線程執行的,同步操做,彼此之間不會等待,這能夠說是它的優點,可是也有它的弊端,如一些網絡操做,瀏覽器事件,文件等操做等,都必須異步執行,針對這些狀況,起初的操做都是使用回調函數實現。javascript

實現方式以下(僞代碼):html

function One(callback) {
    if (success) {
        callback(err, result);
    } else {
        callback(err, null);
    }
}

One(function (err, result) {
    //執行完One函數內的內容,成功的結果回調回來向下執行
})

上述代碼只是一層級回調,若是代碼複雜後,會出現多層級的回調,代碼可讀性也會不好,那有沒有一種方式,不用考慮裏面的內容,直接根據結果成功仍是失敗執行下面的代碼呢?有的,Promise(承諾),在ES6中對Promise進行了統一的規範前端

什麼是promise

Promise可能你們都不陌生,由於Promise規範已經出來好一段時間了,同時Promise也已經歸入了ES6,並且高版本的chrome、firefox瀏覽器都已經原生實現了Promise,只不過和現現在流行的類Promise類庫相比少些API。java

Promise規範以下:git

  • 一個promise可能有三種狀態:等待(pending)、已完成(fulfilled)、已拒絕(rejected)
  • 一個promise的狀態只可能從「等待」轉到「完成」態或者「拒絕」態,不能逆向轉換,同時「完成」態和「拒絕」態不能相互轉換
  • promise必須實現then方法(能夠說,then就是promise的核心),並且then必須返回一個promise,同一個promise的then能夠調用屢次,而且回調的執行順序跟它們被定義時的順序一致
  • then方法接受兩個參數,第一個參數是成功時的回調,在promise由「等待」態轉換到「完成」態時調用,另外一個是失敗時的回調,在promise由「等待」態轉換到「拒絕」態時調用。同時,then能夠接受另外一個promise傳入,也接受一個「類then」的對象或方法,即thenable對象。

Promise原理與講解

原理剖析--極簡promise

由以上規範就容易就能實現這個類的大體結構github

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>Promise</title>
</head>
<body>
<script type="text/javascript">
    class Promise {
        callbacks = [];
        constructor(fn) {
            fn(this.resolve.bind(this));
        }
        then(onFulfilled) {
            this.callbacks.push(onFulfilled);
        }
        resolve(value) {
            this.callbacks.forEach(fn => fn(value));
        }
    }

    //Promise應用
    let p = new Promise(resolve => {
        setTimeout(() => {
            resolve('測試');
        }, 2000);
    })
    p.then((tip)=>{
        console.log('tip1',tip) // tip1,測試
    })
    p.then((tip)=>{
        console.log('tip2',tip) // tip2,測試
    })
</script>
</body>
</html>

這個簡單版本大體邏輯是:
實例化Promise時,其類構造函數初始化執行了回調函數,並將綁定了當前實例的resolve方法做爲參數回傳給回到函數。接着調動Promise對象的then方法, 註冊異步操做完成後的回調函數。 當異步操做完成時,調用resolve方法, 該方法執行then方法註冊的回調函數。
這裏then 方法註冊完成時的回到是一個數組, then方法能夠屢次調用。註冊的函數會在異步操做完成後根據添加的順序依次執行。chrome

相信仔細的人應該能夠看出來,then方法應該可以鏈式調用,可是上面的最基礎簡單的版本顯然沒法支持鏈式調用。想讓then方法支持鏈式調用,其實也是很簡單的(若是寫過jQuery插件的同窗應該熟悉):數組

// 在上方代碼添加
then(onFulfilled) {
    this.callbacks.push(onFulfilled);
    return this; //看這裏
}

// 修改其調用方式
p.then((tip)=>{
    console.log('tip1',tip) // tip1,測試
}).then((tip)=>{
    console.log('tip2',tip) // tip2,測試
})

加入延時機制

首先咱們吧上方代碼的栗子中 setTimeout 去掉,在執行一下promise

//Promise應用
let p = new Promise(resolve => {
    console.log('同步執行1');
    resolve('同步執行2');
}).then(tip => {
    console.log('then1', tip);
}).then(tip => {
    console.log('then2', tip);
});

發現只打印了 同步執行1
image.png
這顯然是不容許的,Promises/A+規範明確要求回調須要經過異步方式執行,用以保證一致可靠的執行順序。所以咱們要加入一些處理,保證在resolve執行以前,then方法已經註冊完全部的回調。咱們能夠這樣改造下resolve函數瀏覽器

// 修改上方代碼
resolve(value) {
    //看這裏
    setTimeout(() => {
        this.callbacks.forEach(fn => fn(value));
    });
}
//經過`setTimeout`機制,將`resolve`中執行回調的邏輯放置到`JS`任務隊列末尾,以保證在`resolve`執行時,`then`方法的回調函數已經註冊完成.

image.png

加入狀態

爲了解決上面提到的問題, 咱們須要加入狀態機制, 也就是你們熟知的pending, fulfilled, rejected。
Promises/A+ 規範中明確規定了, pending 能夠轉化爲fulfilled或rejected而且只能轉化一次。 也就是說若是pending轉爲fulfiled就不能再轉化到rejected。 而且fulfilled 和 rejected狀態只能pending轉化而來, 二者之間不能互相轉換。
image.png

// 修改以下
class Promise {
    callbacks = [];
    state = 'pending';//增長狀態
    value = null;//保存結果
    constructor(fn) {
        fn(this.resolve.bind(this));
    }
    then(onFulfilled) {
        if (this.state === 'pending') {
            //在resolve以前,跟以前邏輯同樣,添加到callbacks中
            this.callbacks.push(onFulfilled);
        } else {
            //在resolve以後,直接執行回調,返回結果了
            onFulfilled(this.value);
        }
        return this;
    }
    resolve(value) {
        this.state = 'fulfilled';//改變狀態
        this.value = value;//保存結果
        this.callbacks.forEach(fn => fn(value));
    }
}

resolve 執行時, 會將狀態設置爲 fulfilled , 並把 value 的值存起來, 在此以後調用 then 添加的新回調都會當即執行, 直接返回保存的value值

有同窗發現增長了狀態的後代碼把setTimeout去掉了,緣由是:
resolve執行時,會將狀態設置爲fulfilled,在此以後調用then添加的新回調,都會當即執行

鏈式Promise

那麼這裏問題又來了,若是用戶再then函數裏面註冊的仍然是一個Promise,該如何解決?好比下面

p()
.then(()=>{
    // 這裏返回Promise
    return new Promise(function (resolve) {
         resolve(resolve);
    });
})
.then(function (res) {
    // res拿到上一個Promise.resolve的值
});

真正的鏈式調用
真正的鏈式Promise是指在當前Promise達到fulfilled狀態後, 即開始進行下一個Promise(後鄰Promise)。 那麼咱們如何銜接當前Promise和後鄰Promise呢,這個是重點,修改較多,我附一段完整的代碼

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>Promise</title>
</head>
<body>
<script type="text/javascript">
    class Promise {
        callbacks = [];
        state = 'pending';//增長狀態
        value = null;//保存結果
        constructor(fn) {
            fn(this.resolve.bind(this));
        }
        then(onFulfilled) {
            return new Promise(resolve => {
                this.handle({
                    onFulfilled: onFulfilled || null,
                    resolve: resolve
                });
            });
        }
        handle(callback) {
            if (this.state === 'pending') {
                this.callbacks.push(callback);
                return false;
            }
            //若是then中沒有傳遞任何東西
            if (!callback.onFulfilled) {
                callback.resolve(this.value);
                return false;
            }
            var ret = callback.onFulfilled(this.value);
            callback.resolve(ret);
        }
        resolve(value) {
            if (value && (typeof value === 'object' || typeof value === 'function')) {
                var then = value.then;
                if (typeof then === 'function') {
                    then.call(value, this.resolve.bind(this));
                    return;
                }
            }
            this.state = 'fulfilled';//改變狀態
            this.value = value;//保存結果
            this.callbacks.forEach(fn => fn(value));
        }
    }

    // then中返回Promise
    let p = new Promise(resolve => {
        console.log('同步執行1');
        resolve('同步執行2');
    }).then(tip => {
        console.log(tip);
        return new Promise((resolve)=>{
            resolve('同步執行3')
        })
    }).then(tip => {
        console.log(tip);
    });
</script>
</body>
</html>

image.png

  1. then方法中,建立並返回了新的Promise實例,這是串行Promise的基礎,而且支持鏈式調用。
  2. handle方法是promise內部的方法。then方法傳入的形參onFulfilled以及建立新Promise實例時傳入的resolve均被push到當前promisecallbacks隊列中,這是銜接當前promise和後鄰promise的關鍵所在(這裏必定要好好的分析下handle的做用)。
  3. resolve 方法中會先檢查value是否是 Promise 對象, 若是是一個新的Promise, 那麼先不改變當前 promise 的狀態。

錯誤處理

在異常操做失敗時,標記其狀態爲rejected, 並執行註冊的失敗回調

<!--完整代碼-->
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>Promise</title>
</head>
<body>
<script type="text/javascript">
    class Promise {
        callbacks = [];
        state = 'pending';//增長狀態
        value = null;//保存結果
        constructor(fn) {
            fn(this.resolve.bind(this), this.reject.bind(this));
        }
        then(onFulfilled, onRejected) {
            return new Promise((resolve, reject) => {
                this.handle({
                    onFulfilled: onFulfilled || null,
                    onRejected: onRejected || null,
                    resolve: resolve,
                    reject: reject
                });
            });
        }
        handle(callback) {
            if (this.state === 'pending') {
                this.callbacks.push(callback);
                return;
            }

            let cb = this.state === 'fulfilled' ? callback.onFulfilled : callback.onRejected;

            if (!cb) {//若是then中沒有傳遞任何東西
                cb = this.state === 'fulfilled' ? callback.resolve : callback.reject;
                cb(this.value);
                return;
            }

            // 這裏處理,若是在執行成功回調、失敗回調時代碼出錯怎麼辦,對於相似異常, 處理也很簡單, 可使用try-catch捕獲錯誤, 而後將相應的promise狀態設置爲rejected狀態
            let ret;
            try {
                ret = cb(this.value);
                cb = this.state === 'fulfilled' ? callback.resolve : callback.reject;
            } catch (error) {
                ret = error;
                cb = callback.reject
            } finally {
                cb(ret);
            }

        }
        resolve(value) {
            if (value && (typeof value === 'object' || typeof value === 'function')) {
                var then = value.then;
                if (typeof then === 'function') {
                    then.call(value, this.resolve.bind(this), this.reject.bind(this));
                    return;
                }
            }
            this.state = 'fulfilled';//改變狀態
            this.value = value;//保存結果
            this.callbacks.forEach(callback => this.handle(callback));
        }
        reject(error) {
            this.state = 'rejected';
            this.value = error;
            this.callbacks.forEach(callback => this.handle(callback));
        }
    }

    //Promise應用
    let p = new Promise(resolve => {
        console.log('同步執行1');
        resolve('同步執行2');
    }).then(tip => {
        return new Promise((resolve,reject)=>{
            // 作個隨機數控制resolve或者reject的調用
            if(parseInt(Math.random()*10) > 4){
                resolve(tip+' 成功')
            }else{
                reject(tip+' 失敗')
            }
        })
    }).then(result => {
        console.log(result);
    }, error => {
        console.log(error);
    });
</script>
</body>
</html>

image.png

總結

promise 裏面的 then 函數僅僅是註冊了後續須要執行的回調函數,同時返回一個新的Promise對象,以延續鏈式調用,真正的邏輯是在handle裏面
對於內部 pending 、fulfilled 和 rejected 的狀態轉變,經過 handler 觸發 resolve 和 reject 方法,而後更改state狀態值

參考文章

MPromise
分層解析 Promise 的實現原理
Promise 原理解析 (漸進實例詳解)

關於我

https://www.vipbic.com/.png

相關文章
相關標籤/搜索