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源码,两位大佬也解释得很明白了,这里就不赘述了。


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

相关推荐
还是大剑师兰特27 分钟前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解27 分钟前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~34 分钟前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding38 分钟前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT43 分钟前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓43 分钟前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶213643 分钟前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了43 分钟前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django
张张打怪兽1 小时前
css-50 Projects in 50 Days(3)
前端·css