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

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

前言

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方法添加到微任务队列中
相关文章
|
6月前
|
前端开发 JavaScript API
链式编程艺术:探索 Promise 链的美妙之处
链式编程艺术:探索 Promise 链的美妙之处
|
前端开发
promise的一些巨坑
promise的一些巨坑
|
6月前
|
前端开发 JavaScript
js开发:请解释Promise是什么,以及它如何解决回调地狱(callback hell)问题。
Promise是JavaScript解决异步操作回调地狱的工具,代表未来可能完成的值。传统的回调函数嵌套导致代码难以维护,而Promise通过链式调用`.then()`和`.catch()`使异步流程清晰扁平。每个异步操作封装为Promise,成功时`.then()`传递结果,出错时`.catch()`捕获异常。ES6的`async/await`进一步简化Promise的使用,使异步代码更接近同步风格。
91 1
|
6月前
|
前端开发 JavaScript API
一盏茶的功夫帮你彻底搞懂JavaScript异步编程从回调地狱到async/await
在深入讨论 async/await 之前,我们需要了解一下 JavaScript 的单线程和非阻塞的特性。JavaScript 是单线程的,也就是说在任何给定的时间点,只能执行一个操作。然而,对于需要大量时间的操作(例如从服务器获取数据),如果没有适当的管理机制,这种单线程特性可能会导致应用程序的阻塞。为了解决这个问题,JavaScript 引入了回调函数和后来的 Promise,用来管理这些异步操作。
|
6月前
|
前端开发
【特别甜的语法糖】async 与 await
【特别甜的语法糖】async 与 await
50 0
|
6月前
|
前端开发
【源码共读】如何优雅的处理 Promise 的错误
【源码共读】如何优雅的处理 Promise 的错误
102 0
|
Web App开发 前端开发 JavaScript
关于 Await、Promise 执行顺序差异问题
关于 Await、Promise 执行顺序差异问题
301 0
关于 Await、Promise 执行顺序差异问题
|
前端开发
浅尝一颗语法糖 async / await
这是 ES7 提供的语法糖,是真的很友好!
71 0
|
前端开发
Promise是什么?怎么用?
Promise 是一种异步编程的解决方案。它可以使异步操作更加清晰、简单、优雅,避免了回调地狱的问题。Promise 对象表示一个异步操作的最终完成或失败,并且它的最终状态(完成或失败)和返回值(或错误)不依赖于调用它的代码。
|
存储 前端开发 API
我打破了 React Hook 必须按顺序、不能在条件语句中调用的枷锁!
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。(如果你对此感到好奇,我们在下面会有更深入的解释。)