包教包會,和你實現一個Promise(二)

1、承上

本篇文章是《包教包會,和你實現一個Promise》的第二篇,緊接着第一篇的內容。因此,若是你尚未看過第一篇,你可能須要看一下包教包會,和你實現一個Promise(一)javascript

第一篇咱們已經實現了一個形式上很是相似Promise的MyPromise,可是儘管使用形式有點像,它離徹底符合Promise A+規範的Promise還差的遠。如今的MyPromise:java

function MyPromise(executor) {
  this.status = 'pending'
  this.data = undefined
  this.reason = undefined
  this.resolvedCallbacks = []
  this.rejectedCallbacks = []

  let resolve = (value) => {
    if (this.status === 'pending') {
      this.status = 'fulfilled'
      this.data = value
      this.resolvedCallbacks.forEach(fn => fn(this.data))
    }
  }
  let reject = (reason) => {
    if (this.status === 'pending') {
      this.status = 'rejected'
      this.reason = reason
      this.rejectedCallbacks.forEach(fn => fn(this.reason))
    }
  }
  executor(resolve, reject)
}

MyPromise.prototype.then = function(onResolved, onRejected) {
  let promise2
  if (this.status === 'pending') {
    this.resolvedCallbacks.push(onResolved)
    this.rejectedCallbacks.push(onRejected)
  }
  if (this.status === 'fulfilled') {
    onResolved(this.data)
  }
  if (this.status === 'rejected') {
    onRejected(this.reason)
  }
}
複製代碼

如今這個MyPromise的問題在於,它沒法進行鏈式調用。咱們在使用Promise的時候,會有這樣的代碼:promise

let promise1 = new Promise(function(resolve, reject) {
  // 模擬異步
  setTimeout(() => {
    let flag = Math.random() > 0.5 ? true : false
    if (flag) {
      resolve('success')
    } else {
      reject('fail')
    }
  }, 1000)
})

promise1.then(res => {
  return 1
}, err => {
  return err
}).then(res => {
  console.log(res)
})
複製代碼

可是目前MyPromise的then是一次性,執行完了就完了,沒有返回能then的東西。瀏覽器

2、鏈式調用

根據Promise A+規範,一個Promise實例在then以後必定會返回一個新的Promise實例,這樣就可使用then來實現鏈式調用了。app

在實現then方法以前,咱們先簡單聊一下實現鏈式調用的技巧。通常來講,實現鏈式調用有兩個方法:dom

  • 當某個方法執行時,返回自身,也就是返回this,好比jQuery就是這樣作的
  • 另外一種,就是Promise這樣,當then方法執行時,返回一個新的Promise實例

一、jQuery的鏈式調用思路

function $(selector) {
  return new jQuery(selector) 
}

class jQuery {
  constructor(selector) {
    let slice = Array.prototype.slice
    this.domArray = slice.call(document.querySelectorAll(selector))
    // ...
  }
  append() {
    // ...
    return this
  }
  addClass() {
    //..
    return this
  }
}
複製代碼

上面代碼思路:先構造一個jQuery對象,它的某些方法在執行完了以後返回this,也就是這個一開始構造的jQuery對象。這樣,就能夠這樣鏈式調用下去:$('.app').append($span).addClass('test')異步

二、promise鏈式調用的思路

class Promise {
  constructor() {
    //...
  }
  then() {
    //...
    let promise2 = new Promise()  // 重點在這
    return promise2
  }
}

// 使用
let promise1 = new Promise()
let promise2 = promise1.then()
let promise3 = promise2.then()
// 也能夠直接寫成這樣
promise1.then().then()
複製代碼

這樣,每次then方法執行的時候,都會返回一個徹底新的Promise實例,就能夠繼續往下then了。函數

3、then方法的功能

到這裏,咱們終於來到了Promise最核心部分,那就是then方法。在整個Promise A+規範裏,大部份內容都在說then是如何實現的,說then方法是Promise的核心一點問題都沒有。post

咱們先明確then方法的功能:測試

  • then方法必須返回一個新的Promise實例,這樣才能進行鏈式回調,這個上面說過
  • then方法返回的新的Promise實例的狀態依賴於當前實例的狀態和回調返回值的狀態
  • 當前Promise狀態肯定後的回調返回值能夠被新的實例拿到

