Promise 详解(下)之 原理篇 - EventLoop

使用篇在这里

实际上在这个使用篇中就已经穿插了一些原理,如 Promise 有3种状态且状态只能被改变一次等。在使用篇我们说过 Promise 的相关回调是属于微任务(Promise本身是同步执行的)。下面就详细说下其中的原理。本文其实已经不再单单是 Promise 的原理的,涉及到更广的任务与事件循环。

JavaScript 是一门单线程执行的编程语言。简单来说就是同一时间只能做一件事情。这可能会导致一个问题:当前一个任务非常耗时,则后续任务一直等待,程序会被阻塞,影响用户体验。

注:关于进程、线程、同步、异步、任务等概念默认已了解。

宏任务和微任务

  • 宏任务和微任务是于JS相关的
  • JS代码从执行逻辑上分成两类:同步代码、异步代码
  • 异步代码又分成:微任务、宏任务
  • ES6 规范中,宏任务(Macrotask) 称为 Task, 微任务(Microtask) 称为 Jobs
宏任务 Script代码、setTimeout、setInterval、postMessage、MessageChannel、DOM事件、AJAX回调、setImmediate(Node.js环境)等
微任务 Promise.then()、Object.observe、MutationObserver、process.nextTick(Node.js环境)等

EventLoop

EventLoop - 事件循环,就是我们经常使用异步的原理,是指在浏览器或 Node.js 中保证 JavaScript 单线程运行时不会阻塞的一种机制。

先看流程:

图解:

  1. 进入到script标签,就进入到了第一次事件循环
  2. 遇到同步代码,立即执行
  3. 遇到宏任务或微任务时,放入对应的任务队列中
  4. 执行完成所有的同步代码
  5. 执行微任务队列中的任务,若产生新的任务放入对应的队列
  6. 微任务队列清空
  7. 取出下一个宏任务,重复步骤,期间若产生新的任务放入对应的队列
  8. 如此循环往复,直到清空所有宏任务

常见面试题分析

了解了基本原理,我们再来看几个常见的面试题,由浅入深巩固一下。

分析1

我们按步骤分析:

  • s1setTimeout异宏任务,放入宏任务队列;此时宏任务队列:[s1]
  • s2Promise 成功的回调,放入微任务队列;此时微任务队列:[s2]
  • s3Promise 成功的回调,放入微任务队列;此时微任务队列:[s2 ,s3]
  • s4 同步任务,立即执行,输出4
  • 先执行微任务队列的任务,依次输出23
  • 微任务队列清空,查看宏任务队列,有任务则取出压栈执行,s1 任务输出1

由上可知,先执行同步任务,输出4;再从微任务队列取任务执行,依次输出23;最后执行宏队列中的任务,输出1

输出:

分析2

  • s1 setTimeout 异宏任务,放入宏任务队列;此时宏任务队列:[s1]
  • s2 Promise 中的执行函数是同步的,立即执行,会立即输出2;同时返回一个成功回调
  • s3 是微任务,放入微任务队列,此时微任务队列:[s3]
  • s5 是同步任务,立即执行,立即输出5
  • 同步任务执行完毕,取出微任务队列任务执行,此时取出s3 执行,输出3,同时返回成功的回调,又产生一个新的微任务s4 ,将其放入微任务队列,此时微任务队列:[s4]
  • 微任务队列还没清空,还存在s4 任务,继续取出执行,输出4
  • 微任务队列清空,查看宏任务队列,有任务则取出压栈执行,s1 任务输出1

输出:

分析3

整体分析:fn是一个返回值为Promise对象的函数,fn函数调用后紧接着又调用了Promise成功的回调函数。

  • s10fn函数执行
  • 进入到s1Promise 中的执行函数是同步的,所以s2 为同步任务,立即执行,输出1
  • s3 新建一个Promise对象 p,同理执行函数是同步的,所以s4 立即执行,输出2
  • setTimeout为宏任务,放入宏任务队列,此时宏任务队列:[s5 ,s6]
  • s7 是返回一个p的成功回调,参数为5;此时将p.then()s9 ,放入微任务队列,此时微任务队列:[s9]
  • s8 返回的是fn函数返回值 Promise成功的回调,参数为6;此时将s10fn().then()中的s11 放入微队列中,此时微任务队列:[s9 ,s11]
  • s12 为同步任务,立即执行,输出7
  • 先微后宏,取出微任务队列中的任务执行,依次输出56
  • 微任务队列清空,取宏任务队列任务执行;取出s5 执行,输出3
  • s6 不会执行,因为Promise的状态只能改变一次,s7已经改变状态过了。

所以输出:

分析4

整体分析:一个定时器加两个Promise对象。

  • s1 setTimeout 异宏任务,放入宏任务队列;此时宏任务队列:[s1]
  • s2 Promise执行函数是同步执行的,所以s3 会立即执行,输出2
  • s4 返回一个成功的回调s5 ,将s5 放入微任务队列中,此时微任务队列:[s5]
  • 接着往下执行,遇到s13 ,同理执行函数是同步任务,s14 会立即执行,输出8
  • s15 返回一个成功的微任务回调s16 ,放入微任务队列,此时微任务队列:[s5 ,s16]
  • 接下来在微任务队列中,将s5 入栈执行,s6 立即输出3s7 新建一个Promise对象,其执行函数是同步,所以s8 立即执行,输出4s9 返回一个成功的回调s10 ,将其放入微任务队列中,此时微任务队列:[s16 ,s10]
  • s5 执行完毕,同时返回一个成功的回调s12 ,将其放入微任务队列,此时微任务队列:[s16 ,s10 ,s12]
  • 继续取微任务执行,将s16 入栈执行,输出9,此时微任务队列:[s10 ,s12]
  • 继续取微任务执行,将s10 入栈执行,输出5,同时返回一个成功的回调s11 ,将其放入微任务队列,此时微任务队列:[s12 ,s11]
  • 继续取微任务执行,将s12 入栈执行,输出7,此时微任务队列:[s11]
  • 继续取微任务执行,将s11 入栈执行,输出6,此时微任务队列清空
  • 从宏任务队列将s1 入栈执行,输出1

输出:

MyPromise

因为篇幅问题,这里就不按步骤一步步描述了。经过测试,符合Promises/A+的规范。

MyPromise

测试结果:

扩展

最后再来看下面这道经典的面试题,很多大佬的博客中都提到:

我们先按照上面的分析,不难得出结果是:[0 1 2 4 3 5 6]。

我们运行一下看输出:

4为啥在3后面?也就是说return Promise.resolve(4) 产生了2个微任务?

我们再用手写的MyPromise替换系统的运行下结果:

咦,跟我们之前的预期一样,为什么会这样呢?MyPromise和系统的Promise相同的输入得到的结果不一样?难道是MyPromise有缺陷?但为什么Promises/A+规范测试却通过了?

可以肯定的是Promise.resolve()new Promise((resolve, reject) => resolve())中的两者 resolve是不一样的,不能混淆了。

有兴趣的可以看看这里,对照着V8源码,两位大佬也解释得很明白了,这里就不赘述了。


个人的学习总结,有误的地方欢迎指正!

相关推荐
GISer_Jing2 小时前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪3 小时前
CSS复习
前端·css
咖啡の猫5 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲8 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5818 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路8 小时前
GeoTools 读取影像元数据
前端
ssshooter9 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友9 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry10 小时前
Jetpack Compose 中的状态
前端
dae bal10 小时前
关于RSA和AES加密
前端·vue.js