剖析Promise内部结构,一步一步实现一个完整的、能经过全部Test case的Promise类

本文写给有必定Promise使用经验的人,若是你尚未使用过Promise,这篇文章可能不适合你,建议先了解Promise的使用html

Promise标准解读

1.只有一个then方法,没有catch,race,all等方法,甚至没有构造函数html5

Promise标准中仅指定了Promise对象的then方法的行为,其它一切咱们常见的方法/函数都并无指定,包括catch,race,all等经常使用方法,甚至也没有指定该如何构造出一个Promise对象,另外then也没有通常实现中(Q, $q等)所支持的第三个参数,通常称onProgressjava

2.then方法返回一个新的Promisegit

Promise的then方法返回一个新的Promise,而不是返回this,此处在下文会有更多解释es6

promise2 = promise1.then(alert)
promise2 != promise1 // true

3.不一样Promise的实现须要能够相互调用(interoperable)github

4.Promise的初始状态为pending,它能够由此状态转换为fulfilled(本文为了一致把此状态叫作resolved)或者rejected,一旦状态肯定,就不能够再次转换为其它状态,状态肯定的过程称为settleweb

5.更具体的标准见这里npm

一步一步实现一个Promise

下面咱们就来一步一步实现一个Promise数组

构造函数

由于标准并无指定如何构造一个Promise对象,因此咱们一样以目前通常Promise实现中通用的方法来构造一个Promise对象,也是ES6原生Promise里所使用的方式,即:promise

// Promise构造函数接收一个executor函数,executor函数执行完同步或异步操做后,调用它的两个参数resolve和reject
var promise = new Promise(function(resolve, reject) {
  /*
    若是操做成功,调用resolve并传入value
    若是操做失败,调用reject并传入reason
  */
})

咱们先实现构造函数的框架以下:

function Promise(executor) {
  var self = this
  self.status = 'pending' // Promise当前的状态
  self.data = undefined  // Promise的值
  self.onResolvedCallback = [] // Promise resolve时的回调函数集,由于在Promise结束以前有可能有多个回调添加到它上面
  self.onRejectedCallback = [] // Promise reject时的回调函数集,由于在Promise结束以前有可能有多个回调添加到它上面

  executor(resolve, reject) // 执行executor并传入相应的参数
}

上面的代码基本实现了Promise构造函数的主体,但目前还有两个问题:

1.咱们给executor函数传了两个参数:resolve和reject,这两个参数目前尚未定义

2.executor有可能会出错(throw),相似下面这样,而若是executor出错,Promise应该被其throw出的值reject:

new Promise(function(resolve, reject) {
  throw 2
})

因此咱们须要在构造函数里定义resolve和reject这两个函数:

function Promise(executor) {
  var self = this
  self.status = 'pending' // Promise当前的状态
  self.data = undefined  // Promise的值
  self.onResolvedCallback = [] // Promise resolve时的回调函数集,由于在Promise结束以前有可能有多个回调添加到它上面
  self.onRejectedCallback = [] // Promise reject时的回调函数集,由于在Promise结束以前有可能有多个回调添加到它上面

  function resolve(value) {
    // TODO
  }

  function reject(reason) {
    // TODO
  }

  try { // 考虑到执行executor的过程当中有可能出错,因此咱们用try/catch块给包起来,而且在出错后以catch到的值reject掉这个Promise
    executor(resolve, reject) // 执行executor
  } catch(e) {
    reject(e)
  }
}

有人可能会问,resolve和reject这两个函数能不能不定义在构造函数里呢?考虑到咱们在executor函数里是以resolve(value),reject(reason)的形式调用的这两个函数,而不是以resolve.call(promise, value),reject.call(promise, reason)这种形式调用的,因此这两个函数在调用时的内部也必然有一个隐含的this,也就是说,要么这两个函数是通过bind后传给了executor,要么它们定义在构造函数的内部,使用self来访问所属的Promise对象。因此若是咱们想把这两个函数定义在构造函数的外部,确实是能够这么写的:

function resolve() {
  // TODO
}
function reject() {
  // TODO
}
function Promise(executor) {
  try {
    executor(resolve.bind(this), reject.bind(this))
  } catch(e) {
    reject.bind(this)(e)
  }
}