接下來分開解釋。

第一點,實現鏈式調用的方法,上面講過了,再也不贅述。

第二點,then方法返回的新Promise實例狀態依賴於當前實例狀態和回調返回值的狀態。要理解這句話,請仔細看下面的例子:

let promise1 = new Promise(function(resolve, reject) {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})

let promise2 = promise1.then(res => {
  return res
})

console.log(promise1) // pendding狀態
console.log(promise2) // pending狀態

setTimeout(() => {
  console.log(promise1) // 成功狀態
  console.log(promise2) // 成功狀態
}, 1000)
複製代碼

從上面的代碼能夠看出:若是promise1是pending狀態,那promise2也必定是pending狀態。只有當promise1狀態肯定,promise2的狀態纔有可能肯定。 那當promise1狀態肯定的時候,是否是promise2的狀態就肯定了呢?也不是,還要看promise1的then裏註冊的兩個函數的返回值。 請看下面的例子:

let promise1 = new Promise(function(resolve, reject) {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})

let promise2 = promise1.then(res => {
  return Promise.reject('拒絕') // 注意這裏,這個回調返回了一個拒絕狀態Promise哦
})

console.log(promise1) // pending狀態
console.log(promise2) // pending狀態

setTimeout(() => {
  console.log(promise1) // 成功狀態
  console.log(promise2) // 這裏是失敗狀態
}, 1000)
複製代碼

從上面的兩個例子,咱們能夠獲得如下結論:當咱們在then方法裏構造新的Promise時,咱們不只要根據當前Promise實例的狀態使用不一樣的策略,同時還要考慮當前then方法傳遞的兩個回調的結果。

第三點,其實很好理解,當promise1的then方法裏傳遞的回調執行的結果,能夠被下個實例拿到,這裏很簡單,得到回調的結果,再根據條件resolve或者reject傳進去便可。

以上的內容,若是聽不懂不要緊,下面講then方法具體實現的時候還會再說道。

4、then方法的具體實現

then方法須要返回一個新的Promise實例,並且須要根據當前實例的狀態來構造這個新的實例。因此MyPromise的then方法的代碼改爲以下的樣子:

MyPromise.prototype.then = function(onResolved, onRejected) {
  let promise2

  if (this.status === 'pending') {
    promise2 = new MyPromise((resolve, reject) => {

    })
    // this.resolvedCallbacks.push(onResolved)
    // this.rejectedCallbacks.push(onRejected)
  }

  if (this.status === 'fulfilled') {
    promise2 = new MyPromise((resolve, reject) => {

    })
    // onResolved(this.data)
  }

  if (this.status === 'rejected') {
    promise2 = new Promise((resolve, reject) => {

    })
    // onRejected(this.reason)
  }
  
  return promise2
}
複製代碼

聲明一個須要返回的promise2,而後根據當前當前實例的狀態來給它賦值。在這裏,咱們先註釋了每一個if判斷裏原來的代碼。接下來的重點就是構造promise2,看它的executor函數具體如何實現了。 咱們先開討論。

1. 當前實例狀態爲pending時

根據以前的討論,當前爲pending時,須要等到它肯定時,promise2的狀態纔有可能肯定。因此MyPromise裏這部分代碼這樣寫

MyPromise.prototype.then = function(onResolved, onRejected) {
  let promise2

  if (this.status === 'pending') {
    promise2 = new MyPromise((resolve, reject) => {
      // 聲明一個成功函數
      function successFn(value) {
        let x = onResolved(value)   
      }
      // 聲明一個失敗函數
      function failFn(reason) {
        let x = onRejected(reason) 
      }
      // 將成功函數push到當前實例的resolvedCallbacks
      this.resolvedCallbacks.push(successFn)
      // 將失敗函數push到當前實例的rejectedCallbacks
      this.rejectedCallbacks.push(failFn)
    })
  }

  if (this.status === 'fulfilled') {
    promise2 = new MyPromise((resolve, reject) => {

    })
    // onResolved(this.data)
  }

  if (this.status === 'rejected') {
    promise2 = new Promise((resolve, reject) => {

    })
    // onRejected(this.reason)
  }
  
  return promise2
}
複製代碼

