Promise的分層解析及實現

lulujianglab發表於2019-02-28

今天寫的這篇文章是關於 Promise 的,其實在谷歌一搜尋,會出來很多關於Promise的文章,那為什麼還要寫這篇文章呢?

我相信一定有人用過 Promise ,但總有點似懂非懂的感覺,比如我們知道非同步操作的執行是通過 then 來實現的,那後面的操作是如何得知前面非同步操作完成的呢? Promise 具體是怎樣實現的呢?

所以我寫這篇文章的目的主要是從最基礎的點開始剖析,一步一步來理解 Promise 的背後實現原理

也是因為最近自己的困惑,後面邊看文章,邊除錯程式碼,以至於對 Promise 的理解又上升了一個臺階~

為什麼會有 Promise 的產生

我們可以想象這樣一種應用場景,需要連續執行兩個或者多個非同步操作,每一個後來的操作都在前面的操作執行成功之後,帶著上一步操作所返回的結果開始執行

在過去,我們會做多重的非同步操作,比如

doFirstThing((firstResult) => {
  doSecondThing(firstResult, (secondResult) => {
    console.log(`The secondResult is:` + secondResult)
  })
})
複製程式碼

這種多層巢狀來解決一個非同步操作依賴前一個非同步操作的需求,不僅層次不夠清晰,當非同步操作過多時,還會出現經典的回撥地獄

那正確的開啟方式是怎樣的呢?Promise 提供了一個解決上述問題的模式,我們先回到上面那個多層非同步巢狀的問題,接下來轉變為 Promise 的實現方式:

function doFirstThing() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('獲取第一個資料')
      let firstResult = 3 + 4
      resolve(firstResult)
    },400)
  })
}

function doSecondThing(firstResult) {
  console.log('獲取第二個資料')
  let secondResult = firstResult * 5
  return secondResult
}

doFirstThing()
  .then(firstResult => doSecondThing(firstResult))
  .then(secondResult => {
    console.log(`The secondResult Result: ${secondResult}`
  )})
  .catch(err => {
    console.log('err',err)
  })
複製程式碼

可以看到結果就是我們預期得到的,需要注意的一點是,如果想要在回撥中獲取上個 Promise 中的結果,上個 Promise 中必須有返回結果

Promise 到底是什麼

相信經過上面的應用場景,已經大致明白 Promise 的作用了,那它的具體定義是什麼呢?

Promise 是對非同步程式設計的一種抽象,是一個代理物件,代表一個必須進行非同步處理的函式返回的值或丟擲的異常

簡單來說,Promise 主要就是為了解決非同步回撥的問題,正如上面的例子所示

可以將非同步物件和回撥函式脫離開來,通過 then 方法在這個非同步操作上面繫結回撥函式

用 Promise 來處理非同步回撥使得程式碼層析清晰,便於理解,且更加容易維護,其主流規範目前主要是 Promises/A+ ,下面介紹具體的API

狀態和值

Promise 有3種狀態: pending (待解決,這也是初始狀態), fulfilled (完成), rejected (拒絕)

狀態只能由 pending 變為 fulfilled 或由 pending 變為 rejected ,且狀態改變之後不會再發生變化,會一直保持這個狀態

Promise 的值是指狀態改變時傳遞給回撥函式的值

介面

Promise 唯一介面 then 方法,它需要2個引數,分別是 onResolvedonRejected

並且需要返回一個 promise 物件來支援鏈式呼叫

Promise 的建構函式接收一個函式引數,引數形式是固定的非同步任務,接收的函式引數又包含 resolvereject 兩個函式引數,可以用於改變 Promise 的狀態和傳入 Promise 的值

  1. resolve:將 Promise 物件的狀態從 pending (進行中)變為 fulfilled (已成功)

  2. reject:將 Promise 物件的狀態從 pending (進行中)變為 rejected (已失敗)

  3. resolve 和 reject 都可以傳入任意型別的值作為實參,表示 Promise 物件成功( fulfilled )和失敗( rejected )的值

瞭解了 Promise 的狀態和值,接下來,我們開始講解 Promise 的實現步驟

Promise 是怎樣實現的

