山寨一個 Promise

一點感悟

Promise 是編寫異步的另外一種方式,鄙人愚見,它就是 Callback 的一種封裝git

相比 Callback ,它有如下特色github

  • Promise 將異步結果保存起來,能夠隨時獲取
  • 鏈式調用 then 方法會返回一個新的 Promise ,從而避免了回調地獄

決定一次異步有兩個環節異步

  1. 發起異步事件
  2. 處理異步結果

Promise 能夠給一個異步事件註冊多個處理函數,舉個栗子,就像這樣函數

let p1 = new Promise((resolve) => {
  fs.readFile('./test.js', 'utf8', (err, data) => {
    resolve(data)
  })
})
p1.then(data => console.log(data))
p1.then(data => console.log(data.toUpperCase()))

用 Callback 實現同樣的效果測試

  • 用 callbacks 將全部註冊的函數保存
  • 待異步事件返回結果,再遍歷 callbacks ,依次執行全部註冊的函數

就像這樣ui

let callbacks = []
function resolve(data){
  callbacks.forEach(cb => cb(data))
}

fs.readFile('./test.js', 'utf8', (err, data) => {
  resolve(data)
})

callbacks.push(data => console.log(data))
callbacks.push(data => console.log(data.toUpperCase()))

將上述代碼封裝一下this

const fs = require('fs')

class FakePromise {
  constructor(fn){
      this.callbacks = []
      resolve = resolve.bind(this)
    function resolve(data){
      this.callbacks.forEach(cb => cb(data))
    }
    fn(resolve)
  }
  
  then(onFulfilled){
    this.callbacks.push(onFulfilled)
  }
}

let p1 = new FakePromise(resolve => {
  fs.readFile('./test.js', 'utf8', (err, data) => {
    resolve(data)
  })
})
p1.then(data => console.log(data))
p1.then(data => console.log(data.toUpperCase()))

哈?是否是和真的 Promise 有點像code

從發佈-訂閱模式的角度來看:對象

  • FakePromise 中經過 .then(onFulfilled) 來訂閱消息,註冊處理異步結果的函數
  • 經過 resolve(data) 來發布消息,觸發處理異步結果的函數去執行,發佈的時機是異步事件完成時

延時 resolve

先前的代碼存在一個問題,若是在執行 p1.then(data => console.log(data)) 以前,resolve(data) 就已經執行了,那麼再經過 .then(onFulfilled) 註冊的處理異步結果的函數將永遠不會執行隊列

爲了不這種狀況,改造 resolve 函數,在其內部添加 setTimeout,從而保證那些註冊的處理函數是在下一個事件隊列中執行,就像這樣

function resolve(value) {
    setTimeout(() => {
        this.callbacks.forEach(cb => cb(value))
    }, 0)
}

經過延時執行 resolve 內部的函數,保證了先訂閱消息,再發布消息

可是 Promise 還有個額外的功能是在發佈消息後,仍然能夠訂閱消息,而且當即執行,就像這樣

const fs = require('fs')

let p1 = new Promise(resolve => {
    fs.readFile('./test.js', 'utf8', (err, data) => resolve(data))
})

p1.then(data => console.log(data))
setTimeout(function(){
    p1.then(data => console.log(data.toUpperCase()))
}, 5000)

5s以內,文件早已讀取成功,可是在5s以後,依然能夠經過 .then 註冊處理事件,而且該事件會當即執行

先發布,再訂閱

實現先發布,再訂閱的基礎是將消息保存下來。其次要記錄狀態,判斷消息是否已被髮布,若是未發佈消息,則經過 .then 來註冊回調時,是將回調函數添加到內部的回調隊列中;若是消息已發佈,則經過 .then 來註冊回調時,直接將消息傳至回調函數,並執行

Promise 規範中採用的狀態機制是 pendingfulfilledrejected

pending 能夠轉化爲 fulfilledrejected ,而且只能轉化一次。

轉化爲 fulfilledrejected 後,狀態就不可再變

修改代碼以下

class FakePromise {
    constructor(fn) {
        this.value = null
        this.state = 'pending'
        this.callbacks = []
        resolve = resolve.bind(this)

        function resolve(value) {
            setTimeout(() => {
                this.value = value
                this.state = 'fulfilled'
                this.callbacks.forEach(cb => cb(value))
            }, 0)
        }
        fn(resolve)
    }