解釋一下上面代碼的意思:若是當前Promise的then調用時狀態爲pending時,咱們聲明successFn和failFn,而且把它們分別push到resolvedCallbacks和rejectedCallbacks裏。這樣,successFn和failFn的執行時機就交給了當前promise實例。噹噹前Promise實例狀態肯定時,successFn或者failFn就會被執行,這樣就能夠經過調用onResovled或者onRejected拿到回調的結果了。

這一步很是關鍵,若是當前Promise實例狀態爲pending時,then方法裏返回新的promise2就必須等到它狀態肯定時才能拿到它成功或者失敗回調的值。而後根據回調執行後的結果x來肯定promise2的狀態。

整個流程的順序以下:

  • 當前實例處理異步代碼,狀態爲pending
  • then方法執行,當前實例狀態仍爲pending,走到if(this.status === 'pending')分支裏
  • 構造並返回一個promise2,promise2裏此時狀態爲pending
  • promise2的executor聲明successFn和failFn當push當前實例的resolvedCallbacks和rejectedCallbacks裏
  • 當前實例的resolve或者reject調用,狀態肯定,存放在resolvedCallbacks裏的successFn或者failFn被執行
  • 當前實例then方法提供的onResovled或者onRejected被執行,拿到當前實例resolve或者reject傳過來的值並處理返回x
  • 針對x進行處理。。。

2. 當前實例狀態爲fulfilled或者rejected

這個簡單,直接調用onResovled或者onRejected,也就是當前實例then傳遞的第一個和第二個參數。

MyPromise.prototype.then = function(onResolved, onRejected) {
  let promise2

  if (this.status === 'pending') {
    promise2 = new MyPromise((resolve, reject) => {
      function successFn(value) {
        let x = onResolved(value)   
      }
      function failFn(reason) {
        let x = onRejected(reason) 
      }
      this.resolvedCallbacks.push(successFn)
      this.rejectedCallbacks.push(failFn)
    })
  }

  if (this.status === 'fulfilled') {
    promise2 = new MyPromise((resolve, reject) => {
      // 由於此時當前實例的resolve或者reject已經執行
      // this.data或者this.reason
      let x = onResolved(this.data)
    })
  }

  if (this.status === 'rejected') {
    promise2 = new Promise((resolve, reject) => {
      // 此時當前實例resolve或者reject已經執行
      let x = onRejected(this.reason)
    })
  }
  
  return promise2
}
複製代碼

此時,當前實例的resolve或者reject已經執行,狀態已經肯定,this.data或者this.reason已經有值,直接用then傳遞的onResoved或者onRjected調用便可獲取x。

咱們的then寫到這裏,promise2它已經拿到了當前實例的回調結果了。咱們看一下實際使用中它在哪裏 。請看下面的例子:

let promise1 = new Promise(function(resolve, reject) {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})

function onResolved(res) {
  // 這裏進行處理
  return xxx // 這裏返回的xxx其實就是上面代碼裏的x
}

function onRejected(err) {
  // 處理
  return xxx // 這裏返回的xxx就是上面代碼裏的x
}
let promise2 = promise1.then(onResolved, onRjected)
複製代碼

到這裏,當then方法執行時,咱們已經成功拿到了當前實例的回調值x,接下來,咱們將對這個值進行統一處理,並根據x來調用promise2的構造時的resolve或者reject方法來肯定promise2的狀態。

三、處理x

在規範裏,關於如何處理x來肯定promise2的狀態有一個專門的章節來論述。它在規範的2.3節,稱爲The Promise Resolution Procedure ,它根據x的可能的值進行鍼對處理。x能夠是如下的值:

  • x就是promise2實例自己,後面解釋
  • x是咱們本身寫的MyPromise實例
  • x是函數或者對象時
  • x不是函數或者對象時

咱們須要寫一個函數resolve_promise 來對x進行處理,並肯定promise2的狀態

