实际上在这个使用篇中就已经穿插了一些原理,如 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 单线程运行时不会阻塞的一种机制。
先看流程:
图解:
- 进入到script标签,就进入到了第一次事件循环
- 遇到同步代码,立即执行
- 遇到宏任务或微任务时,放入对应的任务队列中
- 执行完成所有的同步代码
- 执行微任务队列中的任务,若产生新的任务放入对应的队列
- 微任务队列清空
- 取出下一个宏任务,重复步骤,期间若产生新的任务放入对应的队列
- 如此循环往复,直到清空所有宏任务
常见面试题分析
了解了基本原理,我们再来看几个常见的面试题,由浅入深巩固一下。
分析1

我们按步骤分析:
- s1 中
setTimeout异宏任务,放入宏任务队列;此时宏任务队列:[s1] - s2 中
Promise成功的回调,放入微任务队列;此时微任务队列:[s2] - s3 中
Promise成功的回调,放入微任务队列;此时微任务队列:[s2 ,s3] - s4 同步任务,立即执行,输出
4 - 先执行微任务队列的任务,依次输出
2、3 - 微任务队列清空,查看宏任务队列,有任务则取出压栈执行,s1 任务输出
1
由上可知,先执行同步任务,输出4;再从微任务队列取任务执行,依次输出2、3;最后执行宏队列中的任务,输出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成功的回调函数。
- s10 中
fn函数执行 - 进入到s1 ,
Promise中的执行函数是同步的,所以s2 为同步任务,立即执行,输出1 - s3 新建一个
Promise对象p,同理执行函数是同步的,所以s4 立即执行,输出2 setTimeout为宏任务,放入宏任务队列,此时宏任务队列:[s5 ,s6]- s7 是返回一个
p的成功回调,参数为5;此时将p.then()的s9 ,放入微任务队列,此时微任务队列:[s9] - s8 返回的是
fn函数返回值 Promise成功的回调,参数为6;此时将s10 的fn().then()中的s11 放入微队列中,此时微任务队列:[s9 ,s11] - s12 为同步任务,立即执行,输出
7 - 先微后宏,取出微任务队列中的任务执行,依次输出
5、6 - 微任务队列清空,取宏任务队列任务执行;取出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 立即输出
3;s7 新建一个Promise对象,其执行函数是同步,所以s8 立即执行,输出4;s9 返回一个成功的回调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源码,两位大佬也解释得很明白了,这里就不赘述了。
个人的学习总结,有误的地方欢迎指正!