我們已經瞭解到實現多個相互依賴非同步操作的執行是通過 then 來實現的,那重新回到最開始的疑問,後面的操作是怎麼得知非同步操作完成了呢?

在講解 Promise 實現之前,我們還是先簡要提一下Vue的釋出/訂閱模式:首先有一個事件陣列來收集事件,然後訂閱通過 on 將事件放入陣列, emit 觸發陣列相應事件

那 Promise 呢? Promise 內部其實也有一個陣列佇列存放事件, then 裡邊的回撥函式就存放陣列佇列中。下面我們可以看下具體的實現步驟

實現 promise 雛形

( demo1 )

class Promise {
  constructor (executor) {
    this.value = undefined
    this.status = 'pending'
    executor(value => {
      this.status = 'resolve',
      this.value = value
    }, reason => {
      this.status = 'rejected'
      this.value = reason
    })
  }

  then(onResolved) {
    onResolved(this.value)
  }
}

// 測試
var promise = new Promise((resolve, reject) => {
  resolve('promise')
})

promise.then(value => {
  console.log('value',value)
})
promise.then(value => {
  console.log('value',value)
})
複製程式碼

上述程式碼很簡單,大致的邏輯是:

通過構造器 constructor 定義 Promise 的初始狀態和初始值,通過 Promise 的建構函式接收一個函式引數 executor , 接收的函式引數又包含 resolvereject 兩個函式引數,可以用於改變 Promise 的狀態和傳入 Promise 的值。

然後呼叫 then 方法,將 Promise 操作成功後的值傳入回撥函式

非同步操作

相信有人會好奇,上述 Promise 例項中都是進行的同步操作,但是往往我們使用 Promise 都是進行的非同步操作,那會出現怎樣的結果呢?在上述例子上進行修改,我們用 setTimeout 來模擬非同步的實現

// 測試
var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise')
  },300)
})
複製程式碼

會發現後面的回撥函式中列印出來的值都是undefined

image

很明顯,這種錯誤的造成是因為 then 裡邊的回撥函式在例項化 Promise 操作 resolve 或 reject 之前就執行完成了,所以我們應該設定觸發回撥函式執行的標識,也就是在狀態和值發生改變之後再執行回撥函式

正確的邏輯是這樣的:

  1. 呼叫 then 方法,將需要在 Promise 非同步操作成功時執行的回撥函式放入 children 陣列佇列中,其實也就是註冊回撥函式,類似於觀察者模式

  2. 建立 Promise 例項時傳入的函式會被賦予一個函式型別的引數,即 resolve ( reject ),它接收一個引數 value ,當非同步操作執行成功後,會呼叫 resolve ( reject )方法,這時候其實真正執行的操作是將 children 佇列中的回撥一一執行

在 demo1 的基礎上修改如下:

( demo2 )

class Promise {
  constructor (executor) {
    this.value = undefined
    this.status = 'pending'
    this.children = [] // children為陣列佇列,存放多個回撥函式
    executor(value => {
      this.status = 'resolve',
      this.setValue(value)
    }, reason => {
      this.status = 'rejected'
      this.setValue(reason)
    })
  }

  then (onResolved) {
    this.children.push(onResolved)
  }

  setValue (value) {
    this.value = value
    this.children.forEach(child => {
      child(this.value)
    })
  }
}

// 測試
var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise')
  },300)
})
promise.then(value => {
  console.log('value',value)
})
複製程式碼

首先例項化 Promise 時,傳給 promise 的函式傳送非同步請求,接著呼叫 promise 物件的 then 函式,註冊請求成功的回撥函式,然後當非同步請求傳送成功時,呼叫 resolve ( rejected )方法,該方法依次執行 then 方法註冊的回撥陣列

實現 promise 開枝散葉

相信仔細的人應該可以看出來,then 方法應該能夠支援鏈式呼叫,但是上面的初步實現顯然無法支援鏈式呼叫

那怎樣才能做到支援鏈式呼叫呢?其實實現也很簡單:

