包教包會,和你完成一個Promise(三)

1、承上

本篇文章是包教包會,和你實現一個Promise的第三篇文章。由於這個小系列旨在完成一個符合規範的Promise,並且這三篇文章有先後關係,若是沒有看過前兩篇,第三篇看起來會有些莫名其妙。要是沒看過,仍是建議先看下前兩篇。在這裏:javascript

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

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

到目前爲止,咱們的MyPromise雖然能夠用了,可是還有一些地方不符合規範,再就是有些地方還有點問題,咱們來進行逐個修改。github

2、then方法完善

2.1 給參數設置默認值

如今then方法的兩個參數是沒有默認值的,因此若是使用者在調用then方法時,沒有傳遞參數,後面咱們使用onResolved或者onRejected時,程序會報onResolved/onRejected is not a function,這會致使咱們的Promise被迫終止,因此要給它們加上默認值。npm

MyPromise.prototype.then = function (onResolved, onRejected) {
  // 看這裏,設置onResovled的默認值
  if (typeof onResolved !== 'function') {
    onResolved = function(value) {
      return value
    }
  }
  // 看這裏,設置onRejected的默認值
  if (typeof onRejected !== 'function') {
    onRejected = function(reason) {
      throw reason
    }
  }

  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 MyPromise((resolve, reject) => {
      setTimeout(() => {
        let x = onRejected(this.reason)
        resolve_promise(promise2, x, resolve, reject)
      })
    })
  }

  return promise2
}
複製代碼

上面經過tyepof判斷onResovled或者onRejected是否是function類型,若是不是,就給一個函數。這樣,不只避免了不傳值的狀況,也解決了使用隨便亂傳參數的問題。數組

對於onResolved的方法,咱們聲明一個函數直接返回value便可,對於onRejected的方法,咱們拋一個錯誤出來。這樣promise2也就能夠經過then方法拿到它們返回或者拋出的錯誤的。promise

2.2 onResolved/onRejected的執行問題

咱們來看這裏的代碼,這裏只是其中一處:bash

if (this.status === 'fulfilled') {
  promise2 = new MyPromise((resolve, reject) => {
    setTimeout(() => {
      // 看這裏看這裏
      let x = onResolved(this.data)
      resolve_promise(promise2, x, resolve, reject)
    })
  })
}
複製代碼

咱們知道,then方法執行時,用戶會寫兩個函數onResovled和onRejected而且傳進來,而後咱們調用它們來得到當前Promise實例resolve或者reject傳遞過來的值。可是問題來了,由於onResovled和onRejected是用戶實現、咱們調用的,那用戶寫onResolved或者onRejected時寫錯了,裏面語法有問題,執行的時候報錯了,那咱們的程序也會終止掉。因此咱們要針對這種可能性作一個try...catch捕捉。異步

總共有四個地方須要try...catch:函數

