关于一道经典Promise面试题的思考

3,404 阅读4分钟

最近碰到一道这样的题,颠覆了我对Promise的认知.

题如下:

Promise.resolve().then(() => {
  console.log(0);
  return Promise.resolve(4);
}).then((e) => {
  console.log(e);
})
Promise.resolve().then(() => {
  console.log(1);
}).then(() => {
  console.log(2);
}).then(() => {
  console.log(3);
}).then(() => {
  console.log(5);
}).then(() => {
  console.log(6);
})

按照我之前的理解, 输出的值应该是 0,1,2,3,5,6,4
然而实际输出的值却是: 0,1,2,3,4,5,6

先给出我的分析结果,方便喜欢思考的朋友看到此为止,自己思考~.
[[0,[[[4]]]], [1, [2, [3, [5, [6]]]]]]
上边的题可以理解成是这样一个数组,实际执行可以理解为是对这个数组做广度优先遍历.依次加入到微任务队列。再顺序执行。



以下是我的分析过程:

首先Promise的静态resolve方法以及实例中的then方法都是返回一个新的 Promise实例,
也就是 return new Promise();

同时,promise实例内部的resolve方法如果传入的参数是个promise实例,它会直接调用这个实例的then方法也就是说也会产生一层新的promise实例, 然后then方法中的成功回调执行时如果入参是个promise实例, 也会直接调用这个实例的then方法, 也会产生一层新的promise实例

然后then方法会将这个新的promise实例内部的resolve方法等存入到当前实例的一个状态列表中
假设一个Promise实例a调用了多次then方法实际会在a内部维护一个列表,这个列表里边包含了多个Promise实例的相关信息.
当这个列表被循环的时候, 内部的所有任务才开始加入到微任务队列中去.
但是, 上边说的是当前实例多次调用then方法,然而题中实际上是多次链式调用.
实际上也就是这里有问题, 对于了解链式调用的朋友,应该都知道其内部正常是通过return this来实现链式调用.
而Promise的then方法是通过return 一个新的Promise实例 达到的支持链式调用.
所以链式调用就会导致这些任务并没有被加在同一层列表中。

那么, 为了方便理解, 我们简化一下上边的问题,如下:

Promise.resolve().then(() => {
  console.log(1);
}).then(() => {
  console.log(3);
})
Promise.resolve().then(() => {
  console.log(2);
}).then(() => {
  console.log(4);
})

这个最终输出结果是 1,2,3,4
因为Promise.resolve返回的是个新的promise实例, 所以上边其实是在一个宏任务中,也就是主代码块下产生了2个promise实例 可以这么表示[外层promise实例1, 外层promise实例2]
然后外层promise实例1由于链式调用了2次then方法,所以产生了一个这样的列表: [promise实例1, [promise实例3]].
同理, 外层promise实例2内部产生的是这样一个列表[promise实例2, [promise实例4]].
为什么这里是这样嵌套的,而不是平铺的,因为then方法中返回的是一个新的promise实例. 也就是说它内部是 return new Promise(), 而不是在里边return this
所以then方法的链式调用并不是产生一个平铺的数组列表, 而是产生一个层层嵌套的列表.

实际执行的时候我们可以理解成这样:

(function(){
    setTimeout(() => {
        setTimeout(() => {
            console.log(1)
            setTimeout(() => {
                console.log(3)
            }, 0)
        }, 0)
    }, 0)
    setTimeout(() => {
        setTimeout(() => {
            console.log(2)
            setTimeout(() => {
                console.log(4)
            }, 0)
        }, 0)
    }, 0)
})()

再说说原题,其实就是Promise.resolve(4)本身产生了一个新的promise实例,所以多了一层promise实例, 然后它作为值传给了then方法的成功回调, 由于值是promise实例,所以会调用值的then方法, 所以又产生了一层新的promise实例,这也就导致4在3后.
如果这里将 return Promise.resolve(4) 换成普通的 return 4那么结果就类似于这一题 交错打印.

以上的分析过程实际上是通过先实现一个简版的Promise推导过来的.具体实现如下: Promise的代码实现是求职期间准备面试题准备的,看到这道题的时候就想着用它来试试,最终补充上静态resolve方法后,发现答案和实际一致,于是有了以上分析过程. 最后, 仓促行文, 如有纰漏, 欢迎指正.

// 如果还是不能理解,可通过此简易版Promise实现结合题目,在合适处打断点自行探索
function isAObjAndHaveThen(value){
  return value && typeof value === 'object' && typeof value.then === 'function'
}
function runAsMicroTask(task){
  //  由于当前题目并未和其他异步的宏任务掺和,这里姑且用setTimeout也不会对题意有何冲突
  setTimeout(task, 0)
}
class Promise {
  cbs = [];
  status = "pending";
  value = null;
  constructor(fn) {
    fn(this._resolve.bind(this), this._reject.bind(this));
  }
  _handle(cb){
    if(this.status === 'pending'){
      this.cbs.push(cb);
      return
    }
    const fn = cb[this.status === 'fulfilled' ? 'resolve' : 'reject']
    const fnCb = cb[this.status === 'fulfilled' ? 'onSuccess' : 'onError']
    console.log('cbs', JSON.stringify(this.cbs))
    runAsMicroTask(() => {
      fn(fnCb ? fnCb(this.value) : this.value)
    })
  }
  _resolve(value) {
    if(isAObjAndHaveThen(value)){
      runAsMicroTask(() => {
        value.then(this._resolve.bind(this), this._reject.bind(this))
      })
      return
    }
    this.status = 'fulfilled'
    this.value = value
    this.cbs.forEach(cb => this._handle(cb))
  }
  _reject(err) {
    this.status = 'rejected'
    this.value = err
    this.cbs.forEach(cb => this._handle(cb))
  }
  then(onSuccess, onError) {
    return new Promise((resolve, reject) => {
      this._handle({ onSuccess, onError, resolve, reject });
    });
  }
  static resolve(arg){
    if(arg instanceof Promise){
      return arg;
    }
    return new Promise(res => {
      res(arg)
    });
  }
}