JS 非同步系列 —— Promise 札記

牧云云發表於2018-07-01

JS 非同步系列 —— Promise 札記

Promise 札記

研究 Promise 的動機大體有以下幾點:

  • 對其 api 的不熟悉以及對實現機制的好奇;

  • 很多庫(比如 fetch)是基於 Promise 封裝的,那麼要了解這些庫的前置條件得先熟悉 Promise;

  • 要了解其它更為高階的非同步操作得先熟悉 Promise;

基於這些目的,實踐了一個符合 Promise/A+ 規範的 repromise

本札記系列總共三篇文章,作為之前的文章 Node.js 非同步異聞錄 的拆分和矯正。

Promise/A+ 核心

JS 非同步系列 —— Promise 札記

在實現一個符合 Promise/A+ 規範的 promise 之前,先了解下 Promise/A+ 核心,想更全面地瞭解可以閱讀 Promise/A+規範

  • Promise 操作只會處在 3 種狀態的一種:未完成態(pending)、完成態(resolved) 和失敗態(rejected);
  • Promise 的狀態只會出現從未完成態向完成態或失敗態轉化;
  • Promise 的狀態一旦轉化,將不能被更改;

repromise api 食用手冊

Promise.resolve()

Promise.resolve() 括號內有 4 種情況

/* 跟 Promise 物件 */
Promise.resolve(Promise.resolve(1))
// Promise {state: "resolved", data: 1, callbackQueue: Array(0)}

/* 跟 thenable 物件 */
var thenable = {
  then: function(resolve, reject) {
    resolve(1)
  }
}

Promise.resolve(thenable)
// Promise {state: "resolved", data: 1, callbackQueue: Array(0)}

/* 普通引數 */
Promise.resolve(1)
// Promise {state: "resolved", data: 1, callbackQueue: Array(0)}

/* 不跟引數 */
Promise.resolve()
// Promise {state: "resolved", data: undefined, callbackQueue: Array(0)}
複製程式碼

Promise.reject()

相較於 Promise.resolve(),Promise.reject() 原封不動地返回引數值

Promise.all(arr)

對於 Promise.all(arr) 來說,在引數陣列中所有元素都變為決定態後,然後才返回新的 promise。

// 以下 demo,請求兩個 url,當兩個非同步請求返還結果後,再請求第三個 url
const p1 = request(`http://some.url.1`)
const p2 = request(`http://some.url.2`)

Promise.all([p1, p2])
  .then((datas) => { // 此處 datas 為呼叫 p1, p2 後的結果的陣列
    return request(`http://some.url.3?a=${datas[0]}&b=${datas[1]}`)
  })
  .then((data) => {
    console.log(msg)
  })
複製程式碼

Promise.race(arr)

對於 Promise.race(arr) 來說,只要引數陣列有一個元素變為決定態,便返回新的 promise。

// race 譯為競爭,同樣是請求兩個 url,當且僅當一個請求返還結果後,就請求第三個 url
const p1 = request(`http://some.url.1`)
const p2 = request(`http://some.url.2`)

Promise.race([p1, p2])
  .then((data) => { // 此處 data 取呼叫 p1, p2 後優先返回的結果
    return request(`http://some.url.3?value=${data}`)
  })
  .then((data) => {
    console.log(data)
  })
複製程式碼

Promise.wrap(fn) —— 回撥函式轉 Promise

通過下面這個案例,提供回撥函式 Promise 化的思路。

function foo(a, b, cb) {
  ajax(
    `http://some.url?a=${a}&b=${b}`,
    cb
  )
}

foo(1, 2, function(err, data) {
  if (err) {
    console.log(err)
  } else {
    console.log(data)
  }
})
複製程式碼

如上是一個傳統回撥函式使用案例,只要使用 Promise.wrap() 包裹 foo 函式就對其完成了 promise 化,使用如下:

const promiseFoo = Promise.wrap(foo)

promiseFoo(1, 2)
  .then((data) => {
    console.log(data)
  })
  .catch((err) => {
    console.log(err)
  })
複製程式碼

Promise.wrap 的實現邏輯也順帶列出來了:

Promise.wrap = function(fn) {
  return funtion() {
    const args = [].slice.call(arguments)
    return new Promise((resolve, reject) => {
      fn.apply(null, args.concat((err, data) => {
        if (err) {
          reject(err)
        } else {
          resolve(data)
        }
      }))
    })
  }
}
複製程式碼

then/catch/done

這幾個 api 比較簡單,合起來一起帶過

Promise.resolve(1)
  .then((data) => {console.log(data)}, (err) => {console.log(err)}) // 鏈式呼叫,可以傳一個引數(推薦),也可以傳兩個引數
  .catch((err) => {console.log(err)}) // 捕獲鏈式呼叫中丟擲的錯誤 || 捕獲變為失敗態的值
  .done()                             // 能捕獲前面鏈式呼叫的錯誤(包括 catch 中),可以傳兩個引數也可不傳
複製程式碼

實踐過程總結

坑點 1:事件迴圈

事件迴圈:同步佇列執行完後,在指定時間後再執行非同步佇列的內容。

之所以要單列事件迴圈,因為程式碼的執行順序與其息息相關,此處用 setTimeout 來模擬事件迴圈;

下面程式碼片段中,① 處執行完並不會馬上執行 setTimeout() 中的程式碼(③),而是此時有多少次 then 的呼叫,就會重新進入 ② 處多少次後,再進入 ③

excuteAsyncCallback(callback, value) {
  const that = this
  setTimeout(function() {
    const res = callback(value) // ③
    that.excuteCallback('fulfilled', res)
  }, 4)
}

then(onResolved, onRejected) {
  const promise = new this.constructor()
  if (this.state !== 'PENDING') {
    const callback = this.state === 'fulfilled' ? onResolved : onRejected
    this.excuteAsyncCallback.call(promise, callback, this.data)              // ①
  } else {
    this.callbackArr.push(new CallbackItem(promise, onResolved, onRejected)) // ②
  }
  return promise
}
複製程式碼

坑點 2:this 的指向問題

this.callbackArr.push() 中的 this 指向的是 ‘上一個’ promise,所以類 CallbackItem 中,this.promise 儲存的是'下一個' promise(then 物件)。

class Promise {
  ...
  then(onResolved, onRejected) {
    const promise = new this.constructor()
    if (this.state !== 'PENDING') {        // 第一次進入 then,狀態是 RESOLVED 或者是 REJECTED
      const callback = this.state === 'fulfilled' ? onResolved : onRejected
      this.excuteAsyncCallback.call(promise, callback, this.data)  // 繫結 this 到 promise
    } else {                               // 從第二次開始以後,進入 then,狀態是 PENDING
      this.callbackArr.push(new CallbackItem(promise, onResolved, onRejected)) // 這裡的 this 也是指向‘上一個’ promise
    }
    return promise
  }
  ...
}

class CallbackItem {
  constructor(promise, onResolve, onReject) {
    this.promise = promise // 相應地,這裡儲存的 promise 是來自下一個 then 的
    this.onResolve = typeof(onResolve) === 'function' ? onResolve : (resolve) => {}
    this.onReject = typeof(onRejected) === 'function' ? onRejected : (rejected) => {}
  }
  ...
}
複製程式碼

more

實踐的更多過程可以參考測試用例。有好的意見歡迎交流。

相關文章