实际上在这个使用篇中就已经穿插了一些原理,如 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源码,两位大佬也解释得很明白了,这里就不赘述了。
个人的学习总结,有误的地方欢迎指正!