让人头秃的promise-then执行顺序问题

1,042 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情

前言

Promise对于前端开发来说应该不陌生了,其主要用于在一个异步操作中返回结果值,并且支持链式调用。今天就来讨论一个Promise链式调用相关的面试题。

说出其打印结果并解释过程

    Promise.resolve().then(() => {
      console.log(0);
      return Promise.resolve(4);
    }).then((res) => {
      console.log(res);
    });

    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
4
5
6

注意,本文涉及异步、微任务和事件循环等知识,如果还不了解这一块的朋友可移步:搞不清楚事件循环,那就看看这篇文章。上述案例主要是涉及了两个知识点:

  • 交替执行then
  • Promise状态变化对应的事件循环

交替执行

Promise可通过new或者调用类方法resolve()reject()等创建一个实例对象,每一个实例对象都会有thencatchfinally等实例方法。这些方法在调用时会返回一个新生成的promise对象,这就是链式调用的基础。举个栗子:

Promise.resolve().then(() => {
      throw new Error('error');
    }).catch((e) => console.log(e)).finally(() => console.log('finally')).then(() => console.log('hello'))

运行结果:

image-20220614203158430.png

上述例子结合使用了then、catch和finally,说明Promise是可以链式调用的。

如果有多个fulfilled(已兑现) 的promise实例,同时执行then链式调用,then会交替执行。这个是编译器做的优化,主要是为了避免某一个promise占用的时间太长。

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

运行结果:

image-20220614204017213.png

运行结果很直接的反应了then是会交替执行的。注意是多个已兑现的promise实例,如果是单个promise实例,则会按照顺序执行,即使有多个then:

    const p1 = Promise.resolve(1);
    p1.then(res => console.log(res)).then(() => console.log(0))
    p1.then(res => console.log(res * 10))
    p1.then(res => console.log(res * 100))
    p1.then(res => console.log(res * 1000))

运行结果:

image-20220614204531569.png

这里需要注意:最后打印出来的是0,这是因为事件循环会先执行第一轮的微任务事件,然后才会执行第二轮。

微任务队列和事件循环

在promise实例的then方法中返回一个promise实例,听起来有点绕,看代码就清晰了:

Promise.resolve().then(() => {
    return Promise.resolve(100);
}).then((res) => console.log(res)) // 100

每一个promise实例必然存在于pending、fulfilled、rejected三种状态的某一个,上述代码可以解释为两步:

  1. promise实例有初始的pending状态变为fulfilled状态
  2. then挂载到微任务队列,在下一轮事件循环中执行

等价于:

Promise.resolve().then(() => {
    // 第一步,状态改变
    const p = Promise.resolve(100);
    // 第二步,添加到队列中
    Promise.resolve().then(() => {
        p.then(res => console.log(res)); // 100
    })
})

看完这个,我们再来看一个复杂点的例子:

    Promise.resolve().then(() => {
      console.log(1);
      return Promise.resolve(2);
    }).then((res) => {
      console.log(res);
    }).then(() => {
      console.log(3);
    });
    
    Promise.resolve().then(() => {
      console.log(10);
    }).then(() => {
      console.log(20);
    }).then(() => {
      console.log(30);  
    }).then(() => {
      console.log(40);
    });

执行结果:

image-20220614211201700.png

我们结合事件循环来分析:

  • 第一轮事件循环中,先执行打印1,然后实例化promise是一个异步过程,添加到新的事件循环中;打印10
  • 第二轮事件循环中,实例化promise并由pending状态转为fulfilled状态;打印20
  • 第三轮事件循环中,将promise实例的then添加到微任务队列中,并且在下一次事件循环中执行;打印30
  • 第四轮事件循环中,执行promise实例的then方法;打印40
  • 第五轮事件循环中,打印3

看懂了这个例子,再回头看面试题,是不是就很好理解了。

总结

虽然是一道简单的面试题,但涉及了异步、事件循环、微任务等知识点。本次面试题还仅仅是对微任务的考量,如果在Promise的过程中嵌套宏任务,或者宏任务中嵌套微任务,那么复杂程度提升的就不是一点点了。在实际开发中,这样混乱的场景应该要避免,否则出现了问题都不知道怎么去排查。

最后,还是总结一下关键的知识点:

  • 多个promise实例的then方法是交替执行的
  • 在then方法中返回一个promise实例,可能会有两个异步过程:pending状态变为fulfilled、将实例的then方法添加到微任务队列中

原创不易,转载请注明出处