可是众所周知,bind也会返回一个新的函数,这么一来仍是至关于每一个Promise对象都有一对属于本身的resolve和reject函数,就跟写在构造函数内部没什么区别了,因此咱们就直接把这两个函数定义在构造函数里面了。不过话说回来,若是浏览器对bind的所优化,使用后一种形式应该能够提高一下内存使用效率。

另外咱们这里的实现并无考虑隐藏this上的变量,这使得这个Promise的状态能够在executor函数外部被改变,在一个靠谱的实现里,构造出的Promise对象的状态和最终结果应当是没法从外部更改的。

接下来,咱们实现resolve和reject这两个函数

function Promise(executor) {
  // ...

  function resolve(value) {
    if (self.status === 'pending') {
      self.status = 'resolved'
      self.data = value
      for(var i = 0; i < self.onResolvedCallback.length; i++) {
        self.onResolvedCallback[i](value)
      }
    }
  }

  function reject(reason) {
    if (self.status === 'pending') {
      self.status = 'rejected'
      self.data = reason
      for(var i = 0; i < self.onRejectedCallback.length; i++) {
        self.onRejectedCallback[i](reason)
      }
    }
  }

  // ...
}

基本上就是在判断状态为pending以后把状态改成相应的值,并把对应的value和reason存在self的data属性上面,以后执行相应的回调函数,逻辑很简单,这里就很少解释了。

then方法

Promise对象有一个then方法,用来注册在这个Promise状态肯定后的回调,很明显,then方法须要写在原型链上。then方法会返回一个Promise,关于这一点,Promise/A+标准并无要求返回的这个Promise是一个新的对象,但在Promise/A标准中,明确规定了then要返回一个新的对象,目前的Promise实现中then几乎都是返回一个新的Promise(详情)对象,因此在咱们的实现中,也让then返回一个新的Promise对象。

关于这一点,我认为标准中是有一点矛盾的:

标准中说,若是promise2 = promise1.then(onResolved, onRejected)里的onResolved/onRejected返回一个Promise,则promise2直接取这个Promise的状态和值为己用,但考虑以下代码:

promise2 = promise1.then(function foo(value) {
  return Promise.reject(3)
})

此处若是foo运行了,则promise1的状态必然已经肯定且为resolved,若是then返回了this(即promise2 === promise1),说明promise2和promise1是同一个对象,而此时promise1/2的状态已经肯定,没有办法再取Promise.reject(3)的状态和结果为己用,由于Promise的状态肯定后就不可再转换为其它状态。

另外每一个Promise对象均可以在其上屡次调用then方法,而每次调用then返回的Promise的状态取决于那一次调用then时传入参数的返回值,因此then不能返回this,由于then每次返回的Promise的结果都有可能不一样。

下面咱们来实现then方法:

// then方法接收两个参数,onResolved,onRejected,分别为Promise成功或失败后的回调
Promise.prototype.then = function(onResolved, onRejected) {
  var self = this
  var promise2

  // 根据标准,若是then的参数不是function,则咱们须要忽略它,此处以以下方式处理
  onResolved = typeof onResolved === 'function' ? onResolved : function(v) {}
  onRejected = typeof onRejected === 'function' ? onRejected : function(r) {}

  if (self.status === 'resolved') {
    return promise2 = new Promise(function(resolve, reject) {

    })
  }

  if (self.status === 'rejected') {
    return promise2 = new Promise(function(resolve, reject) {

    })
  }

  if (self.status === 'pending') {
    return promise2 = new Promise(function(resolve, reject) {

    })
  }
}

Promise总共有三种可能的状态,咱们分三个if块来处理,在里面分别都返回一个new Promise。

根据标准,咱们知道,对于以下代码,promise2的值取决于then里面函数的返回值:

promise2 = promise1.then(function(value) {
  return 4
}, function(reason) {
  throw new Error('sth went wrong')
})

若是promise1被resolve了,promise2的将被4 resolve,若是promise1被reject了,promise2将被new Error('sth went wrong') reject,更多复杂的状况再也不详述。