MyPromise.prototype.then = function(onResolved, onRejected) {
  let promise2

  if (this.status === 'pending') {
    promise2 = new MyPromise((resolve, reject) => {
      function successFn(value) {
        let x = onResolved(value)
        // 注意這裏
        resolve_promise(promise2, x, resolve, reject)
      }
      function failFn(reason) {
        let x = onRejected(reason)
        // 這裏也有
        resolve_promise(promise2, x, resolve, reject)
      }
      this.resolvedCallbacks.push(successFn)
      this.rejectedCallbacks.push(failFn)
    })
  }

  if (this.status === 'fulfilled') {
    promise2 = new MyPromise((resolve, reject) => {
      let x = onResolved(this.data)
      // 這裏也有哦
      resolve_promise(promise2, x, resolve, reject)
    })
  }

  if (this.status === 'rejected') {
    promise2 = new Promise((resolve, reject) => {
      // 還有這裏哦
      let x = onRejected(this.reason)
      resolve_promise(promise2, x, resolve, reject)
    })
  }
  
  return promise2
}
// 這裏是resolve_promise
function resolve_promise(promise2, x, resolve, reject) {
  
}
複製代碼

要寫的resolve_promise接收4個參數,一個是當前正在構造的promise2實例,一個是經過當前回調拿到的結果x,resolve和reject是用來肯定promise2狀態的兩個方法,由於只有resolve或者reject被調用了,promise2的狀態才能肯定嘛 。

接下來,咱們根據x可能的四種狀態,來分別處理,這些都是規範的具體內容:

3.1 若是x是promise2實例自己

這種狀況只有在當前實例是pending情況下才有可能發生,例子以下:

let promise1 = new Promise(function(resolve, reject) {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})

function onResolved(res) {
  return promise2
}
let promise2 = promise1.then(onResolved)

promise2.then(res => {
  console.log(res)
}, err => {
  console.log(err) 
  // 這裏會打印出 TypeError: Chaining cycle detected for promise
})
複製代碼

因此當這種狀況發生時,根據規範,直接把promise2使用reject拒絕,並傳一個TypeError

function resolve_promise(promise2, x, resolve, reject) {
  if (x === promise2) {
    reject(new TypeError('Chaining cycle detected for promise'))
    return //這裏return不用再往下了
  }
}
複製代碼
3.2 若是x是咱們本身寫的MyPromise實例

使用場景是這種狀況:

let promise1 = new Promise(function(resolve, reject) {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})

function onResolved(res) {
  // 這裏返回的promise就是咱們要處理的x
  return new Promise(function(resolve, reject) {
    reject('fail')
  })
}
let promise2 = promise1.then(onResolved)
複製代碼

這個時候根據規範,promise2的狀態和值使用x的狀態和值,分三種狀況:

  • 若是x是pending狀態,咱們的promise2必須等到x狀態變爲成功或者失敗,在此以前都是pending
  • 若是x是fulfilled也就是成功狀態,將promise2也標記爲成功狀態,傳遞的值也是x裏成功的值
  • 若是x是rejected也就是失敗狀態,把Promise2票房爲失敗狀態,傳遞的值也是x裏失敗的值

此時的resolve_promise方法進行以下更改:

