基本概念回顾
1、执行和运行
2、浏览器线程
3、执行栈
Browser Context 的 Event Loop
Browser Context 的 Event Loop 解读
一道题彻底理清 Event loop
先来分析第一段代码
inner.click() 发生了什么
结束语
本期作者:领创集团 ADVANCE.AI 前端工程师 胡晓晓
基本概念回顾
在进入本文的整体之前,我们先来回顾一些基本的概念。
1、执行和运行
执行:依赖于环境,如浏览器、node 等等,在不同环境执行机制不尽相同。
运行:JavaScript 的解析引擎。
2、浏览器线程
GUI 渲染线程
主要负责页面的渲染,解析 HTML、CSS,构建 DOM 树,布局和绘制等。
当界面需要重绘或者由于某种操作引发回流时,将执行该线程。
该线程与 JavaScript 引擎线程互斥,当执行 JavaScript 引擎线程时,GUI 渲染会被挂起,当任务队列空闲时,JavaScript 引擎才会去执行 GUI 渲染。
JavaScript 引擎线程
主要负责处理 JavaScript 脚本,执行代码。
也是主要负责执行准备好待执行的事件,即定时器计数结束或者异步请求成功并正确返回时,将依次进入任务队列,等待 JS 引擎线程的执行。
该线程与 GUI 渲染线程互斥,当 JavaScript 引擎线程执行 JavaScript 脚本时间过长,将导致页面渲染的阻塞。
定时触发器线程
负责执行异步定时器一类的线程,如:setTimeout,setInterval。
主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待 JS 引擎线程执行。
事件触发线程
主要负责将准备好的事件交给 JavaScript 引擎线程执行。
比如 setTimeout 定时器计数结束, ajax 等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待 JS 引擎线程的执行。
异步 http 请求线程
负责执行异步请求一类的线程,如:Promise,axios,ajax 等。
主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待 JavaScript 引擎线程执行。
3、执行栈
--> 控制流程到达当前栈中的下一个上下文
Browser Context 的 Event Loop
宏任务队列是一次只执行一个任务
微任务是一次执行当前微任务队列中的所有任务,
常见的macro-task 如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染等。 常见的 micro-task 如: process.nextTick、new Promise().then(回调)、MutationObserver 等。
Browser Context 的 Event Loop 解读
一开始我们的执行栈为空,全局上下文(script标签)被推入执行栈,代码被执行。
代码执行过程中,会判断是同步任务还是异步任务,同步任务会被放入执行栈,立刻执行;异步任务会交给对应的异步处理模块去处理,异步处理完成后,会把异步任务的回调放入任务队列,当前任务执行完后出栈;
如果主线程任务全部执行完成,执行栈为空,接下来我们会检查微任务队列,如果队列不为空,会把微任务队列的回调放入主线程去执行,直到当前微任务队列被清空;
然后执行宏任务队列,宏任务队列是一次只执行一个任务;
3和4循环往复,直到两个队列都清空。
在 index.html 中有这么一段代码
<div class="outer"> <div class="inner"></div></div>
const outer = document.querySelector('.outer')
const inner = document.querySelector('.inner')
new MutationObserver(function() {
console.log('mutate')
}).observe(outer, {
attributes: true
})
function onClick () {
console.log('click')
setTimeout(() => {
console.log('timeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise')
})
outer.setAttribute('data-random', Math.random())
}
inner.addEventListener('click', onClick)
outer.addEventListener('click', onClick)
click
promise
mutate
click
promise
mutate
timeout
timeout
click
click
promise
mutate
promise
timeout
timeout
上文提到了浏览器内核是多线程的。我们上面这段代码用到了 JavaScript 引擎线程、定时器触发线程以及事件触发线程。
先来分析第一段代码
JavaScript 是单线程,顺序执行代码,所以,当 script 标签里的代码执行完成后,执行栈为空:
点击 inner 时,事件触发线程会把 inner click callback 放入任务队列;发生冒泡,事件触发线程会把outer click callback 放入任务队列;
同步任务继续执行
执行完成出栈
inner.click() 发生了什么
与上一段代码不同,执行 inner.click 之后,Call Stack 队列不为空:
首先是 console.log('click'),同步代码立刻执行,输出`click`
执行到 setTimeout,异步代码,交给定时器触发线程去处理,处理完成后callback放入MacroTasks
执行到 Promise.then 异步代码,放入微任务队列 MicroTasks ;
执行到 MutationObserver 异步代码,放入微任务队列 MicroTasks ;
inner 的click callback代码执行完成,出栈
本文旨在帮助大家理解浏览器的 Event Loop ,以图文的形式呈现给大家,所以图会比较多,如有不同见解,欢迎大家一起探讨。
关于领创集团