天天看點

圖解 Promise 實作原理(二)—— Promise 鍊式調用

本系列文章由淺入深逐漸實作 Promise,并結合流程圖、執行個體以及動畫進行示範,達到深刻了解 Promise 用法的目的。

本文首發于 vivo網際網路技術 微信公衆号 

連結: https://mp.weixin.qq.com/s/Xz2bGaLxVL4xw1M2hb2nJQ

作者:Morrain

很多同學在學習 Promise 時,知其然卻不知其是以然,對其中的用法了解不了。本系列文章由淺入深逐漸實作 Promise,并結合流程圖、執行個體以及動畫進行示範,達到深刻了解 Promise 用法的目的。

本系列文章有如下幾個章節組成:

  1. 圖解 Promise 實作原理(一)—— 基礎實作
  2. 圖解 Promise 實作原理(二)—— Promise 鍊式調用
  3. 圖解 Promise 實作原理(三)—— Promise 原型方法實作
  4. 圖解 Promise 實作原理(四)—— Promise 靜态方法實作

一、前言

上一節中,實作了 Promise 的基礎版本:

//極簡的實作+鍊式調用+延遲機制+狀态
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));
    }
}           

但鍊式調用,隻是在 then 方法中 return 了 this,使得 Promise 執行個體可以多次調用 then 方法,但因為是同一個執行個體,調用再多次 then 也隻能傳回相同的一個結果,通常我們希望的鍊式調用是這樣的:

//使用Promise
function getUserId(url) {
    return new Promise(function (resolve) {
        //異步請求
        http.get(url, function (id) {
            resolve(id)
        })
    })
}
getUserId('some_url').then(function (id) {
    //do something
    return getNameById(id);
}).then(function (name) {
    //do something
    return getCourseByName(name);
}).then(function (course) {
    //do something
    return getCourseDetailByCourse(course);
}).then(function (courseDetail) {
    //do something
});           

每個 then 注冊的 onFulfilled 都傳回了不同的結果,層層遞進,很明顯在 then 方法中 return this 不能達到這個效果。引入真正的鍊式調用,then 傳回的一定是一個新的Promise執行個體。

圖解 Promise 實作原理(二)—— Promise 鍊式調用

真正的鍊式 Promise 是指在目前 Promise 達到 fulfilled 狀态後,即開始進行下一個 Promise(後鄰 Promise)。那麼我們如何銜接目前 Promise 和後鄰 Promise 呢?(這是了解 Promise 的難點,我們會通過動畫示範這個過程)。

二、鍊式調用的實作

先看下實作源碼:

//完整的實作
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;
        }
        //如果then中沒有傳遞任何東西
        if (!callback.onFulfilled) {
            callback.resolve(this.value);
            return;
        }
        var ret = callback.onFulfilled(this.value);
        callback.resolve(ret);
    }
    _resolve(value) {
        this.state = 'fulfilled';//改變狀态
        this.value = value;//儲存結果
        this.callbacks.forEach(callback => this._handle(callback));
    }
}           

由上面的實作,我們可以看到:

  • then 方法中,建立并傳回了新的 Promise 執行個體,這是串行Promise的基礎,是實作真正鍊式調用的根本。
  • then 方法傳入的形參 onFulfilled 以及建立新 Promise 執行個體時傳入的 resolve 放在一起,被push到目前 Promise 的 callbacks 隊列中,這是銜接目前 Promise 和後鄰 Promise 的關鍵所在。
  • 根據規範,onFulfilled 是可以為空的,為空時不調用 onFulfilled。

看下動畫示範:

圖解 Promise 實作原理(二)—— Promise 鍊式調用

(Promise 鍊式調用示範動畫)

當第一個 Promise 成功時,resolve 方法将其狀态置為 fulfilled ,并儲存 resolve 帶過來的value。然後取出 callbacks 中的對象,執行目前 Promise的 onFulfilled,傳回值通過調用第二個 Promise 的 resolve 方法,傳遞給第二個 Promise。動畫示範如下:

圖解 Promise 實作原理(二)—— Promise 鍊式調用

(Promise 鍊式調用 fulfilled)

為了真實的看到鍊式調用的過程,我寫一個mockAjax函數,用來模拟異步請求:

/**
 * 模拟異步請求
 * @param {*} url  請求的URL
 * @param {*} s  指定該請求的耗時,即多久之後請求會傳回。機關秒
 * @param {*} callback 請求傳回後的回調函數
 */