function resolve_promise(promise2, x, resolve, reject) {
  // x和promise2引用相同的狀況
  if (x === promise2) {
    reject(new TypeError('Chaining cycle detected for promise'))
    return
  }
  // 若是x是MyPromise的實例
  if (x instanceof MyPromise) {
    x.then(function (v) {
      resolve_promise(promise2, v, resolve, reject)
    }, function (t) {
      reject(t)
    }
    return
  }
}
複製代碼

這裏你可能看不懂,不是說有三種狀況嗎?這個代碼也沒有針對這三種狀況作判斷呀。是這樣的,規範上說的三種狀況我經過一個x.then就能夠拿到x構造時resolve或者reject函數傳遞的值。這裏真正的坑點在於,x在構造時使用resolve或者reject傳值時也可能傳遞了一個promise實例,並且仍是規範其它實現(好比bluebird或者Q)的promise實例,因此這裏才須要使用遞歸。這個坑點在規範裏沒說,可是若是不這樣寫,有不少測試用例通不過。

3.3 若是x是一個函數或者對象

根據規範:

  • 先取let then = x.then,若是在這個過程出現拋出了異常,就reject(e)
  • 接上,若是咱們上一步賦值的then是一個函數
    • 使用call調用它,把x做爲this傳給then,它的第一個參數爲resolvePromise,第二個參數爲rejectPromise
      • 若是resolvePromise被調用時傳遞了參數y,則遞歸調用resolve_promise
      • 若是rejectPromise被調用時傳遞了參數r,reject(r)
      • resolvePromise和rejectPromise若是都被調用了,先調用的那個做數,後面調用的那個被忽略
    • 若是用call調用then時出現了異常
      • 若是此時resolvePromise或者rejectPromise都被調用了,忽略這個異常
      • 若是沒有,reject這個異常
  • 若是第一步賦值的then不是一個函數,直接resolve(x)

你可能看不懂規範上說的這些空間是幹嗎的,其實它是對Promise實現方案作的一個兼容處理。咱們知道,Promise並無官方實現,只有規範和測試用例,它有多種實現,好比bluebird和Q,若是我在使用時,同時用了bluebird和Q,就須要作兼容處理。

這裏的x就多是bluebird或者Q的實例。

那我如何判斷它是其它Promise的實現呢?看它有沒有一個then方法,全部Promise實現都有合乎規定的then方法。若是有then就會走到這裏的邏輯。若是x是一個包含then方法的普通對象,也會走到這裏。因此這裏纔會有這麼多的判斷,還有遞歸。

直接寫MyPromise:

function resolve_promise(promise2, x, resolve, reject) {
  // x就是promise2的狀況
  if (x === promise2) {
    reject(new TypeError('Chaining cycle detected for promise'))
    return
  }
  // x是MyPromise實例的狀況
  if (x instanceof MyPromise) {
    x.then(function(v) {
      resolve_promise(promise2, v, resolve, reject)
    }, function(t) {
      reject(t)
    })
    return
  }
  // x 是對象或者函數
  if (x !== null && (typeof x === 'function' || typeof x === 'object')) {
    // 開關
    // 控制resolvePromise和rejectPromise還有catch裏reject的調用
    let called = false
    try { // x.then可能有異常,須要捕獲
      let then = x.then
      if (typeof then === 'function') {
        // 有then方法,則調用,若是then方法並無實際resolvePromise
        // 或者rejectPromise參數的話,promise2永遠都是pending狀態
        // 由於resolve和reject永遠都不可能執行
        then.call(x, function resolvePromise(y) {
          if (called) return
          called = true
          resolve_promise(promise2, y, resolve, reject)
        }, function rejectPromise(r) {
          if (called) return
          called = true
          reject(r) 
        })
      } else {
        // 若是then不是一個函數直接resolve
        resolve(x)
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    resolve(x)
  }
}
複製代碼

看到這裏,你就能夠找出Promise的一個"bug"了,請看下面場景的代碼:

let promise1 = new Promise(function(resolve, reject) {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})
let promise2 = promise1.then(function(res) {
  // 這裏返回一個包括then方法的對象
  // 可是這個then方法啥都沒作
  // 致使promise2在構造過程當中resolve或者reject永遠都沒執行
  return { 
    then: function() {} 
  }
})
promise2.then(res => {
  // 這裏永遠都不會執行
  console.log(res)
})
// promise2永遠都是pending狀態
console.log(promise2)
複製代碼

你能夠把上面的代碼粘到瀏覽器裏試一下~

3.4 若是x不是一個函數或者對象

那直接resolve(x)便可,在上面已經有了。

5、改個錯誤

請看咱們已經寫的then函數裏:

MyPromise.prototype.then = function (onResolved, onRejected) {
  let promise2
  if (this.status === 'pending') {
    promise2 = new MyPromise((resolve, reject) => {
      function successFn(value) {
        let x = onResolved(value)
        resolve_promise(promise2, x, resolve, reject)
      }

      function failFn(reason) {
        let x = onRejected(reason)
        resolve_promise(promise2, x, resolve, reject)
      }

      this.resolvedCallbacks.push(successFn)
      this.rejectedCallbacks.push(failFn)
    })
  }

  if (this.status === 'fulfilled') {
    promise2 = new MyPromise((resolve, reject) => {
      let x = onResolved(this.data)
      // 看這裏,看下面這行
      resolve_promise(promise2, x, resolve, reject)
    })
  }

  if (this.status === 'rejected') {
    promise2 = new Promise((resolve, reject) => {
      let x = onRejected(this.reason)
      // 看這裏,看下面這行
      resolve_promise(promise2, x, resolve, reject)
    })
  }

  return promise2
}
複製代碼

this.status === 'fulfilled'this.status === 'rejected'這兩個分支裏執行resolve_promise是拿不到promise2這個實例的,由於它沒還構造完成,是undefined ,因此這裏要加個setTimeout才行。並且,根據規範,onResolved和onRejected必須異步執行呢。

注意,this.stauts === 'pending'的那個不用哦,由於它執行的時候就是異步的。

改爲下面這樣:

if (this.status === 'fulfilled') {
  promise2 = new MyPromise((resolve, reject) => {
    setTimeout(() => {
      let x = onResolved(this.data)
      resolve_promise(promise2, x, resolve, reject)
    })
  })
}

if (this.status === 'rejected') {
  promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      let x = onRejected(this.reason)
      resolve_promise(promise2, x, resolve, reject)
    })
  })
}
複製代碼