then (onResolved) {
  this.children.push(onResolved)
  return this
}
複製程式碼
// 測試
var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise')
  },300)
})
promise.then(value1 => {
  console.log('value1',value1)
}).then(value2 => {
  console.log('value2',value2)
})
複製程式碼

then 方法中加入 return this 實現了鏈式呼叫,但如果需要在 then 回撥函式中返回一個值 value 或者 promise ,傳給下一個 then 回撥函式呢?

先來看返回值 value 的情況,比如:

// 測試
promise.then(value1 => {
  console.log('value1',value1)
  let value = 'promise2'
  return value
}).then(value2 => {
  console.log('value2',value2)
})
複製程式碼

在 demo2 的基礎上進行改造:

( demo3 )

then (onResolved) {
  var child = new Promise(() => {})
  child.onResolved = onResolved
  this.children.push(child)
  return this
}

setValue (value) {
  this.value = value
  this.children.forEach(child => {
    var ret = child.onResolved(this.value)
    this.value = ret
  })
}
複製程式碼

原理就是在呼叫 Promise 物件的 then 函式時,註冊所有請求成功的回撥函式,後續在 setValue 函式中迴圈所有的回撥函式,每次執行完一個回撥函式就會更新 this.value 的值,然後將更新後的 this.value 傳入下一個回撥函式裡,這樣就解決了傳值的問題

但這樣也會出現一個問題,我們只考慮了序列 Promise 的情況下依次更新 this.value 的值,如果序列和並行一起呢?比如:

// 測試
// 序列
promise.then(value1 => {
  console.log('value1',value1)
  let value = 'promise2'
  return value
}).then(value2 => {
  console.log('value2',value2)
})

// 並行
promise.then(value1 => {
  console.log('value1',value1)
})
複製程式碼

列印出來的結果最後一個 value1 為 undefined ,因為我們一直在改變 this.value 的值,並且在序列最後一個 then 回撥函式中也顯示設定返回值,預設返回 undefined

image

可見 return this 並行不通,繼續在 demo3 的基礎上改造 then 和 setValue 函式如下:

( demo4 )

then (onResolved) {
  var child = new Promise(() => {})
  child.onResolved = onResolved
  this.children.push(child)
  return child
}
setValue (value) {
  this.value = value
  this.children.forEach(child => {
    var ret = child.onResolved(this.value)
    child.setValue(ret)
  })
}
複製程式碼

那如果 then 回撥函式中返回一個 promise 呢?比如:

// 測試
promise.then(value1 => {
  console.log('value1',value1)
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('promise2')
    },200)
  })
}).then(value2 => {
  console.log('value2',value2)
})
複製程式碼

image

很明顯,列印出來的結果是個 Promise 。繼續在 demo4 的基礎上改造 setValue 函式

( demo5 )

setValue (value) {
  if (value && value.then) {
    value.then(realValue => {
      this.setValue(realValue)
    })
  } else {
    this.value = value
    this.children.forEach(child => {
      var ret = child.onResolved(this.value)
      child.setValue(ret)
    })
  }
}
複製程式碼

在 setValue 方法裡面,我們對 value 進行了判斷,如果是一個 promise 物件,就會呼叫其 then 方法,形成一個巢狀,直到其不是 promise 物件為止

到目前為止,我們已經實現了 Promise 的主要功能—'開枝散葉',狀態和值的有序更新

實現 promise 錯誤處理

上面所有列舉到的 demo 都是在非同步操作成功的情況下進行的,但非同步操作不可能都成功,在非同步操作失敗時,狀態為標記為 rejected ,並執行註冊的失敗回撥

rejected 失敗的錯誤處理也類似於 resolve 成功狀態下的處理,接著在 demo5 的註冊回撥、處理狀態上加入新的邏輯,在 Promise 上加入 resolvereject 靜態函式

( demo6 )

class Promise {
  constructor (executor) {
    this.value = undefined
    this.status = 'pending'
    this.children = []
    executor(value => {
      this.setValue(value, 'resolved')
    }, reason => {
      this.setValue(reason, 'rejected')
    })
  }

