以前写了一篇 Event Loop 的文章,然而在后续的深刻学习中发现当时有些概念的理解是错误的,因而又从新修改了一下,更新上来。javascript
javascript
是单线程语言,使用的是异步非阻塞的运行方式,不少状况下须要经过事件和回调函数进行驱动,那么这些注册的回调函数,是在何时被运行环境调用的,彼此之间又是以怎样的顺序执行的?这就绕不开一个机制——Event Loop ,也就是事件循环。前端
JS Event Loop 即事件循环,是运行在浏览器环境 / Node 环境中的一种消息通讯机制,它是主线程以外的独立线程。当主线程内须要执行某些可能致使线程阻塞的耗时操做时(好比请求发送与接收响应、文件 I/O、数据计算)主线程会注册一个回调函数并抛给 Event Loop 线程进行监听,本身则继续往下执行,一旦有消息返回而且主线程空闲的状况下,Event Loop 会及时通知主线程,执行对应的回调函数获取信息,以此达到非阻塞的目的。java
在解析 Event Loop
运行机制以前,咱们要先理解栈(stack
)和队列(queue
)的概念。 栈和队列,二者都是线性结构,可是栈遵循的是后进先出(last in first off LIFO),开口封底。而队列遵循的是先进先出 (fisrt in first out,FIFO),两头通透。webpack
Event Loop
得以顺利执行,它所依赖的容器环境,就和这两个概念有关。git
咱们知道,在 js
代码执行过程当中,会生成一个当前环境的“执行上下文( 执行环境 / 做用域)”,用于存放当前环境中的变量,这个上下文环境被生成之后,就会被推入js
的执行栈。一旦执行完成,那么这个执行上下文就会被执行栈弹出,里面相关的变量会被销毁,在下一轮垃圾收集到来的时候,环境里的变量占据的内存就能得以释放。web
这个执行栈,也能够理解为JavaScript
的单一线程,全部代码都跑在这个里面,以同步的方式依次执行,或者阻塞,这就是同步场景。面试
那么异步场景呢?显然就须要一个独立于“执行栈”以外的容器,专门管理这些异步的状态,因而在“主线程”、“执行栈”以外,有了一个 Task
的队列结构,专门用于管理异步逻辑。全部异步操做的回调,都会暂时被塞入这个队列。Event Loop
处在二者之间,扮演一个大管家的角色,它会以一个固定的时间间隔不断轮询,当它发现主线程空闲,就会去到 Task
队列里拿一个异步回调,把它塞入执行栈中执行,一段时间后,主线程执行完成,弹出上下文环境,再次空闲,Event Loop
又会执行一样的操做。。。依次循环,因而构成了一套完整的事件循环运行机制。express
上图比较简洁地描绘了整个过程,只不过其中多了
heap
(堆)的概念,堆和栈,简单来讲,堆是留给开发者分配的内存空间,而栈是原生编译器要使用的内存空间,两者独立。promise
若是只想应付普通点的面试,上面一节的内容就足够了,可是想要答出下面的这条面试题,就必须再次深刻 Event Loop
,了解任务队列的深层原理:microtask
(微任务)和 macrotask
(宏任务)。浏览器
// 请给出下面这段代码执行后,log 的打印顺序 console.log('script start') async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') } async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') }) console.log('script end') // log 打印顺序:script start -> async2 end -> Promise -> script end -> async1 end -> promise1 -> promise2 -> setTimeout 复制代码
若是只有一个单一的 Task
队列,就不存在上面的顺序问题了。但事实状况是,浏览器会根据任务性质的不一样,将不一样的任务源塞入不一样的队列中,任务源能够分为微任务(microtask
) 和宏任务(macrotask
),介于浏览器对两种不一样任务源队列中回调函数的读取机制,形成了上述代码中的执行顺序问题。
微任务包括 process.nextTick
,promise
,MutationObserver
,其中 process.nextTick
为 Node 独有。
宏任务包括 script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
。
了解了微任务宏任务的概念后,咱们就能够完整地分析一边 Event Loop
的执行机制了。
micro-task
为空,macro-task
里有且只有一个 script
脚本(总体代码)script
脚本执行:全局上下文(script 任务)被推入执行栈,代码以同步的方式以此执行。在执行的过程当中,可能会产生新的 macro-task
与 micro-task
,它们会分别被推入各自的任务队列里script
脚本出队:同步代码执行完了,script
脚本会被移出 macro-task
,这个过程本质上是宏任务队列的执行和出队的过程。script
宏任务执行并出队了,这时候执行栈为空,Event Loop
会去 micro-task
中将微任务推入主线程执行,这里的微任务的执行方式和宏任务的执行方式有个很重要的区别,就是:宏任务是一个一个执行,而微任务是一队一队执行的。也就是说,执行一个宏任务,要执行一队的微任务。(注意:在执行微任务的过程当中,仍有可能有新的微任务插入 micro-task
那么这种状况下,Event Loop
仍然须要将本次 Tick
(循环) 下的微任务拿到主线程中执行完毕)Web worker
任务,若是有,则对其进行处理 。在上面浏览器 Event Loop 的执行机制中,有很重要的一块内容,就是浏览器的渲染时机,浏览器会等到当前的 micro-task
为空的时候,进行一次从新渲染。因此若是你须要在异步的操做后从新渲染 DOM 最好的方法是将它包装成 micro
任务,这样 DOM 渲染将会在本次 Tick
内就完成。
看到这里,相信你已经明白上面的那条面试题是怎么一回事了,咱们能够用对 Event Loop 的理解来分析一下这道题目的执行:
// 请给出下面这段代码执行后,log 的打印顺序 console.log('script start') // 这边的 await 可能不太好理解,我换成了另外一种写法 function async1() { async2().then(res => { console.log('async1 end') }) } function async2() { console.log('async2 end') return Promise.resolve(undefined); } async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') }) console.log('script end') // log 打印顺序:script start -> async2 end -> Promise -> script end -> async1 end -> promise1 -> promise2 -> setTimeout 复制代码
本篇文章已收录入 前端面试指南专栏