const mockAjax = (url, s, callback) => {
    setTimeout(() => {
        callback(url + '異步請求耗時' + s + '秒');
    }, 1000 * s)
}           

除此之外,我給 Promise 的源碼加上了日志輸出并增加了構造順序辨別,可以清楚的看到構造以及執行過程:

//Demo1
new Promise(resolve => {
  mockAjax('getUserId', 1, function (result) {
    resolve(result);
  })
}).then(result => {
  console.log(result);
})           

【Demo1 的源碼】

執行結果如下:

[Promse-1]:constructor
[Promse-1]:then
[Promse-2]:constructor
[Promse-1]:_handle state= pending
[Promse-1]:_handle callbacks= [ { onFulfilled: [Function], resolve: [Function] } ]
=> Promise { callbacks: [], name: 'Promse-2', state: 'pending', value: null }
[Promse-1]:_resolve
[Promse-1]:_resolve value= getUserId異步請求耗時1秒
[Promse-1]:_handle state= fulfilled
getUserId異步請求耗時1秒
[Promse-2]:_resolve
[Promse-2]:_resolve value= undefined           

通過列印出來的日志,可以看到:

  1. 構造 Promise-1 執行個體,立即執行 mackAjax('getUserId',callback);
  2. 調用 Promise-1 的 then 方法,注冊 Promise-1 的 onFulfilled 函數。
  3. then 函數内部構造了一個新的 Promise執行個體:Promise-2。立即執行 Promise-1 的 _handle方法。
  4. 此時 Promise-1 還是pending的狀态。
  5. Promise-1._handle 中就把注冊在 Promise-1 的 onFulfilled 和 Promise-2 的 resolve 儲存在 Promise-1 内部的 callbacks。
  6. 至此目前線程執行結束。傳回的是 Promise-2 的 Promise執行個體。
  7. 1s後,異步請求傳回,要改變 Promise-1 的狀态和結果,執行 resolve(result)。
  8. Promise-1 的值被改變,内容為異步請求傳回的結果:"getUserId異步請求耗時1s"。
  9. Promise-1 的狀态變成 fulfilled。
  10. Promise-1 的 onFulfilled 被執行,列印出了"getUserId異步請求耗時1秒"。
  11. 然後再調用 Promise-2.resolve。
  12. 改變 Promise-2 的值和狀态,因為 Promise-1 的 onFulfilled 沒有傳回值,是以 Promise-2的值為undefined。

上例中,如果把異步的請求改成同步會是什麼的效果?

new Promise(resolve => {
  resolve('getUserId同步請求');
}).then(result => {
    console.log(result);
});

//列印日志
[Promse-1]:constructor
[Promse-1]:_resolve
[Promse-1]:_resolve value= getUserId同步請求
[Promse-1]:then
[Promse-2]:constructor
[Promse-1]:_handle state= fulfilled
getUserId同步請求
[Promse-2]:_resolve
[Promse-2]:_resolve value= undefined
=> Promise {
  callbacks: [],
  name: 'Promse-2',
  state: 'fulfilled',
  value: undefined }           

感興趣的可以自己去分析一下。

三、鍊式調用真正的意義

執行目前 Promise 的 onFulfilled 時,傳回值通過調用第二個 Promise 的 resolve 方法,傳遞給第二個 Promise,作為第二個 Promise 的值。于是我們考慮如下Demo:

//Demo2
new Promise(resolve => {
    mockAjax('getUserId', 1, function (result) {
        resolve(result);
    })
}).then(result => {
    console.log(result);
    //對result進行第一層加工
    let exResult = '字首:' + result;
    return exResult;
}).then(exResult => {
    console.log(exResult);
});           

【Demo2 的源碼】

我們加了一層 then,來看下執行的結果:

[Promse-1]:constructor
[Promse-1]:then
[Promse-2]:constructor
[Promse-1]:_handle state= pending
[Promse-1]:_handle callbacks= [ { onFulfilled: [Function], resolve: [Function] } ]
[Promse-2]:then
[Promse-3]:constructor
[Promse-2]:_handle state= pending
[Promse-2]:_handle callbacks= [ { onFulfilled: [Function], resolve: [Function] } ]
=> Promise { callbacks: [], name: 'Promse-3', state: 'pending', value: null }
[Promse-1]:_resolve
[Promse-1]:_resolve value= getUserId異步請求耗時1秒
[Promse-1]:_handle state= fulfilled
getUserId異步請求耗時1秒
[Promse-2]:_resolve
[Promse-2]:_resolve value= 字首:getUserId異步請求耗時1秒
[Promse-2]:_handle state= fulfilled
字首:getUserId異步請求耗時1秒
[Promse-3]:_resolve
[Promse-3]:_resolve value= undefined:           