  then (onResolved, onRejected) {
    var child = new Promise(() => {})
    this.children.push(child)
    Object.assign(child, {
      onResolved: onResolved || (value => value),
      onRejected: onRejected || (reason => Promise.reject(reason))
    })
    if (this.status !== 'pending') {
      child.triggerHandler(this.value, this.status)
    }
    return child
  }

  catch (onRejected) {
    return this.then(null, onRejected)
  }

  triggerHandler (parentValue, status) {
    var handler
    if (status === 'resolved') {
      handler = this.onResolved
    } else if (status === 'rejected') {
      handler = this.onRejected
    }
    this.setValue(handler(parentValue), 'resolved')
  }

  setValue (value, status) {
    if (value && value.then) {
      value.then(realValue => {
        this.setValue(realValue, 'resolved')
      }, reason => {
        this.setValue(reason, 'rejected')
      })
    } else {
      this.status = status
      this.value = value
      this.children.forEach(child => {
        child.triggerHandler(value, status)
      })
    }
  }

  static resolve (value) {
    return new Promise(resolve => {
      resolve(value)
    })
  }

  static reject (reason) {
    return new Promise((resolve, reject) => {
      reject(reason)
    })
  }
}
複製程式碼

then 函式中有兩個回撥 handler, 分別是onResolvedonResolved ,表示成功執行的回撥函式和是失敗執行的回撥函式,並設定預設值,保持鏈式連線

定義一個 triggerHandler 函式用來判斷當前 child 的 status ,並觸發自己的 handler ,執行回撥函式,再次更新 status 和 value

setValue 函式同時設定 Promise 自己的狀態和值,然後在重新設定新的狀態之後迴圈遍歷 children

為了更高效率的執行,在 then 函式中註冊回撥函式時加入狀態判斷,如果狀態改變不為 pending ,說明 setValue 函式已經執行,狀態已經發生了更改,就立馬執行 triggerHandle r函式;如果狀態為 pending ,則在 setValue 函式執行時再觸發 triggerHandle `函式

Promise 中的 nextTick

Promise/A+規範要求 handler 執行必須是非同步的, 具體可以參見標準 3.1 條

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called

這裡用 setTimeout 簡單實現一個跨平臺的 nextTick

function nextTick(func) {
  setTimeout(func)
}
複製程式碼

然後使用 nextTick 包裹 triggerHandler

triggerHandler (status, parentValue) {
  nextTick(() => {
    var handler
    if (status === 'resolved') {
      handler = this.onResolved
    } else if (status === 'rejected') {
      handler = this.onRejected
    }
    this.setStatus('resolved', handler(parentValue))
  })
}
複製程式碼

在 demo6 中我們實現了不管是非同步還是同步都可以執行 triggerHandler ,那為什麼要強制非同步的要求呢?

主要是為了流程可預測,標準需要強制非同步。可類比於經典的 image onload 問題

var image = new Image()
image.onload = funtion
image.src = 'url'
複製程式碼

src 屬性為什麼需要寫在 onload 事件後面?

因為 js 內部是按順序逐行執行的,可以認為是同步的,給 image 賦值 src 時,去載入圖片這個過程是非同步的,這個非同步過程完成後,如果有 onload ,則執行 onload

如果先賦值 src ,那麼這個非同步過程可能在你賦值 onload 之前就完成了(比如圖片快取),那麼 onload 就不會執行

反之, js 同步執行確定 onload 賦值完成後才會賦值 src ,可以保證這個非同步過程在 onload 賦值完成後才開始進行,也就保證了 onload 一定會被執行到

同樣的,在Promise中,我們希望程式碼執行順序是完全可以預測的,不允許出現任何問題

總結

上述 Promise 每個階段層次的實現程式碼可見我的 github 分層實現Promise

需要注意的是:

  1. promise 裡面的 then 函式僅僅是註冊了後續需要執行的回撥函式,真正的執行是在 triggerHandler 方法裡

  2. then 和 catch 註冊完回撥函式後,返回的是一個新的 Promise 物件,以延續鏈式呼叫

  3. 對於內部 pending 、fulfilled 和 rejected 的狀態轉變,通過 handler 觸發 resolve 和 reject 方法,然後在 setValue 中更改狀態和值

參考文獻

深入理解Promise

相關文章