MyPromise.prototype.then = function (onResolved, onRejected) {
  if (typeof onResolved !== 'function') {
    onResolved = function(value) {
      return value
    }
  }
  if (typeof onRejected !== 'function') {
    onRejected = function(reason) {
      throw reason
    }
  }
  let promise2

  if (this.status === 'pending') {
    promise2 = new MyPromise((resolve, reject) => {
      function successFn(value) {
        // 這裏要try...catch
        try {
          let x = onResolved(value)
          resolve_promise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      }

      function failFn(reason) {
        // 這裏要try...catch
        try {
          let x = onRejected(reason)
          resolve_promise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      }

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

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

  if (this.status === 'rejected') {
    promise2 = new MyPromise((resolve, reject) => {
      setTimeout(() => {
        // 這裏也要哦
        try {
          let x = onRejected(this.reason)
          resolve_promise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      })
    })
  }

  return promise2
}
複製代碼

總共四個地方,若是onResolved或者onRejected執行報錯,會被catch到,而且直接用reject把promise2標記爲失敗狀態。then方法改完了,下面構造函數也要小小改一下。

3、構造函數完善

構造函數有個地方要修改:resolve和reject裏的內容須要異步執行。 爲啥要這樣寫,其實我也不清楚,由於從邏輯上來看即便不寫彷佛也沒毛病。可是不它們設置成異步執行的話,有6個測試用例通不過。

因此我去看了它的測試用例,你也能夠看看,地址在這裏 :它是這麼測的:

specify("fulfilled after a delay", function (done) {
  var d = deferred();
  var isFulfilled = false;

  d.promise.then(function onFulfilled() {
    assert.strictEqual(isFulfilled, true);
    done();
  });

  setTimeout(function () {
    d.resolve(dummy);
    isFulfilled = true;
  }, 50);
});
複製代碼

根據規範,我只要保證onResolved在resolve以後執行就行,因此如今寫的代碼沒啥問題,但問題是它的測試用例,先聲明瞭一個isFulfilled,而後使用setTimeout調用resolve並將isFulfilled置爲true,並在onResolved判斷一個isFulfilled爲true。

可是,咱們是在resolve函數裏面就已經調用resolve了,因此此時isFufilled仍是false,因此咱們須要給resolve和reject裏的代碼都異步執行才能經過這裏的測試。

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

  let resolve = (value) => {
    // 這裏加個setTimeout
    setTimeout(() => {
      if (this.status === 'pending') {
        this.status = 'fulfilled'
        this.data = value
        this.resolvedCallbacks.forEach(fn => fn(this.data))
      }
    })
  }
  let reject = (reason) => {
    // 這裏也加個setTimeout
    setTimeout(() => {
      if (this.status === 'pending') {
        this.status = 'rejected'
        this.reason = reason
        this.rejectedCallbacks.forEach(fn => fn(this.reason))
      }
    })
  }
  executor(resolve, reject)
}
複製代碼

4、測試

測試很簡單,有專門的測試工具。

npm install -g promises-aplus-tests
複製代碼

同時還要將咱們的MyPromise暴露出去,並提供promise實例和resolve以及reject函數引用。

MyPromise.deferred = function() {
  let dfd = {}
  dfd.promise = new MyPromise(function(resolve, reject) {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}

try {
  module.exports = MyPromise
} catch (e) {}
複製代碼

咱們須要在MyPromise上寫一個靜態方法,執行後返回一個dfd對象。這個對象上有三個屬性:

  • promise: 一個咱們要測試的Promise實例
  • resolve: 這個實例的resolve函數
  • reject: 這個實例的reject函數

好了,到這裏看一下包括測試須要在內的全部的代碼:

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

  let resolve = (value) => {
    setTimeout(() => {
      if (this.status === 'pending') {
        this.status = 'fulfilled'
        this.data = value
        this.resolvedCallbacks.forEach(fn => fn(this.data))
      }
    })
  }
  let reject = (reason) => {
    setTimeout(() => {
      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) {
  if (typeof onResolved !== 'function') {
    onResolved = function(value) {
      return value
    }
  }

  if (typeof onRejected !== 'function') {
    onRejected = function(reason) {
      throw reason
    }
  }

  let promise2

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

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

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

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

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

  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)
  }
}

MyPromise.deferred = function() {
  let dfd = {}
  dfd.promise = new MyPromise(function(resolve, reject) {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}

try {
  module.exports = MyPromise
} catch (e) {

}
複製代碼

開始測試:

promises-aplus-tests MyPromise.js
複製代碼

你將會看到:

測試成功

872個測試用例所有經過!

5、錦上添花

接下來,咱們實現一些很是經常使用的方法,這些方法雖然不在規範裏,可是它們平時也有用武之地!

5.1 MyPromise.all()

這個方法有如下特色:

  • 參數:是一個MyPromise實例組成的數組
  • 返回一個MyPromise實例,不過要等到全部的參數裏全部的promise狀態肯定
  • 參數的實例中,只要有一個是reject,返回的promise就是reject狀態
MyPromise.all = function(arr) {
  return new MyPromise(function(resolve, reject) {
    let result = new Array(arr.length)
    let count = 0
    for(let i=0; i<arr.length; ++i) {
      let currentPromise = arr[i]
      currentPromise.then(function(res) {
        result[i] = res
        count++
        if (count === arr.length) {
          resolve(result)
        }
      }, function(reason) {
        reject(reason)
      })
    }
  }) 
}
複製代碼

很好理解,就很少講了~

5.2 catch()方法

注意,這個方法是實例方法:

  • 接收一個函數做爲參數
  • 當then出現問題時會執行這個函數
MyPromise.prototype.catch = function(failFn) {
  this.then(null, function(reason) {
    failFn(reason)
  })
}
複製代碼

很少說了

5.3 race()方法

注意,這是一個靜態方法:

  • 接收一個promise實例構成的數組,返回一個MyPromise實例
  • 當參數裏的promise第一個狀態確認時,把它做爲成功或者失敗的值返回一個新實例
MyPromise.race = function(arr) {
  return new MyPromise(function(resolve, reject) {
    arr.forEach(promise => {
      promise.then(resolve, reject)
    })
  })
}
複製代碼

完事~

5.4 MyPromise.resolve()

直接返回一個成功狀態的Promise,靜態方法:

MyPromise.resolve = function(value) {
  return new MyPromise(function(resolve, reject) {
    resolve(value)
  })
}
複製代碼

5.5 MyPromise.reject()

直接返回一個失敗狀態的Promise實例,靜態方法:

MyPromise.reject = function(reason) {
  return new MyPromise(function(resolve, reject) {
    reject(reason)
  })
}
複製代碼

後面咱們實現的這些方法是沒有測試用例的哦~

6、起一個帥氣的名字

好不容易完成了一個徹底符合規範的Promise,給它起一個帥氣的名字吧,就叫Zoro吧。Zoro是《海賊王》裏羅羅諾亞·索隆的英文名字,索隆在小時候曾經向古伊娜許諾要成爲世界最強劍豪 ,而如今索隆剛拿到閻魔這把刀,在成爲最強的道路上一路狂奔,因此我以爲這個名字再合適不過了!

zoro

完成的zoro地址在這裏 ,歡迎點贊,感謝您的閱讀!

(完)

相關文章
相關標籤/搜索