6、總結

其實,如今咱們已經基本寫出一個Promise了!可是你能夠會問,Promise.all Promise.race 實例上的catch 等方法如今尚未啊!不用擔憂,只要完成了then,一個Promise就完成了百分之八十了,以上常的的幾個API都是小意思,要實現分分鐘~ 固然,它還有點小瑕疵,這些問題,還有測試,咱們下篇文章完成!

完成的代碼在這裏:

function MyPromise(executor) {
  this.status = 'pending'
  this.data = undefined
  this.reason = undefined
  this.resolvedCallbacks = []
  this.rejectedCallbacks = []

  let resolve = (value) => {
    if (this.status === 'pending') {
      this.status = 'fulfilled'
      this.data = value
      this.resolvedCallbacks.forEach(fn => fn(this.data))
    }
  }
  let reject = (reason) => {
    if (this.status === 'pending') {
      this.status = 'rejected'
      this.reason = reason
      this.rejectedCallbacks.forEach(fn => fn(this.reason))
    }
  }
  executor(resolve, reject)
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  let promise2

  if (this.status === 'pending') {
    promise2 = new MyPromise((resolve, reject) => {
      function successFn(value) {
        let x = onResolved(value)
        resolve_promise(promise2, x, resolve, reject)
      }

      function failFn(reason) {
        let x = onRejected(reason)
        resolve_promise(promise2, x, resolve, reject)
      }

      this.resolvedCallbacks.push(successFn)
      this.rejectedCallbacks.push(failFn)
    })
  }

  if (this.status === 'fulfilled') {
    promise2 = new MyPromise((resolve, reject) => {
      setTimeout(() => {
        let x = onResolved(this.data)
        resolve_promise(promise2, x, resolve, reject)
      })
    })
  }

  if (this.status === 'rejected') {
    promise2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        let x = onRejected(this.reason)
        resolve_promise(promise2, x, resolve, reject)
      })
    })
  }

  return promise2
}

function resolve_promise(promise2, x, resolve, reject) {
  if (x === promise2) {
    reject(new TypeError('Chaining cycle detected for promise'))
    return
  }
  if (x instanceof MyPromise) {
    x.then(function(v) {
      resolve_promise(promise2, v, resolve, reject)
    }, function(t) {
      reject(t)
    })
    return
  }
  if (x !== null && (typeof x === 'function' || typeof x === 'object')) {
    let called = false
    try {
      let then = x.then
      if (typeof then === 'function') {
        then.call(x, function resolvePromise(y) {
          if (called) return
          called = true
          resolve_promise(promise2, y, resolve, reject)
        }, function rejectPromise(r) {
          if (called) return
          called = true
          reject(r)
        })
      } else {
        resolve(x)
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    resolve(x)
  }
}

複製代碼

感謝您的閱讀!

相關文章
相關標籤/搜索