因此,咱们须要在then里面执行onResolved或者onRejected,并根据返回值(标准中记为x)来肯定promise2的结果,而且,若是onResolved/onRejected返回的是一个Promise,promise2将直接取这个Promise的结果:

Promise.prototype.then = function(onResolved, onRejected) {
  var self = this
  var promise2

  // 根据标准,若是then的参数不是function,则咱们须要忽略它,此处以以下方式处理
  onResolved = typeof onResolved === 'function' ? onResolved : function(value) {}
  onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {}

  if (self.status === 'resolved') {
    // 若是promise1(此处即为this/self)的状态已经肯定而且是resolved,咱们调用onResolved
    // 由于考虑到有可能throw,因此咱们将其包在try/catch块里
    return promise2 = new Promise(function(resolve, reject) {
      try {
        var x = onResolved(self.data)
        if (x instanceof Promise) { // 若是onResolved的返回值是一个Promise对象,直接取它的结果作为promise2的结果
          x.then(resolve, reject)
        }
        resolve(x) // 不然,以它的返回值作为promise2的结果
      } catch (e) {
        reject(e) // 若是出错,以捕获到的错误作为promise2的结果
      }
    })
  }

  // 此处与前一个if块的逻辑几乎相同,区别在于所调用的是onRejected函数,就再也不作过多解释
  if (self.status === 'rejected') {
    return promise2 = new Promise(function(resolve, reject) {
      try {
        var x = onRejected(self.data)
        if (x instanceof Promise) {
          x.then(resolve, reject)
        }
      } catch (e) {
        reject(e)
      }
    })
  }

  if (self.status === 'pending') {
  // 若是当前的Promise还处于pending状态,咱们并不能肯定调用onResolved仍是onRejected,
  // 只能等到Promise的状态肯定后,才能确实如何处理。
  // 因此咱们须要把咱们的**两种状况**的处理逻辑作为callback放入promise1(此处即this/self)的回调数组里
  // 逻辑自己跟第一个if块内的几乎一致,此处不作过多解释
    return promise2 = new Promise(function(resolve, reject) {
      self.onResolvedCallback.push(function(value) {
        try {
          var x = onResolved(self.data)
          if (x instanceof Promise) {
            x.then(resolve, reject)
          }
        } catch (e) {
          reject(e)
        }
      })

      self.onRejectedCallback.push(function(reason) {
        try {
          var x = onRejected(self.data)
          if (x instanceof Promise) {
            x.then(resolve, reject)
          }
        } catch (e) {
          reject(e)
        }
      })
    })
  }
}

// 为了下文方便,咱们顺便实现一个catch方法
Promise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}

至此,咱们基本实现了Promise标准中所涉及到的内容,但还有几个问题:

1.不一样的Promise实现之间须要无缝的可交互,即Q的Promise,ES6的Promise,和咱们实现的Promise之间以及其它的Promise实现,应该而且是有必要无缝相互调用的,好比:

// 此处用MyPromise来表明咱们实现的Promise
new MyPromise(function(resolve, reject) { // 咱们实现的Promise
  setTimeout(function() {
    resolve(42)
  }, 2000)
}).then(function() {
  return new Promise.reject(2) // ES6的Promise
}).then(function() {
  return Q.all([ // Q的Promise
    new MyPromise(resolve=>resolve(8)), // 咱们实现的Promise
    new Promise.resolve(9), // ES6的Promise
    Q.resolve(9) // Q的Promise
  ])
})

咱们前面实现的代码并无处理这样的逻辑,咱们只判断了onResolved/onRejected的返回值是否为咱们实现的Promise的实例,并无作任何其它的判断,因此上面这样的代码目前是没有办法在咱们的Promise里正确运行的。

2.下面这样的代码目前也是没办法处理的:

new Promise(resolve=>resolve(8))
  .then()
  .then()
  .then(function foo(value) {
    alert(value)
  })

正确的行为应该是alert出8,而若是拿咱们的Promise,运行上述代码,将会alert出undefined。这种行为称为穿透,即8这个值会穿透两个then(说Promise更为准确)到达最后一个then里的foo函数里,成为它的实参,最终将会alert出8。

下面咱们首先处理简单的状况,值的穿透

Promise值的穿透

经过观察,会发现咱们但愿下面这段代码

