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

发布于 2022-08-05 20:54:20 字数 29499 浏览 5 评论 47

本文写给有一定 Promise 使用经验的人,如果你还没有使用过 Promise,这篇文章可能不适合你,建议先 了解 Promise 的使用

Promise 标准解读

1、只有一个 then 方法,没有 catchraceall 等方法,甚至没有构造函数

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

2、then方法返回一个新的Promise

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

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

3、不同 Promise 的实现需要可以相互调用(interoperable)

4、Promise 的初始状态为 pending,它可以由此状态转换为 fulfilled(本文为了一致把此状态叫做 resolved)或者 rejected,一旦状态确定,就不可以再次转换为其它状态,状态确定的过程称为 settle,更具体的标准见这里

一步一步实现一个 Promise

下面我们就来一步一步实现一个 Promise

构造函数

因为标准并没有指定如何构造一个 Promise 对象,所以我们同样以目前一般 Promise 实现中通用的方法来构造一个 Promise 对象,也是 ES6 原生 Promise 里所使用的方式,即:

// Promise构造函数接收一个executor函数,executor函数执行完同步或异步操作后,调用它的两个参数resolve和reject
var promise = new Promise(function(resolve, reject) {
  /*
    如果操作成功,调用resolve并传入https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/value)reject(reason)的形式调用的这两个函数,而不是以resolve.call(promise, https://github.com/xieranmaya/blog/issues/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对象都有一对属于自己的resolvereject函数,就跟写在构造函数内部没什么区别了,所以我们就直接把这两个函数定义在构造函数里面了。不过话说回来,如果浏览器对bind的所优化,使用后一种形式应该可以提升一下内存使用效率。

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

接下来,我们实现resolvereject这两个函数

function Promise(executor) {
  // ...

  function resolve(https://github.com/xieranmaya/blog/issues/value) {
    if (self.status === 'pending') {
      self.status = 'resolved'
      self.data = https://github.com/xieranmaya/blog/issues/value
      for(var i = 0; i < self.onResolvedCallback.length; i++) {
        self.onResolvedCallback[i](https://github.com/xieranmaya/blog/issues/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之后把状态改为相应的值,并把对应的https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/value) {
        alert(https://github.com/xieranmaya/blog/issues/value)
      })

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

下面我们首先处理简单的情况,值的穿透

Promise值的穿透

通过观察,会发现我们希望下面这段代码

new Promise(resolve=>resolve(8))
  .then()
  .catch()
  .then(function(https://github.com/xieranmaya/blog/issues/value) {
    alert(https://github.com/xieranmaya/blog/issues/value)
  })

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

new Promise(resolve=>resolve(8))
  .then(function(https://github.com/xieranmaya/blog/issues/value){
    return https://github.com/xieranmaya/blog/issues/value
  })
  .catch(function(reason){
    throw reason
  })
  .then(function(https://github.com/xieranmaya/blog/issues/value) {
    alert(https://github.com/xieranmaya/blog/issues/value)
  })

所以如果想要把then的实参留空且让值可以穿透到后面,意味着then的两个参数的默认值分别为function(https://github.com/xieranmaya/blog/issues/value) {return https://github.com/xieranmaya/blog/issues/value}function(reason) {throw reason}
所以我们只需要把then里判断onResolvedonRejected的部分改成如下即可:

onResolved = typeof onResolved === 'function' ? onResolved : function(https://github.com/xieranmaya/blog/issues/value) {return https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/value) {
        resolvePromise(promise2, https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/value) {
    if (https://github.com/xieranmaya/blog/issues/value instanceof Promise) {
      return https://github.com/xieranmaya/blog/issues/value.then(resolve, reject)
    }
    setTimeout(function() { // 异步执行所有的回调函数
      if (self.status === 'pending') {
        self.status = 'resolved'
        self.data = https://github.com/xieranmaya/blog/issues/value
        for (var i = 0; i < self.onResolvedCallback.length; i++) {
          self.onResolvedCallback[i](https://github.com/xieranmaya/blog/issues/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 https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/value) {
        try {
          var x = onResolved(https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/value) {
    // "Big ERROR!!!"
  })
  .catch()
  .then()
  .then()
  .catch()
  .then()

假设这个Big ERROR!!!的出现让我们完全没有必要运行后面所有的代码了,但链式调用的后面即有catch,也有then,无论我们是return还是throw,都不可避免的会进入某一个catchthen里面,那有没有办法让这个链式调用在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(https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/value) {
    // "Big ERROR!!!"
    return Promise.stop()
  })
  .catch()
  .then()
  .then()
  .catch()
  .then()

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

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

考虑如下代码:

new Promise(function(resolve) {
  resolve(42)
})
  .then(function(https://github.com/xieranmaya/blog/issues/value) {
    alter(https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/value) {
    $scope.https://github.com/xieranmaya/blog/issues/value = https://github.com/xieranmaya/blog/issues/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(https://github.com/xieranmaya/blog/issues/value) {
    $scope.https://github.com/xieranmaya/blog/issues/value = https://github.com/xieranmaya/blog/issues/value
  })
})

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

Promise.prototype.$digest = function() {
  $rootScope.$digest()
  return this
}
// 然后这么使用
OtherPromise
  .resolve(42)
  .then(function(https://github.com/xieranmaya/blog/issues/value) {
    $scope.https://github.com/xieranmaya/blog/issues/value = https://github.com/xieranmaya/blog/issues/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/rejectonResolved/onRejected),也会花费一定的时间和内存。

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

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

最佳实践

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

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

// 错误的写法
promise1.then(function(https://github.com/xieranmaya/blog/issues/value) {
  promise1.then(function(https://github.com/xieranmaya/blog/issues/value) {
    promise1.then(function(https://github.com/xieranmaya/blog/issues/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来实现。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(47

吹泡泡o 2022-05-04 13:46:49 47 楼

大佬,有个问题请教下。

self.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面
self.onRejectedCallback = [] // Promise reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面

onResolvedCallback 为什么是一个数组队列呢?我试了好久,也只弄出一个回调函数,什么情况下会出现多个回调。

var p = new Promise(resolve => {
  setTimeout(resolve, 1000, 'hi')
});
p.then(() => {
  console.log(1)
})
p.then(() => {
  console.log(2)
})
月牙弯弯 2022-05-04 13:46:49 46 楼

mark~~!!

七婞 2022-05-04 13:46:49 45 楼

@think2011 有点没理解。

第一段代码考虑的不是 x 中的 resolve 可能是 resolve 一个 Promise 吗?但是构造函数中写了,当 resolve 一个 Promise 的时候,执行下面的代码

if (https://github.com/xieranmaya/blog/issues/value instanceof MyPromise) {
  return https://github.com/xieranmaya/blog/issues/value.then(resolve, reject);
}

测试发现 resolve 函数里面的判断是没必要的,忽略掉就好

一身骄傲 2022-05-04 13:46:49 44 楼

有个问题请教一下,既然then是异步的,那么then里面是不是不需要分status处理then的回调了,全都放到callbacks里,然后在resolve或reject里面一起执行?

当 promise 对象已经处非 pending 的状态时,就没法调用 resolve 或 reject 方法了呀,所以也就不存在一起执行的条件。这时 promise 的 executor 已经处理完成任务并且把运行结果挂在 https://github.com/xieranmaya/blog/issues/value 上了,那么就直接从 https://github.com/xieranmaya/blog/issues/value 里取出来然后直接运行回调

还给你自由 2022-05-04 13:46:49 43 楼

我想知道 是什么实现了promise,是什么规则 让promise 默认走微观队列,代码中使用了setTomeout,本身就进入宏观队列了,但是我看浏览器端没有别的方法,所以求作者大大解答一下,再往深走,如何实现的promise走微观队列????是底层C++的实现吗还是别的原理??

我这里的实现并没有考虑宏任务或微任务,只要仅有宏任务或微任务,所有的测试也都会通过的。
现代浏览器中有一些办法让一个回调函数走micro task,比如MutationObserver

process.nextTick(()=>{})

梦初启 2022-05-04 13:46:49 42 楼

ww我找到原因了。
在判断thenable对象的时候,resolvePromise和rejectPromise应该只调用一次,尤其是在Promise对象和thenable交错嵌套的时候。

if (typeof then === 'function') {
            let hasBeenCalled = false; // BOOOOM!!!!! 这里局部变量导致调用一次的flag没有暴露在判断Promise的代码块中。
                                                      // 把他提到函数顶部即可解决问题。
            try {
                then.call(returnValue, (y)=> {
                    if (hasBeenCalled)return;
                    hasBeenCalled = true;
                    resolveProomise(proomise2, y, resolve, reject);
                }, (r)=> {
                    if (hasBeenCalled)return;
                    hasBeenCalled = true;
                    reject(r);
                });
            } catch (error) {
                if (hasBeenCalled) {
                    // ignore the error
                } else {
                    reject(error);
                }
            }
        } else {
            resolve(returnValue);
        }

你好,我遇到了同样的问题,但是我不是很能理解你说的放到函数顶部。我理解递归调用的时候,这个判断调用一次的flag在每次调用的时候都会被重置为false

东京女 2022-05-04 13:46:49 41 楼

你好,我想请问一下resolve和reject函数内部的实现为什么是需要整个函数体放到setTimeout。我理解状态的改变从pending到fulfilled或者rejected是一个同步的操作。但是实际上我尝试把状态这部分代码放到setTimeout外部之后测试用例就无法通过了。
我尝试了一下浏览器实现版的promise和通过测试用例的_Promise都运行下面这段代码:

const a = new Promise((resolve, reject) => {
  resolve(1);
});

a.then(() => {
  console.log(a, 111);
});

console.log(a);

实际上浏览器的Promise打出来的结果是

Promise {<fulfilled>: 1}
Promise {<fulfilled>: 1} 111

而实现版打出来的结果是

Promise {<pennding>}
Promise {<fulfilled>: 1} 111

我理解是不是因为浏览器的状态改变是同步操作的结果。希望大佬能帮我解答下问题

我的鱼塘能养鲲 2022-05-04 13:46:49 40 楼

似乎不能使用class实现Promise,我照着大佬的代码写了一遍class版本的,并不能通过测试脚本,我找了很久的问题,也没有找到。

class myPromise {
  PENDING = 'pending'
  RESOLVE = 'resolve'
  REJECT = 'reject'
  _data = undefined
  _resolvedCallBacks = []
  _rejectedCallBacks = []
  _status = this.PENDING
  constructor (handler) {
    try {
      handler(this._resolve.bind(this), this._reject.bind(this))
    } catch (e) {
      this._reject(e)
    }
  }
  _resolve (https://github.com/xieranmaya/blog/issues/value) {
    setTimeout(() => {
      if (this._status == this.PENDING) {
        this._status = this.RESOLVE
        this._data = https://github.com/xieranmaya/blog/issues/value
        for (var i = 0; i < this._resolvedCallBacks.length; i++) {
          this._resolvedCallBacks[i](https://github.com/xieranmaya/blog/issues/value)
        }
      }
    })
  }
  _reject (https://github.com/xieranmaya/blog/issues/value) {
    setTimeout(() => {
      if (this.status == this.PENDING) {
        this._status = this.REJECT
        this._data = https://github.com/xieranmaya/blog/issues/value
        for (var i = 0; i < this._rejectedCallBacks.length; i++) {
          this._rejectedCallBacks[i](https://github.com/xieranmaya/blog/issues/value)
        }
      }
    })
  }
  resolvePromise (promise2, x, resolve, reject) {
    let then
    let thenCalledOrThrow = false
    let self = this
    if (promise2 === x) {
      return reject(new TypeError('Chaining cycle detected for promise!'))
    }
  
    if (x instanceof myPromise) {
      if (x._status === 'pending') {
        x.then(function(https://github.com/xieranmaya/blog/issues/value) {
          self.resolvePromise(promise2, https://github.com/xieranmaya/blog/issues/value, resolve, reject)
        }, reject)
      } else {
        x.then(resolve, reject)
      }
      return
    }
  
    if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
      try {
        then = x.then 
        if (typeof then === 'function') {
          then.call(x, function rs(y) {
            if (thenCalledOrThrow) return
            thenCalledOrThrow = true
            return self.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)
    }
  }
  then (onResolve, onReject) {
    let self = this
    let promise2
    onResolve = typeof onResolve === 'function' ? onResolve : function(https://github.com/xieranmaya/blog/issues/value) { return https://github.com/xieranmaya/blog/issues/value }
    onReject = typeof onReject === 'function' ? onReject : function(reason) { return reason }

    if (self._status == this.RESOLVE) {
      return promise2 = new myPromise(function(resolve, reject) {
        setTimeout(function() {
          try {
            let x = onResolve(self._data)
            self.resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      })
    }

    if (self._status == this.REJECT) {
      return promise2 = new myPromise(function(resolve, reject) {
        setTimeout(function() {
          try {
            let x = onReject(self._data)
            self.resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      })
    }
    if (self._status == this.PENDING) {
      return promise2 = new myPromise(function(resolve, reject) {
        self._resolvedCallBacks.push(function(https://github.com/xieranmaya/blog/issues/value) {
          try {
            let x = onResolve(https://github.com/xieranmaya/blog/issues/value)
            self.resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
        self._rejectedCallBacks.push(function(https://github.com/xieranmaya/blog/issues/value) {
          try {
            let x = onReject(https://github.com/xieranmaya/blog/issues/value)
            self.resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      })
    }
  }
}

这个纯粹是语法错误了。class 和构造函数还是有区别的,怎么可以直接在方法外面编写执行语句?运行测试的时候应该会报错 unexcepted token "="

蒲公英的约定 2022-05-04 13:46:49 39 楼

mark

眼趣 2022-05-04 13:46:48 38 楼

mark

梦里泪两行 2022-05-04 13:46:48 37 楼

mark 很不错~慢慢品尝

昔&日梦未散 2022-05-04 13:46:48 36 楼

这里用setTimeout和原生的Promise是有区别的因为setTimeout是macro-task,而Promise是micro-task。micro-task应该优先执行于macro-task , 所以如果你上面用setTimeout 就会导致如果Promise之前有setTimeout分配的task,执行顺序就会出错。应该用process.nextTick来代替setTimeout。但是不过好像在Browser端没有对应的替代方案也只有setTimeout了。文章不错,赞一个

混浊又暗下来 2022-05-04 13:46:48 35 楼

bluebird的实现好像是node端首选process.nextTick其次是setImmediate 。 browser端首选MutationObserver其次是setTimeout

沫离伤花 2022-05-04 13:46:48 34 楼

bluebird的实现好像是node端首选process.nextTick其次是setImmediate 。 browser端首选MutationObserver其次是setTimeout

是的,这个我文中有提到。
另外关于两种task的区别,这种情况下,如果全用setTimeout的话也是不会错乱的。在支持的环境中用更“高效”的函数就可以了

本王不退位尔等都是臣 2022-05-04 13:46:48 33 楼

面试的时候面试官让我手写一个Promise的实现,当时实在不会,现在回来赶紧学习,学到了

情域 2022-05-04 13:46:48 32 楼

@xieranmaya 照着楼主的思路自己实现了一下,有一处代码比较有疑惑,

function resolvePromise(promise2, x, resolve, reject) {
  ...
    if (x.status === 'pending') { //because x could resolved by a Promise Object
      x.then(function(v) {
        resolvePromise(promise2, v, resolve, reject)
      }, reject)
  ...
}

注释中写到 because x could resolved by a Promise Object

可是在 Promise 的构造里面不是写了

if (https://github.com/xieranmaya/blog/issues/value instanceof MyPromise) {
  return https://github.com/xieranmaya/blog/issues/value.then(resolve, reject);
}

这样最后 resolve 调用时候不是一定不是一个 Promise 的实例么?(我理解中第一段代码就是考虑 v可能 Promise 的情况)

望指点~

郁金香雨 2022-05-04 13:46:48 31 楼

@zhuscat
我没有仔细看过源码,不过我想实现思路应该都差不多的..

第一段代码的意思是
如果 x 是一个 Promise,并且状态还处于 pending,那么先 then 拿到最终的 https://github.com/xieranmaya/blog/issues/value,接下来才 resolve https://github.com/xieranmaya/blog/issues/value

第二段代码应该是处理类似

x.then(p2 => {
  return new Promise(...)
})

这样的情况的。

独行侠 2022-05-04 13:46:48 30 楼

@think2011 有点没理解。

第一段代码考虑的不是 x 中的 resolve 可能是 resolve 一个 Promise 吗?但是构造函数中写了,当 resolve 一个 Promise 的时候,执行下面的代码

if (https://github.com/xieranmaya/blog/issues/value instanceof MyPromise) {
  return https://github.com/xieranmaya/blog/issues/value.then(resolve, reject);
}
榕城若虚 2022-05-04 13:46:48 29 楼

mark

萌︼了一个春 2022-05-04 13:46:48 28 楼

写的不错!很详细

热风软妹 2022-05-04 13:46:48 27 楼

非常详细,我自己实现了一小半都写不下去了,佩服作者

乜一 2022-05-04 13:46:48 26 楼

同问,我也没理解这一段@zhuscat

怕倦 2022-05-04 13:46:48 25 楼

LZ有两个问题,麻烦解答一下then方法就是负责执行resovle和reject 通过onResovle和onReject已经执行了, 为啥在在"resolve"状态下后面还执行一次resolve 在reject和pengding状态都没有执行 。 还有
只有resolve才有这个判断,reject没有

完美的未来在梦里。 2022-05-04 13:46:48 24 楼

是不是应该在三种状态下执行try成功后,都应该执行resolve函数呢 以确保后面都链式操作继续执行

@xieranmaya

羁〃客ぐ 2022-05-04 13:46:48 23 楼

前来学习,大家没有疑问吗?
下面代码既然外面返回一个 promise,为什么不直接返回 promise,还要再包一个?是因为规范吗?

  if (self.status === 'resolved') {
    return promise2 = new Promise_(function (resolve, reject) {
      try {
        var x = onResolved(self.data)
        if (x instanceof Promise_) { 
          x.then(resolve, reject)
        }
        resolve(x) 
      } catch (e) {
        reject(e) 
      }
    })
  }

改为

  if (self.status === 'resolved') {
    var x
    try {
      var x = onResolved(self.data)
    } catch (e) {
      reject(e)
    }
    
    if (x instanceof Promise_) {
      return x
    } else {
      return promise2 = new Promise_(function (resolve, reject) {
        try {
          resolve(x)
        } catch (e) {
          reject(e)
        }
      })
    }

若果x里面还有一层或几层promise嵌套,你这样就不行了

纸短情长 2022-05-04 13:46:48 22 楼

我有一个promise关于microtask的问题:
我看到resolve,reject都是用setTimout实现的,而setTimout是macrotask。
那么下面这种情况:
setTimeout(() => {
console.log('setTimeout')
})
const p1 = new MyPromise(r =>{
console.log(1)
r()
}).then(v => {
console.log(2)
})
如果resolve使用setTimeout实现的,那么打印顺序就应该是 1, setTimeout, 2
第一个宏任务script执行完打印了1,并且把遇到的setTimeout与在resolve中执行的console.log(2),都归于下一个宏任务和下下一个宏任务,就会依次打印setTimeout和2.
而promise是归于微任务,第一个宏任务完了会立刻执行被推入队列的微任务
也就是会打印 1 2 setTimout.
所以想问一下,promsie是如何在浏览器中实现这种走微任务队列的呢?

鲜血染红嫁衣 2022-05-04 13:46:48 21 楼

@jiaweiCao @ym754870370 @Arsenal072
统一回复一些问题

首先关于使用setTimeout,这个实现只负责把测试用例全过,并不考虑现实场景,而A+标准中并没有对回调的调用方式进行限制,只要求回调函数在promise自身的调用栈以外调用(请仔细理解这句话),所有的测试用例也只考虑promise自身的调度,不考虑与其它异步函数(如nextTick,setImmediate等)的协同使用。任何语言只要提供了类似setTimeout的功能都是可以实现promise的,显然其它语言不见得有js中宏任务与微任务的区分。所以测试用例不会区分回调是用哪种任务类型调用的。

实际的实现中都是用微任务执行的,但考虑到高版本浏览器自带Promise,这个实现如果运行,也只需要在低版本中,那么是没有微任务函数的。另外这样写也是为了一些新手容易看懂。

另外,A+标准与ES6 Promise的标准是有不同的,ES6 Promise的标准严格很多。

然后中间的那段条件判断,并不是什么刻意写的,是可以优化为更好的逻辑的,只是当时那么写确实通过了所有的test case,然后写文章的时候就直接贴过来了,我又不想修改文章的最后更新时间,就一直留着了,理解好标准完全可以改成你们喜欢的样子,只要逻辑是符合标准定义的就行。

@JunlinZhu-Tommy
then肯定是不能返回this的:

var p = Promise.resolve()
var a = p.then(() => 1)
var b = p.then(() => {throw 2})

很显然a和b一个成功一个失败,如果返回this,a和b岂不是都为p,都成功?

但是在一些情况下是可以返回this的,在A+标准的3.3有明确描述,只要你的实现满足的所有的规则,比如说以下的情况就可以直接返回this:

var p = Promise.resolve()
var a = p.then()

显然,a和p的最终resolve的值是一样的。
也即是在调用p的then时p的状态已经确定为某种状态,但又没给then传相应的参数时。

带上头具痛哭 2022-05-04 13:46:48 20 楼

前来学习,大家没有疑问吗?
下面代码既然外面返回一个 promise,为什么不直接返回 promise,还要再包一个?是因为规范吗?

  if (self.status === 'resolved') {
    return promise2 = new Promise_(function (resolve, reject) {
      try {
        var x = onResolved(self.data)
        if (x instanceof Promise_) { 
          x.then(resolve, reject)
        }
        resolve(x) 
      } catch (e) {
        reject(e) 
      }
    })
  }

改为

  if (self.status === 'resolved') {
    var x
    try {
      var x = onResolved(self.data)
    } catch (e) {
      reject(e)
    }
    
    if (x instanceof Promise_) {
      return x
    } else {
      return promise2 = new Promise_(function (resolve, reject) {
        try {
          resolve(x)
        } catch (e) {
          reject(e)
        }
      })
    }

若果x里面还有一层或几层promise嵌套,你这样就不行了

不能这样写,按你的写法,onResolved会在调用then的时候同步调用,但回调必须异步调用。
这么久了我也不确定细节,你改了可以跑一下测试,能通过就可以,不能通过就拿用例跑一下就明白了。

月下凄凉 2022-05-04 13:46:48 19 楼

看其他人的实现有点不太懂,麻烦大佬答疑一下
1、new Promise(resolve, reject),这里调用resolve会执行成功回调队列的方法,第一次创建
promise对象,成功回调队列还没有值吧
2、调用promise.then()时,状态是PENDING是怎么来的?当监听到状态发生变化时,先执行resolve(https://github.com/xieranmaya/blog/issues/value),但此时成功回调队列中还没有值

1应该是resolve操作是一个异步操作,会以一个新的任务来运行,而通过then注册回调函数的操作是同步的。所以只要then了,在resolve之后回调的队列一定会有值。
2的话promise的默认状态就是pending吧,resolve就是把pending转为resolved,reject就是把pending转为rejected。至于此时你说的‘但此时成功回调队列中还没有值’,如果1能理解的话2应该没问题。

也不知道我说的对不对,如果有错误希望大佬能指正,感激不敬

@Arsenal072 这个回答说的很对。
resolve一般是被executor异步调用的,调用时,对象上已经有了回调函数了。

即使是被同步调用也没关系,同步调用时状态已经改变,再调用then时有了状态就会立刻运行传入的回调,而不push到数组里。

池木 2022-05-04 13:46:48 18 楼


@ ENder1217 @xieranmaya
你们好,这个红框这里,我也与疑问,理解不了。
不应该是这样的吗?

if (x instanceof Promise) {
      x.then(resolve, reject);
} else {
     resolve(x);
 }

没有elseresolve不是会执行两次吗?
一次当次执行,
一次x resolve的时候执行。
不是吗?是我理解的有问题吗?求解答。

Promise中状态只能由Pending变为已实现或由Pending变为已拒绝,并且状态改变之后不会在发生变化,会一直保持这个状态。
就是说一次x resolve后就不会在执行resolve和reject了,而在大喵之前实现的resolve函数
function resolve(https://github.com/xieranmaya/blog/issues/value){
if(https://github.com/xieranmaya/blog/issues/value instanceof Promise){
返回https://github.com/xieranmaya/blog/issues/value.then(resolve,reject)
}
setTimeout(function(){//正在进行执行所有的函数
if(self.status ==='pending'){
self.status ='resolved'self.data

(var i = 0; i <self.onResolvedCallback.length; i ++)的值{
self.onResolvedCallback i
}
}
})
}
中只有在self.status =='pending'时才会执行,依次当x已经执行过resolve后就不会在执行resolve了

作者说得对,请再想一想。

I think repeatedly, but I still don't know why this part doesn't need else.
I hope I can understand.

我真的看了很久 不太了解為何此處不需要else
這部分要處理的目的就是鏈狀then內如果有promise的狀況 但我實在無法了解
我有看過此篇 https://segmentfault.com/a/1190000008656872 是有加else 不知是否我有誤解?
x.then(resolve, reject) 中的resolve會是then中的onResolved
也就是reslove確實會在then中被執行 不知道是否能提供極端範例

我也觉得这边要么加else,要么加return;同理self.status === 'rejected'时也应该加上,我觉得时作者漏了。这边逻辑其实和 function resolve()中是一样的

国粹 2022-05-04 13:46:48 17 楼

前来学习,大家没有疑问吗?
下面代码既然外面返回一个 promise,为什么不直接返回 promise,还要再包一个?是因为规范吗?

  if (self.status === 'resolved') {
    return promise2 = new Promise_(function (resolve, reject) {
      try {
        var x = onResolved(self.data)
        if (x instanceof Promise_) { 
          x.then(resolve, reject)
        }
        resolve(x) 
      } catch (e) {
        reject(e) 
      }
    })
  }

改为

  if (self.status === 'resolved') {
    var x
    try {
      var x = onResolved(self.data)
    } catch (e) {
      reject(e)
    }
    
    if (x instanceof Promise_) {
      return x
    } else {
      return promise2 = new Promise_(function (resolve, reject) {
        try {
          resolve(x)
        } catch (e) {
          reject(e)
        }
      })
    }

我也有这个疑问,我猜测是,then 里返回的是一个 新 promise 对象 p2, 那么肯定需要 p2 调用了 resovle 函数,后面链式的 then 里的函数才能执行。 如果直接 resolve(x), 那么相当于把 x 这个promsie 对象传递给了 then 参数,这肯定不符合预期。 个人理解写成var x = onResolved(self.data); if ( x instanceof Promise) { x.then(data => resolve(data)); } else { resolve(x); }
可能更好理解一些。

少跟Wǒ拽 2022-05-04 13:46:48 16 楼

似乎不能使用class实现Promise,我照着大佬的代码写了一遍class版本的,并不能通过测试脚本,我找了很久的问题,也没有找到。

class myPromise {
  PENDING = 'pending'
  RESOLVE = 'resolve'
  REJECT = 'reject'
  _data = undefined
  _resolvedCallBacks = []
  _rejectedCallBacks = []
  _status = this.PENDING
  constructor (handler) {
    try {
      handler(this._resolve.bind(this), this._reject.bind(this))
    } catch (e) {
      this._reject(e)
    }
  }
  _resolve (https://github.com/xieranmaya/blog/issues/value) {
    setTimeout(() => {
      if (this._status == this.PENDING) {
        this._status = this.RESOLVE
        this._data = https://github.com/xieranmaya/blog/issues/value
        for (var i = 0; i < this._resolvedCallBacks.length; i++) {
          this._resolvedCallBacks[i](https://github.com/xieranmaya/blog/issues/value)
        }
      }
    })
  }
  _reject (https://github.com/xieranmaya/blog/issues/value) {
    setTimeout(() => {
      if (this.status == this.PENDING) {
        this._status = this.REJECT
        this._data = https://github.com/xieranmaya/blog/issues/value
        for (var i = 0; i < this._rejectedCallBacks.length; i++) {
          this._rejectedCallBacks[i](https://github.com/xieranmaya/blog/issues/value)
        }
      }
    })
  }
  resolvePromise (promise2, x, resolve, reject) {
    let then
    let thenCalledOrThrow = false
    let self = this
    if (promise2 === x) {
      return reject(new TypeError('Chaining cycle detected for promise!'))
    }
  
    if (x instanceof myPromise) {
      if (x._status === 'pending') {
        x.then(function(https://github.com/xieranmaya/blog/issues/value) {
          self.resolvePromise(promise2, https://github.com/xieranmaya/blog/issues/value, resolve, reject)
        }, reject)
      } else {
        x.then(resolve, reject)
      }
      return
    }
  
    if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
      try {
        then = x.then 
        if (typeof then === 'function') {
          then.call(x, function rs(y) {
            if (thenCalledOrThrow) return
            thenCalledOrThrow = true
            return self.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)
    }
  }
  then (onResolve, onReject) {
    let self = this
    let promise2
    onResolve = typeof onResolve === 'function' ? onResolve : function(https://github.com/xieranmaya/blog/issues/value) { return https://github.com/xieranmaya/blog/issues/value }
    onReject = typeof onReject === 'function' ? onReject : function(reason) { return reason }

    if (self._status == this.RESOLVE) {
      return promise2 = new myPromise(function(resolve, reject) {
        setTimeout(function() {
          try {
            let x = onResolve(self._data)
            self.resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      })
    }

    if (self._status == this.REJECT) {
      return promise2 = new myPromise(function(resolve, reject) {
        setTimeout(function() {
          try {
            let x = onReject(self._data)
            self.resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      })
    }
    if (self._status == this.PENDING) {
      return promise2 = new myPromise(function(resolve, reject) {
        self._resolvedCallBacks.push(function(https://github.com/xieranmaya/blog/issues/value) {
          try {
            let x = onResolve(https://github.com/xieranmaya/blog/issues/value)
            self.resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
        self._rejectedCallBacks.push(function(https://github.com/xieranmaya/blog/issues/value) {
          try {
            let x = onReject(https://github.com/xieranmaya/blog/issues/value)
            self.resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      })
    }
  }
}
地狱即天堂 2022-05-04 13:46:48 15 楼


@ ENder1217 @xieranmaya
你们好,这个红框这里,我也与疑问,理解不了。
不应该是这样的吗?

if (x instanceof Promise) {
      x.then(resolve, reject);
} else {
     resolve(x);
 }

没有elseresolve不是会执行两次吗?
一次当次执行,
一次x resolve的时候执行。
不是吗?是我理解的有问题吗?求解答。

Promise中状态只能由Pending变为已实现或由Pending变为已拒绝,并且状态改变之后不会在发生变化,会一直保持这个状态。

就是说一次x resolve后就不会在执行resolve和reject了,而在大喵之前实现的resolve函数
function resolve(https://github.com/xieranmaya/blog/issues/value){
if(https://github.com/xieranmaya/blog/issues/value instanceof Promise){
返回https://github.com/xieranmaya/blog/issues/value.then(resolve,reject)
}
setTimeout(function(){//正在进行执行所有的函数
if(self.status ==='pending'){
self.status ='resolved'self.data
(var i = 0; i <self.onResolvedCallback.length; i ++)的值{
self.onResolvedCallback i
}
}
})
}
中只有在self.status =='pending'时才会执行,依次当x已经执行过resolve后就不会在执行resolve了

作者说得对,请再想一想。

I think repeatedly, but I still don't know why this part doesn't need else.
I hope I can understand.
我真的看了很久 不太了解為何此處不需要else
這部分要處理的目的就是鏈狀then內如果有promise的狀況 但我實在無法了解
我有看過此篇 https://segmentfault.com/a/1190000008656872 是有加else 不知是否我有誤解?
x.then(resolve, reject) 中的resolve會是then中的onResolved
也就是reslove確實會在then中被執行 不知道是否能提供極端範例

我也觉得这边要么加else,要么加return;同理self.status === 'rejected'时也应该加上,我觉得时作者漏了。这边逻辑其实和 function resolve()中是一样的

我的理解是,红框部分应该是有三个promise的,第一个是调用then时里面新创建的promise,也就是promise2,第二个是用户自己创建的promise,也就是x,最后一个是判断x为promise后,调用x的then方法里面又新建的promise,称为thenPromise,也就是x.then(resolve, reject)时内部创建的promise,如此一来,当then内部判断x为promise时,就执行x.then(resolve, reject),此时promise2的resolve作为参数传入then中被当作onResolved方法执行,这样便把用户自己创建的promise传入的值传递给了promise2,以便后面的then继续使用,但是因为resolve本身没有返回值,所以此时的thenPromise中的x的值为undefined,不过此时并不影响后续的操作了,因为promise2已经拿到了用户新建promise时要传递的值了

梦萦几度 2022-05-04 13:46:48 14 楼

当p resolve(1)时,p.then()返回的新的promise2,默认有onFulfilled函数,执行,也就是图中的x = onFulfilled(https://github.com/xieranmaya/blog/issues/value),要用promise2的resolve来决议promise2吧,这样p.then()返回的promise才是决议掉的(resolved)。然后p.then().then(),第二个then返回的promise会用前一个then()返回的promise的终值作为自己的终值,不然直接被第一个then()返回的promise卡着了,如果不决议它的话。

这里的写法我用的else { resolve(x) }。而作者在function resolve的实现中是if(){...}; resolve(x),我觉得有点问题,分析如下

予囚 2022-05-04 13:46:48 13 楼

大佬,有个问题请教下。

self.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面
self.onRejectedCallback = [] // Promise reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面

onResolvedCallback 为什么是一个数组队列呢?我试了好久,也只弄出一个回调函数,什么情况下会出现多个回调。

国际总奸 2022-05-04 13:46:48 12 楼
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 https://github.com/xieranmaya/blog/issues/value;
      x.then(resolve, reject)
    }
    return
  }

resolvePromise方法里这一段能不能直接去掉呢,因为promise也是thenable,我测了也能通过

穿透光 2022-05-04 13:46:47 11 楼

@HeskeyBaozi 这个问题在我最初实现的时候也是纠结了我好久,最终跟你差不多,发现还是自己没有考虑全面~~我的代码里面也有几乎相同的逻辑,你在当前页面搜“thenCalledOrThrow”就能看到了~

活雷疯! 2022-05-04 13:46:45 10 楼

我根据大大的教程实现了一遍。但是在测试框架的时候一直提示我resolvePromise这个参数函数的y参数处理有问题= =。
测试报告:

2.3.3.3.1: If/when resolvePromise is called with https://github.com/xieranmaya/blog/issues/value y, run [[Resolve]](promise, y)

y is a thenable for a thenable

y is an already-fulfilled promise for a synchronously-fulfilled custom thenable

then calls resolvePromise synchronously

via return from a fulfilled promise

Error: timeout of 200ms exceeded. Ensure the done() callback is being called in this test.

via return from a rejected promise

Error: timeout of 200ms exceeded. Ensure the done() callback is being called in this test.

then calls resolvePromise asynchronously

via return from a fulfilled promise

Error: timeout of 200ms exceeded. Ensure the done() callback is being called in this test.

via return from a rejected promise

Error: timeout of 200ms exceeded. Ensure the done() callback is being called in this test.

下面是resolveProomise实现:

function resolveProomise(proomise2, returnValue, resolve, reject) {

    if (proomise2 === returnValue)
        reject(new TypeError());

    // 如果 returnValue 为 Promise ,则使 promise 接受 returnValue 的状态
    if (returnValue instanceof Proomise) {
        if (returnValue.ProomiseStatus === Status.PENDING) {
            returnValue.then((https://github.com/xieranmaya/blog/issues/value)=> {
                resolveProomise(proomise2, https://github.com/xieranmaya/blog/issues/value, resolve, reject);
            }, reject);
        } else
            returnValue.then(resolve, reject);
    }

    if ((typeof returnValue === 'object' && returnValue !== null) || typeof returnValue === 'function') {
        let then;
        try {
            then = returnValue.then;
        } catch (error) {
            reject(error);
        }

        if (typeof then === 'function') {
            let hasBeenCalled = false;
            try {
                then.call(returnValue, (y)=> {
                    if (hasBeenCalled)return;
                    hasBeenCalled = true;
                    resolveProomise(proomise2, y, resolve, reject);
                }, (r)=> {
                    if (hasBeenCalled)return;
                    hasBeenCalled = true;
                    reject(r);
                });
            } catch (error) {
                if (hasBeenCalled) {
                    // ignore the error
                } else {
                    reject(error);
                }
            }
        } else {
            resolve(returnValue);
        }
    } else {
        resolve(returnValue);
    }
}
知足的幸福 2022-05-04 13:46:45 9 楼

ww我找到原因了。
在判断thenable对象的时候,resolvePromise和rejectPromise应该只调用一次,尤其是在Promise对象和thenable交错嵌套的时候。

if (typeof then === 'function') {
            let hasBeenCalled = false; // BOOOOM!!!!! 这里局部变量导致调用一次的flag没有暴露在判断Promise的代码块中。
                                                      // 把他提到函数顶部即可解决问题。
            try {
                then.call(returnValue, (y)=> {
                    if (hasBeenCalled)return;
                    hasBeenCalled = true;
                    resolveProomise(proomise2, y, resolve, reject);
                }, (r)=> {
                    if (hasBeenCalled)return;
                    hasBeenCalled = true;
                    reject(r);
                });
            } catch (error) {
                if (hasBeenCalled) {
                    // ignore the error
                } else {
                    reject(error);
                }
            }
        } else {
            resolve(returnValue);
        }
源来凯始玺欢你 2022-05-04 13:46:38 8 楼

@nobodiness 问题提的很好,不过注意看源代码, try块是包在setTimeout里面的

陈年往事 2022-05-04 13:46:20 7 楼
Promise.prototype.then = function(onResolved, onRejected) {
...
    if (self.state === 'resolved') {
        return new Promise(function(resolve, reject) {
            try {
                var x = onResolved(self.data);
                // 如果onResolved的返回值是一个Promise对象,直接取它的结果做为promise2的结果
                if (x instanceof Promise) x.then(resolve, reject);
                resolve(x);
...

这里var x = onResolved(self.data)直接把值算出来了, 异步不就变成同步了么, 计算onResolved一定会卡住的

蹲在坟头点根烟丶 2022-05-04 13:42:53 6 楼

THX

放赐﹉ 2022-05-04 13:41:56 5 楼

受教了,谢谢分享

傲娇萝莉攻 2022-05-04 13:32:26 4 楼

@xcatliu typo,已改~

本王不退位尔等都是臣 2022-05-04 12:32:51 3 楼

也就是说,在Promise链的最后一个then里出现的错误,非常难以发现,有文章指出,可以在所有的Promise链的最后都加上一个catch,这样出错后就能被捕获到,这种方法确实是可行的,但是首先在每个地方都加上几乎相同的代码,违背了DIY原则,其次也相当的繁琐。

应该是「违背了DRY原则」?

远昼 2022-05-04 04:31:17 2 楼

Great.

╰沐子 2022-05-03 14:38:53 1 楼

来不及全部看完,很好奇! 回头接着看。

~没有更多了~

关于作者

帝王念

暂无简介

0 文章
0 评论
0 人气
更多

推荐作者

遥远的她

文章 0 评论 0

情深如许

文章 0 评论 0

18120987591

文章 0 评论 0

女皇必胜

文章 0 评论 0

13002228876

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击“接受”或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。