让人头秃的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方法添加到微任务队列中
相关文章
|
JavaScript 前端开发
vue前端下载,实现点击按钮弹出本地窗口,选择自定义保存路径
这个不用代码实现(网上也找不到方法可以调出另存为窗口),更改浏览器设置就可以,否则,现在的浏览器都是默认直接保存到下载路径中
1864 3
|
自然语言处理 JavaScript 前端开发
使用Pagefind为VitePress文档添加离线全文搜索能力
前言 VitePress 相信大家都或多或少听说过或者用过了 默认 UI相比 VuePress2.x 好看,启动速度也快(由Vite驱动,当然VuePress也可以切换构建引擎至Vite) 做内容定制也相对简单,笔者的很多静态文档站点(使用VuePress1.x),文章内容多的时候启动非常的慢,于是就从之前的 VuePress 迁移到了 VitePress,并做了一个博客主题 @sugarat/theme => 之前也有过介绍一个简约风的VitePress博客主题 但是 VitePress 官方目前还没有内置开箱即用的搜索能力(相关PR还在施工中)
1365 0
|
API UED
如何实现Vue2项目升级Vue3?
如何实现Vue2项目升级Vue3?
1131 157
|
JavaScript 前端开发 Docker
拿下奇怪的前端报错(二):nvm不可用报错`GLIBC_2.27‘‘GLIBCXX_3.4.20‘not Found?+ 使用docker构建多个前端项目实践
本文介绍了在多版本Node.js环境中使用nvm进行版本管理和遇到的问题,以及通过Docker化构建流程来解决兼容性问题的方法。文中详细描述了构建Docker镜像、启动临时容器复制构建产物的具体步骤,有效解决了不同项目对Node.js版本的不同需求。
1081 1
Threejs制作骨骼模型
这篇文章详细介绍了在Three.js中创建骨骼动画的过程,包括骨骼节点的创建、权重设置以及控制骨骼关节实现动态效果的步骤,并通过一个具体的圆柱体模型演示了如何添加和控制骨骼动画。
691 2
|
API
【threejs教程】让你的场景五颜绿色:纹理贴图与环境贴图
【8月更文挑战第6天】【threejs教程】让你的场景五颜绿色:纹理贴图与环境贴图
1168 11
|
数据可视化 前端开发 JavaScript
前端框架与库-D3.js数据可视化基础
【7月更文挑战第21天】D3.js是Web开发中创建动态、交互图表的利器,适用于从基础条形图到复杂地理热力图的广泛需求。核心概念涉及数据绑定至DOM,支持动态更新。初学者常遇难题包括不当数据绑定、选择器误用、过渡动画过量及坐标轴配置失误。避免策略需善用`.data()`, `.enter().append()`, `.exit().remove()`管理数据,熟知选择器差异,适度应用`.transition()`, 并精准设定坐标轴。示例条形图代码展示了数据绑定至`<rect>`元素的过程,奠定基础,助你进阶复杂项目。
713 4
|
人工智能 JavaScript 程序员
vue中内置指令v-model的作用和常见使用方法介绍以及在自定义组件上支持
vue中内置指令v-model的作用和常见使用方法介绍以及在自定义组件上支持
2157 0