new Promise(resolve=>resolve(8))
  .then()
  .catch()
  .then(function(value) {
    alert(value)
  })

跟下面这段代码的行为是同样的

new Promise(resolve=>resolve(8))
  .then(function(value){
    return value
  })
  .catch(function(reason){
    throw reason
  })
  .then(function(value) {
    alert(value)
  })

因此若是想要把then的实参留空且让值能够穿透到后面,意味着then的两个参数的默认值分别为function(value) {return value},function(reason) {throw reason}。
因此咱们只须要把then里判断onResolved和onRejected的部分改为以下便可:

onResolved = typeof onResolved === 'function' ? onResolved : function(value) {return value}
onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {throw reason}

因而Promise神奇的值的穿透也没有那么黑魔法,只不过是then默认参数就是把值日后传或者抛

不一样Promise的交互

关于不一样Promise间的交互,其实标准里是有说明的,其中详细指定了如何经过then的实参返回的值来决定promise2的状态,咱们只须要按照标准把标准的内容转成代码便可。

这里简单解释一下标准:

即咱们要把onResolved/onRejected的返回值,x,当成一个多是Promise的对象,也即标准里所说的thenable,并以最保险的方式调用x上的then方法,若是你们都按照标准实现,那么不一样的Promise之间就能够交互了。而标准为了保险起见,即便x返回了一个带有then属性但并不遵循Promise标准的对象(好比说这个x把它then里的两个参数都调用了,同步或者异步调用(PS,原则上then的两个参数须要异步调用,下文会讲到),或者是出错后又调用了它们,或者then根本不是一个函数),也能尽量正确处理。

关于为什么须要不一样的Promise实现可以相互交互,我想缘由应该是显然的,Promise并非JS一早就有的标准,不一样第三方的实现之间是并不相互知晓的,若是你使用的某一个库中封装了一个Promise实现,想象一下若是它不能跟你本身使用的Promise实现交互的场景。。。

建议各位对照着标准阅读如下代码,由于标准对此说明的很是详细,因此你应该可以在任意一个Promise实现中找到相似的代码:

/*
resolvePromise函数即为根据x的值来决定promise2的状态的函数
也即标准中的[Promise Resolution Procedure](https://promisesaplus.com/#point-47)
x为`promise2 = promise1.then(onResolved, onRejected)`里`onResolved/onRejected`的返回值
`resolve`和`reject`其实是`promise2`的`executor`的两个实参,由于很难挂在其它的地方,因此一并传进来。
相信各位必定能够对照标准把标准转换成代码,这里就只标出代码在标准中对应的位置,只在必要的地方作一些解释
*/
function resolvePromise(promise2, x, resolve, reject) {
  var then
  var thenCalledOrThrow = false

  if (promise2 === x) { // 对应标准2.3.1节
    return reject(new TypeError('Chaining cycle detected for promise!'))
  }

  if (x instanceof Promise) { // 对应标准2.3.2节
    // 若是x的状态尚未肯定,那么它是有可能被一个thenable决定最终状态和值的
    // 因此这里须要作一下处理,而不能一律的觉得它会被一个“正常”的值resolve
    if (x.status === 'pending') {
      x.then(function(value) {
        resolvePromise(promise2, value, resolve, reject)
      }, reject)
    } else { // 但若是这个Promise的状态已经肯定了,那么它确定有一个“正常”的值,而不是一个thenable,因此这里直接取它的状态
      x.then(resolve, reject)
    }
    return
  }

  if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) { // 2.3.3
    try {

      // 2.3.3.1 由于x.then有多是一个getter,这种状况下屡次读取就有可能产生反作用
      // 即要判断它的类型,又要调用它,这就是两次读取
      then = x.then 
      if (typeof then === 'function') { // 2.3.3.3
        then.call(x, function rs(y) { // 2.3.3.3.1
          if (thenCalledOrThrow) return // 2.3.3.3.3 即这三处谁选执行就以谁的结果为准
          thenCalledOrThrow = true
          return resolvePromise(promise2, y, resolve, reject) // 2.3.3.3.1
        }, function rj(r) { // 2.3.3.3.2
          if (thenCalledOrThrow) return // 2.3.3.3.3 即这三处谁选执行就以谁的结果为准
          thenCalledOrThrow = true
          return reject(r)
        })
      } else { // 2.3.3.4
        resolve(x)
      }
    } catch (e) { // 2.3.3.2
      if (thenCalledOrThrow) return // 2.3.3.3.3 即这三处谁选执行就以谁的结果为准
      thenCalledOrThrow = true
      return reject(e)
    }
  } else { // 2.3.4
    resolve(x)
  }
}