    then(onFulfilled) {
        if (this.state === 'pending') {
            this.callbacks.push(onFulfilled)
        } else {
            onFulfilled(this.value)
        }
    }
}

既然實現了先發布,再訂閱,那麼 resolve 中的 setTimeout 是否是能夠去掉了?

並不能夠,由於人家正經的 Promise 是這樣的

let p1 = new Promise(resolve => {
    resolve('haha')
})
p1.then(data => console.log(data))
p1.then(data => console.log(data.toUpperCase()))
console.log('xixi')
// xixi
// haha
// HAHA

只有保留 resolve 中 setTimeout 才能使 FakePromise 實現相同的效果

let p1 = new FakePromise(resolve => {
    resolve('haha')
})
p1.then(data => console.log(data))
p1.then(data => console.log(data.toUpperCase()))
console.log('xixi')
// xixi
// haha
// HAHA

沒有 setTimeout 的輸出結果

// haha
// HAHA
// xixi

鏈式 Promise

正經的 Promise 能夠鏈式調用,從而避免了回調地獄

let p1 = new Promise(resolve => {
    fs.readFile('./test.js', 'utf8', (err, data) => {
        resolve(data)
    })
}).then(res => {
    return new Promise(resolve => {
        fs.readFile('./main.js', 'utf8', (err, data) => {
            resolve(data)
        })
    })
}).then(res => {
    console.log(res)
})

正經的 Promise 調用 then 方法會返回一個新的 Promise 對象

咱們僞造的 FakePromise 並無實現這一功能,原來的 then 方法

...
    then(onFulfilled){
        if (this.state === 'pending') {
            this.callbacks.push(onFulfilled)
        } else {
            onFulfilled(this.value)
        }
    }
...

原來的 then 方法就是根據 state 判斷是註冊 onFulfilled 函數,仍是執行 onFulfilled 函數

爲了實現 FakePromise 的高仿,咱們要改造 then 方法,使其返回一個新的 FakePromise ,爲了方便區分,將返回的 FakePromise 取名爲 SonFakePromise ,而先前調用 then 的對象爲 FatherFakePromise

那麼問題來了

  • 那麼構造這個 SonFakePromise 的函數參數是什麼
  • 這個 SonFakePromise 何時 resolve ?

首先,當構造一個新的 SonFakePromise 時,會將傳入的函數參數 fn 執行一遍,且這個函數有 resolve 參數

...
    then(onFulfilled){
      if(this.state === 'pending'){
        this.callbacks.push(onFulfilled)
        let SonFakePromise = new FakePromise(function fn(resolve){
          
        })
        return SonFakePromise
      }else{
        onFulfilled(this.value)
        let SonFakePromise = new FakePromise(function fn(resolve){
          
        })
        return SonFakePromise
      }
    }
...

如今的問題是這個 SonFakePromise 何時 resolve ?即構造函數中的函數參數 fn 如何定義

結合正經 Promise 的例子來看

let faherPromise = new Promise(resolve => {
    fs.readFile('./test.js', 'utf8', (err, data) => {
        resolve(data)
    })
}).then(res => {
    return new Promise(resolve => {
        fs.readFile('./main.js', 'utf8', (err, data) => {
            resolve(data)
        })
    })
}).then(res => {
    console.log(res)
})
// 等同於
let faherPromise = new Promise(resolve => {
    fs.readFile('./test.js', 'utf8', (err, data) => {
        resolve(data)
    })
})
let sonPromise = faherPromise.then(function onFulfilled(res){
    return new Promise(function fn(resolve){
        fs.readFile('./main.js', 'utf8', (err, data) => {
            resolve(data)
        })
    })
}).then(res => {
    console.log(res)
})

在例子中,onFulfilled 函數以下,且其執行後返回一個新的 Promise,暫時取名爲 fulPromise

function onFulfilled(res) {
  return new Promise(function fn(resolve){
    fs.readFile('./main.js', 'utf8', (err, data) => {
      resolve(data)
    })
  })
}