鍊式調用可以無限的寫下去,上一級 onFulfilled return 的值,會變成下一級 onFulfilled 的結果。可以參考Demo3:

【Demo3 的源碼】

我們很容易發現,上述 Demo3 中隻有第一個是異步請求,後面都是同步的,我們完全沒有必要這麼鍊式的實作。如下一樣能得到我們想要的三個結果: 分别列印出來的值。

//等價于 Demo3
new Promise(resolve => {
    mockAjax('getUserId', 1, function (result) {
        resolve(result);
    })
}).then(result => {
    console.log(result);
    //對result進行第一層加工
    let exResult = '字首:' + result;
    console.log(exResult);

    let finalResult = exResult + ':字尾';
    console.log(finalResult);
});           

那鍊式調用真正的意義在哪裡呢?

剛才示範的都是 onFulfilled 傳回值是 value 的情況,如果是一個 Promise 呢?是不是就可以通過 onFulfilled,由使用 Promise 的開發者決定後續 Promise 的狀态。

于是在 _resolve 中增加對前一個 Promise onFulfilled 傳回值的判斷:

_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(callback => this._handle(callback));
    }           

從代碼上看,它是對 resolve 中的值作了一個特殊的判斷,判斷 resolve 的值是否為 Promise執行個體,如果是 Promise 執行個體,那麼就把目前 Promise 執行個體的狀态改變接口重新注冊到 resolve 的值對應的 Promise 的 onFulfilled 中,也就是說目前 Promise 執行個體的狀态要依賴 resolve 的值的 Promise 執行個體的狀态。

圖解 Promise 實作原理(二)—— Promise 鍊式調用
//Demo4
const pUserId = new Promise(resolve => {
  mockAjax('getUserId', 1, function (result) {
    resolve(result);
  })
})
const pUserName = new Promise(resolve => {
  mockAjax('getUserName', 2, function (result) {
    resolve(result);
  })
})

pUserId.then(id => {
  console.log(id)
  return pUserName
}).then(name => {
  console.log(name)
})           

【Demo 4 的源碼】

執行的結果如下:

[Promse-1]:constructor
[Promse-2]:constructor
[Promse-1]:then
[Promse-3]:constructor
[Promse-1]:_handle state= pending
[Promse-1]:_handle callbacks= [ { onFulfilled: [Function], resolve: [Function] } ]
[Promse-3]:then
[Promse-4]:constructor
[Promse-3]:_handle state= pending
[Promse-3]:_handle callbacks= [ { onFulfilled: [Function], resolve: [Function] } ]
=> Promise { callbacks: [], name: 'Promse-4', state: 'pending', value: null }
[Promse-1]:_resolve
[Promse-1]:_resolve value= getUserId異步請求耗時1秒
[Promse-1]:_handle state= fulfilled
getUserId異步請求耗時1秒
[Promse-3]:_resolve
[Promse-3]:_resolve value= Promise { callbacks: [], name: 'Promse-2', state: 'pending', value: null }
[Promse-2]:then
[Promse-5]:constructor
[Promse-2]:_handle state= pending
[Promse-2]:_handle callbacks= [ { onFulfilled: [Function], resolve: [Function] } ]
[Promse-2]:_resolve
[Promse-2]:_resolve value= getUserName異步請求耗時2秒
[Promse-2]:_handle state= fulfilled
[Promse-3]:_resolve
[Promse-3]:_resolve value= getUserName異步請求耗時2秒
[Promse-3]:_handle state= fulfilled
getUserName異步請求耗時2秒
[Promse-4]:_resolve
[Promse-4]:_resolve value= undefined
[Promse-5]:_resolve
[Promse-5]:_resolve value= undefined           

一樣的,我做了一個示範動畫,還原了這個過程:

圖解 Promise 實作原理(二)—— Promise 鍊式調用

(Promise 真正的鍊式調用)

至此,就實作了 Promise 鍊式調用的全部内容。鍊式調用是 Promise 難點,更是重點。一定要通過執行個體還有動畫,深刻體會。下一節介紹 Promise 其它原型方法的實作。

更多内容敬請關注 vivo 網際網路技術 微信公衆号

圖解 Promise 實作原理(二)—— Promise 鍊式調用

注:轉載文章請先與微信号:Labs2020 聯系。

分享 vivo 網際網路技術幹貨與沙龍活動,推薦最新行業動态與熱門會議。