而后咱们使用这个函数的调用替换then里几处判断x是否为Promise对象的位置便可,见下方完整代码。

最后,咱们刚刚说到,原则上,promise.then(onResolved, onRejected)里的这两相函数须要异步调用,关于这一点,标准里也有说明

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.

因此咱们须要对咱们的代码作一点变更,即在四个地方加上setTimeout(fn, 0),这点会在完整的代码中注释,请各位自行发现。

事实上,即便你不参照标准,最终你在自测试时也会发现若是then的参数不以异步的方式调用,有些状况下Promise会不按预期的方式行为,经过不断的自测,最终你必然会让then的参数异步执行,让executor函数当即执行。本人在一开始实现Promise时就没有参照标准,而是本身凭经验测试,最终发现的这个问题。

至此,咱们就实现了一个的Promise,完整代码以下:

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

function Promise(executor) {
  var self = this

  self.status = 'pending'
  self.onResolvedCallback = []
  self.onRejectedCallback = []

  function resolve(value) {
    if (value instanceof Promise) {
      return value.then(resolve, reject)
    }
    setTimeout(function() { // 异步执行全部的回调函数
      if (self.status === 'pending') {
        self.status = 'resolved'
        self.data = value
        for (var i = 0; i < self.onResolvedCallback.length; i++) {
          self.onResolvedCallback[i](value)
        }
      }
    })
  }

  function reject(reason) {
    setTimeout(function() { // 异步执行全部的回调函数
      if (self.status === 'pending') {
        self.status = 'rejected'
        self.data = reason
        for (var i = 0; i < self.onRejectedCallback.length; i++) {
          self.onRejectedCallback[i](reason)
        }
      }
    })
  }

  try {
    executor(resolve, reject)
  } catch (reason) {
    reject(reason)
  }
}

function resolvePromise(promise2, x, resolve, reject) {
  var then
  var thenCalledOrThrow = false

  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise!'))
  }

  if (x instanceof Promise) {
    if (x.status === 'pending') { //because x could resolved by a Promise Object
      x.then(function(v) {
        resolvePromise(promise2, v, resolve, reject)
      }, reject)
    } else { //but if it is resolved, it will never resolved by a Promise Object but a static value;
      x.then(resolve, reject)
    }
    return
  }

  if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
    try {
      then = x.then //because x.then could be a getter
      if (typeof then === 'function') {
        then.call(x, function rs(y) {
          if (thenCalledOrThrow) return
          thenCalledOrThrow = true
          return resolvePromise(promise2, y, resolve, reject)
        }, function rj(r) {
          if (thenCalledOrThrow) return
          thenCalledOrThrow = true
          return reject(r)
        })
      } else {
        resolve(x)
      }
    } catch (e) {
      if (thenCalledOrThrow) return
      thenCalledOrThrow = true
      return reject(e)
    }
  } else {
    resolve(x)
  }
}