如今來分析一下,fatherPromisesonPromisefulPromise 這三者的關係

  • sonPromise 是調用 fatherPromise 的 then 方法返回的
  • 而調用這個 then 方法須要傳入一個函數參數,取名爲 retFulPromise
  • retFulPromise 函數執行的返回值 fulPromise

但願下面的代碼能有助於理解

let fatherPromise = new Promise(function fatherFn(fatherResolve){
  fs.readFile('./test.js', 'utf8', (err, data) => {
    fatherResolve(data)
  })
})

let sonPromise = fatherPromise.then(retFulPromise)

function retFulPromise(res) {
  let fulPromise = new Promise(function fulFn(fulResolve){
    fs.readFile('./main.js', 'utf8', (err, data) => {
      fulResolve(data)
    })
  })
  return fulPromise
}

fatherPromise 的狀態爲 fulfilled 時,會執行 retFulPromise,其返回 fulPromise ,當這個 fulPromise 執行 fulResolve 時,即完成讀取 main.js 時, sonPromise 也會執行內部的 resolve

因此能夠當作,sonPromise 的 sonResolve 函數,也被註冊到了 fulPromise 上

So,瞭解了整個流程,該怎麼修改本身的 FakePromise 呢?

秀操做,考驗技巧的時候到了,將 sonResolve 的引用保存起來,註冊到 fulFakePromise 上

const fs = require('fs')

class FakePromise {
    constructor(fn) {
        this.value = null
        this.state = 'pending'
        this.callbacks = []
        resolve = resolve.bind(this)

        function resolve(value) {
            setTimeout(() => {
                this.value = value
                this.state = 'fulfilled'
                this.callbacks.forEach(cb => {
                    let returnValue = cb.onFulfilled(value)
                    if (returnValue instanceof FakePromise) {
                        returnValue.then(cb.sonResolveRes)
                    }
                })
            })
        }
        fn(resolve)
    }

    then(onFulfilled) {
        if (this.state === 'pending') {
            let sonResolveRes = null
            let sonFakePromise = new FakePromise(function sonFn(sonResolve) {
                sonResolveRes = sonResolve
            })
            this.callbacks.push({
                sonFakePromise,
                sonResolveRes,
                onFulfilled
            })
            return sonFakePromise
        } else {
            let value = onFulfilled(this.value)
            let sonResolveRes = null
            let sonFakePromise = new FakePromise(function sonFn(sonResolve) {
                sonResolveRes = sonResolve
            })
            if (value instanceof FakePromise) {
                value.then(sonResolveRes)
            }
            return sonFakePromise
        }
    }
}

多角度測試

let fatherFakePromise = new FakePromise(resolve => {
    fs.readFile('./test.js', 'utf8', (err, data) => {
        resolve(data)
    })
})
let sonFakePromise = fatherFakePromise.then(function onFulfilled(res) {
    return new FakePromise(function fn(resolve) {
        fs.readFile('./main.js', 'utf8', (err, data) => {
            resolve(data)
        })
    })
}).then(res => {
    console.log(res)
})
let fatherFakePromise = new FakePromise(resolve => {
    fs.readFile('./test.js', 'utf8', (err, data) => {
        resolve(data)
    })
})
setTimeout(function () {
    let sonFakePromise = fatherFakePromise.then(function onFulfilled(res) {
        return new FakePromise(function fn(resolve) {
            fs.readFile('./main.js', 'utf8', (err, data) => {
                resolve(data)
            })
        })
    }).then(res => {
        console.log(res)
    })
}, 1000)
let fatherFakePromise = new FakePromise(resolve => {
    resolve('haha')
})

let sonFakePromise = fatherFakePromise.then(function onFulfilled(res) {
    return new FakePromise(function fn(resolve) {
        fs.readFile('./main.js', 'utf8', (err, data) => {
            resolve(data)
        })
    })
}).then(res => {
    console.log(res)
})
let fatherFakePromise = new FakePromise(resolve => {
    resolve('haha')
})

setTimeout(function () {
    let sonFakePromise = fatherFakePromise.then(function onFulfilled(res) {
        return new FakePromise(function fn(resolve) {
            fs.readFile('./main.js', 'utf8', (err, data) => {
                resolve(data)
            })
        })
    }).then(res => {
        console.log(res)
    })
}, 1000)

參考資料

相關文章
相關標籤/搜索