Promise.prototype.then = function(onResolved, onRejected) {
  var self = this
  var promise2
  onResolved = typeof onResolved === 'function' ? onResolved : function(v) {
    return v
  }
  onRejected = typeof onRejected === 'function' ? onRejected : function(r) {
    throw r
  }

  if (self.status === 'resolved') {
    return promise2 = new Promise(function(resolve, reject) {
      setTimeout(function() { // 异步执行onResolved
        try {
          var x = onResolved(self.data)
          resolvePromise(promise2, x, resolve, reject)
        } catch (reason) {
          reject(reason)
        }
      })
    })
  }

  if (self.status === 'rejected') {
    return promise2 = new Promise(function(resolve, reject) {
      setTimeout(function() { // 异步执行onRejected
        try {
          var x = onRejected(self.data)
          resolvePromise(promise2, x, resolve, reject)
        } catch (reason) {
          reject(reason)
        }
      })
    })
  }

  if (self.status === 'pending') {
    // 这里之因此没有异步执行,是由于这些函数必然会被resolve或reject调用,而resolve或reject函数里的内容已经是异步执行,构造函数里的定义
    return promise2 = new Promise(function(resolve, reject) {
      self.onResolvedCallback.push(function(value) {
        try {
          var x = onResolved(value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (r) {
          reject(r)
        }
      })

      self.onRejectedCallback.push(function(reason) {
          try {
            var x = onRejected(reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (r) {
            reject(r)
          }
        })
    })
  }
}

Promise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}

Promise.deferred = Promise.defer = function() {
  var dfd = {}
  dfd.promise = new Promise(function(resolve, reject) {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}

测试

如何肯定咱们实现的Promise符合标准呢?Promise有一个配套的测试脚本,只须要咱们在一个CommonJS的模块中暴露一个deferred方法(即exports.deferred方法),就能够了,代码见上述代码的最后。而后执行以下代码便可执行测试:

npm i -g promises-aplus-tests
promises-aplus-tests Promise.js

关于Promise的其它问题

Promise的性能问题

可能各位看官会以为奇怪,Promise能有什么性能问题呢?并无大量的计算啊,几乎都是处理逻辑的代码。

理论上说,不能叫作“性能问题”,而只是有可能出现的延迟问题。什么意思呢,记得刚刚咱们说须要把4块代码包在setTimeout里吧,先考虑以下代码:

var start = +new Date()
function foo() {
  setTimeout(function() {
    console.log('setTimeout')
    if((+new Date) - start < 1000) {
      foo()
    }
  })
}
foo()

运行上面的代码,会打印出多少次'setTimeout'呢,各位能够本身试一下,不出意外的话,应该是250次左右,我刚刚运行了一次,是241次。这说明,上述代码中两次setTimeout运行的时间间隔约是4ms(另外,setInterval也是同样的),实事上,这正是浏览器两次Event Loop之间的时间间隔,相关标准各位能够自行查阅。另外,在Node中,这个时间间隔跟浏览器不同,通过个人测试,是1ms。

单单一个4ms的延迟可能在通常的web应用中并不会有什么问题,可是考虑极端状况,咱们有20个Promise链式调用,加上代码运行的时间,那么这个链式调用的第一行代码跟最后一行代码的运行极可能会超过100ms,若是这之间没有对UI有任何更新的话,虽然本质上没有什么性能问题,但可能会形成必定的卡顿或者闪烁,虽然在web应用中这种情形并不常见,可是在Node应用中,确实是有可能出现这样的case的,因此一个可以应用于生产环境的实现有必要把这个延迟消除掉。在Node中,咱们能够调用process.nextTick或者setImmediate(Q就是这么作的),在浏览器中具体如何作,已经超出了本文的讨论范围,总的来讲,就是咱们须要实现一个函数,行为跟setTimeout同样,但它须要异步且尽早的调用全部已经加入队列的函数,这里有一个实现。

如何中止一个Promise链?

在一些场景下,咱们可能会遇到一个较长的Promise链式调用,在某一步中出现的错误让咱们彻底没有必要去运行链式调用后面全部的代码,相似下面这样(此处略去了then/catch里的函数):

new Promise(function(resolve, reject) {
  resolve(42)
})
  .then(function(value) {
    // "Big ERROR!!!"
  })
  .catch()
  .then()
  .then()
  .catch()
  .then()

假设这个Big ERROR!!!的出现让咱们彻底没有必要运行后面全部的代码了,但链式调用的后面即有catch,也有then,不管咱们是return仍是throw,都不可避免的会进入某一个catch或then里面,那有没有办法让这个链式调用在Big ERROR!!!的后面就停掉,彻底不去执行链式调用后面全部回调函数呢?

一开始遇到这个问题的时候我也百思不得其解,在网上搜遍了也没有结果,有人说能够在每一个catch里面判断Error的类型,若是本身处理不了就接着throw,也有些其它办法,但老是要对现有代码进行一些改动而且全部的地方都要遵循这些约定,甚是麻烦。

然而当我从一个实现者的角度看问题时,确实找到了答案,就是在发生Big ERROR后return一个Promise,但这个Promise的executor函数什么也不作,这就意味着这个Promise将永远处于pending状态,因为then返回的Promise会直接取这个永远处于pending状态的Promise的状态,因而返回的这个Promise也将一直处于pending状态,后面的代码也就一直不会执行了,具体代码以下:

new Promise(function(resolve, reject) {
  resolve(42)
})
  .then(function(value) {
    // "Big ERROR!!!"
    return new Promise(function(){})
  })
  .catch()
  .then()
  .then()
  .catch()
  .then()

这种方式看起来有些山寨,它也确实解决了问题。但它引入的一个新问题就是链式调用后面的全部回调函数都没法被垃圾回收器回收(在一个靠谱的实现里,Promise应该在执行完全部回调后删除对全部回调函数的引用以让它们能被回收,在前文的实现里,为了减小复杂度,并无作这种处理),但若是咱们不使用匿名函数,而是使用函数定义或者函数变量的话,在须要屡次执行的Promise链中,这些函数也都只有一份在内存中,不被回收也是能够接受的。

咱们能够将返回一个什么也不作的Promise封装成一个有语义的函数,以增长代码的可读性:

Promise.cancel = Promise.stop = function() {
  return new Promise(function(){})
}

而后咱们就能够这么使用了:

new Promise(function(resolve, reject) {
  resolve(42)
})
  .then(function(value) {
    // "Big ERROR!!!"
    return Promise.stop()
  })
  .catch()
  .then()
  .then()
  .catch()
  .then()

看起来是否是有语义的多?

Promise链上返回的最后一个Promise出错了怎么办?

考虑以下代码:

new Promise(function(resolve) {
  resolve(42)
})
  .then(function(value) {
    alter(value)
  })

乍一看好像没什么问题,但运行这段代码的话你会发现什么现象也不会发生,既不会alert出42,也不会在控制台报错,怎么回事呢。细看最后一行,alert被打成了alter,那为何控制台也没有报错呢,由于alter所在的函数是被包在try/catch块里的,alter这个变量找不到就直接抛错了,这个错就正好成了then返回的Promise的rejection reason。

也就是说,在Promise链的最后一个then里出现的错误,很是难以发现,有文章指出,能够在全部的Promise链的最后都加上一个catch,这样出错后就能被捕获到,这种方法确实是可行的,可是首先在每一个地方都加上几乎相同的代码,违背了DRY原则,其次也至关的繁琐。另外,最后一个catch依然返回一个Promise,除非你能保证这个catch里的函数再也不出错,不然问题依然存在。在Q中有一个方法叫done,把这个方法链到Promise链的最后,它就可以捕获前面未处理的错误,这其实跟在每一个链后面加上catch没有太大的区别,只是由框架来作了这件事,至关于它提供了一个不会出错的catch链,咱们能够这么实现done方法:

Promise.prototype.done = function(){
  return this.catch(function(e) { // 此处必定要确保这个函数不能再出错
    console.error(e)
  })
}

但是,能不能在不加catch或者done的状况下,也可以让开发者发现Promise链最后的错误呢?答案依然是确定的。

咱们能够在一个Promise被reject的时候检查这个Promise的onRejectedCallback数组,若是它为空,则说明它的错误将没有函数处理,这个时候,咱们须要把错误输出到控制台,让开发者能够发现。如下为具体实现:

function reject(reason) {
  setTimeout(function() {
    if (self.status === 'pending') {
      self.status = 'rejected'
      self.data = reason
      if (self.onRejectedCallback.length === 0) {
        console.error(reason)
      }
      for (var i = 0; i < self.rejectedFn.length; i++) {
        self.rejectedFn[i](reason)
      }
    }
  })
}

上面的代码对于如下的Promise链也能处理的很好:

new Promise(function(){ // promise1
  reject(3)
})
  .then() // returns promise2
  .then() // returns promise3
  .then() // returns promise4

看起来,promise1,2,3,4都没有处理函数,那是否是会在控制台把这个错误输出4次呢,并不会,实际上,promise1,2,3都隐式的有处理函数,就是then的默认参数,各位应该还记得then的默认参数最终是被push到了Promise的callback数组里。只有promise4是真的没有任何callback,由于压根就没有调用它的then方法。

事实上,Bluebird和ES6 Promise都作了相似的处理,在Promise被reject但又没有callback时,把错误输出到控制台。

Q使用了done方法来达成相似的目的,$q在最新的版本中也加入了相似的功能。

Angular里的$q跟其它Promise的交互

通常来讲,咱们不会在Angular里使用其它的Promise,由于Angular已经集成了$q,但有些时候咱们在Angular里须要用到其它的库(好比LeanCloud的JS SDK),而这些库或是封装了ES6的Promise,或者是本身实现了Promise,这时若是你在Angular里使用这些库,就有可能发现视图跟Model不一样步。究其缘由,是由于$q已经集成了Angular的digest loop机制,在Promise被resolve或reject时触发digest,而其它的Promise显然是不会集成的,因此若是你运行下面这样的代码,视图是不会同步的:

app.controller(function($scope) {
  Promise.resolve(42).then(function(value) {
    $scope.value = value
  })
})

Promise结束时并不会触发digest,因此视图没有同步。$q上正好有个when方法,它能够把其它的Promise转换成$q的Promise(有些Promise实现中提供了Promise.cast函数,用于将一个thenable转换为它的Promise),问题就解决了:

app.controller(function($scope, $q) {
  $q.when(Promise.resolve(42)).then(function(value) {
    $scope.value = value
  })
})

固然也有其它的解决方案好比在其它Promise的链的最后加一个digest,相似下面这样:

Promise.prototype.$digest = function() {
  $rootScope.$digest()
  return this
}
// 而后这么使用
OtherPromise
  .resolve(42)
  .then(function(value) {
    $scope.value = value
  })
  .$digest()

由于使用场景并很少,此处不作深刻讨论。

出错时,是用throw new Error()仍是用return Promise.reject(new Error())呢?

这里我以为主要从性能和编码的温馨度角度考虑:

性能方面,throw new Error()会使代码进入catch块里的逻辑(还记得咱们把全部的回调都包在try/catch里了吧),传说throw用多了会影响性能,由于一但throw,代码就有可能跳到不可预知的位置。

但考虑到onResolved/onRejected函数是直接被包在Promise实现里的try里,出错后就直接进入了这个try对应 的catch块,代码的跳跃“幅度”相对较小,我认为这里的性能损失能够忽略不记。有机会能够测试一下。

而使用Promise.reject(new Error()),则须要构造一个新的Promise对象(里面包含2个数组,4个函数:resolve/reject,onResolved/onRejected),也会花费必定的时间和内存。

而从编码温馨度的角度考虑,出错用throw,正常时用return,能够比较明显的区分出错与正常,throw和return又同为关键字,用来处理对应的状况也显得比较对称(-_-)。另外在通常的编辑器里,Promise.reject不会被高亮成与throw和return同样的颜色。最后,若是开发者又不喜欢构造出一个Error对象的话,Error的高亮也没有了。

综上,我以为在Promise里发现显式的错误后,用throw抛出错误会比较好,而不是显式的构造一个被reject的Promise对象。

最佳实践

这里难免再啰嗦两句最佳实践

1.一是不要把Promise写成嵌套结构,至于怎么改进,这里就很少说了

// 错误的写法
promise1.then(function(value) {
  promise1.then(function(value) {
    promise1.then(function(value) {

    })
  })
})

2.二是链式Promise要返回一个Promise,而不仅是构造一个Promise

// 错误的写法
Promise.resolve(1).then(function(){
  Promise.resolve(2)
}).then(function(){
  Promise.resolve(3)
})

Promise相关的convenience method的实现

请到这里查看Promise.race, Promise.all, Promise.resolve, Promise.reject等方法的具体实现,这里就不具体解释了,总的来讲,只要then的实现是没有问题的,其它全部的方法均可以很是方便的依赖then来实现。

结语

最后,若是你以为这篇文章对你有所帮助,欢迎分享给你的朋友或者团队,记得注明出处哦~

原文连接:https://github.com/xieranmaya/blog/issues/3

相关文章
相关标签/搜索