开门见题
本文从一道事件循环的题开始,尝试说明事件循环过程。
javascript
<script>
const button1 = document.getElementById('button1')
function logHandler() {
console.log('start')
setTimeout(() => {
console.log('st1')
const innerLog = () => {
console.log('inner log')
}
Promise.resolve().then(innerLog)
}, 0);
const res1Callback1 = (res)=> {
console.log(res)
return 'then1'
}
const res1Callback2 = (res)=> {
console.log(res)
}
Promise.resolve('res1').then(res1Callback1).then(res1Callback2)
const timerLog = () => {
console.log('st2')
}
setTimeout(timerLog, 0);
}
button1.addEventListener('click', logHandler)
</script>
思考一下,尝试写出答案
Performance 查看任务
Chrome Developer Tools 中的 Performance(性能)面板是一个强大的工具,专门用于分析和调试网页或Web应用程序的性能问题。其中可以查看到主线程任务的执行情况。我们这边仅使用该功能。来看一张 Performance的截图:
上面的代码给按钮绑定了一个 click
事件,执行了 logHandler
事件。查看 Performance 面板的 main 面板(红色箭头指向)。可以看到 logHandler
事件中执行了 4 个子任务。接下来挨个看看其中具体做了哪些事情。
火焰图执行栈
如果你之前没有使用过 Performance, 你可能不清楚,这些大大小小的方块表示什么。 x 轴表示随时间变化的记录。y 轴表示调用堆栈。顶部的事件会导致以下事件。
调用栈:
当函数中抛出错误,可以看到它的完整调用栈,代码最先是从脚本代码段
执行,又 main() -> script1() -> script2()抛出错误。
所以可以将火焰图看做一个倒挂的调用堆栈,由于 main() 函数中可以调用多个子函数,所以上面的方块(表示调用方)总是会比下面的长。并且火焰图中展示的是从 JS 引擎线程
和 渲染线程
发起的任务。本文中仅考虑 JS
的部分,渲染任务简单说明。
Performance.mark
知道了任务执行的流程,但是主线程中的方块比较多,为了方便查看,我们需要在火焰图中标记我们执行的函数,我们可以通过下面代码生成我们想要的标记。
JS
function logHandler() {
// 已一个标志开始
performance.mark('start')
console.log('start')
const timerLog = () => {
console.log('st2')
// 添加函数执行标志
performance.mark('start--timerLog')
//测量两个不同的标志。Performance 面板中可以看到一个 'start--timerLog' 调用
performance.measure('startToTimerLog', 'start', 'start--timerLog');
}
setTimeout(timerLog, 0);
}
完整标志代码
JS
<script>
const button1 = document.getElementById('button1')
function logHandler() {
performance.mark('start')
console.log('start')
setTimeout(() => {
console.log('st1')
const innerLog = () => {
console.log('inner log')
performance.mark('start---inner')
// 在 performance 中会添加一个 'start---inner' 函数名
performance.measure('measureInner', 'start', 'start---inner');
}
Promise.resolve().then(innerLog)
}, 0);
const res1Callback1 = (res) => {
performance.mark('start---res1')
// 在 performance 中会添加一个 'start---res1' 函数名
performance.measure('measureRes1', 'start', 'start---res1');
console.log(res)
return 'then1'
}
const res1Callback2 = (res) => {
console.log(res)
performance.mark('start---res2')
// 在 performance 中会添加一个 'start---res2' 函数名
performance.measure('measureRes2', 'start', 'start---res2');
return 'then2'
}
Promise.resolve('res').then(res1Callback1).then(res1Callback2)
const timerLog = () => {
console.log('st2')
performance.mark('start--timerLog')
// 在 performance 中会添加一个 'start--timerLog' 函数名
performance.measure('startToTimerLog', 'start', 'start--timerLog');
}
setTimeout(timerLog, 0);
}
button1.addEventListener('click', logHandler)
</script>
事件循环
上面都是对于 Performance 面板的介绍,终于可以正式开始分析了。我们都知道 js 是单线程。在运行时,js引擎一次只能执行一行代码。
- 同步代码: 当JavaScript引擎遇到同步代码时,它会立刻执行并阻塞主线程直到完成。
- 异步代码 :
- 宏任务(Macrotasks) : 宏任务是一类较为传统的异步任务,它们在事件循环的下一个循环迭代中执行。
- 微任务(Microtasks) : 微任务是在当前宏任务执行栈清空之后、渲染之前立即执行的任务。相比宏任务,它们拥有更高的执行优先级。
分析代码,查看标志验证
由于事件循环是一轮轮反复把任务推到主线程中执行的过程,接下来就用 第一轮
、第二轮
来描述一个任务的执行过程,一个任务中是包含宏任务和微任务两部分。
当 click 事件触发 logHandler() 函数时,作为第一轮宏任务。
第一轮宏任务
- log('start')
- 注册第一个setTimeout (注意是注册不是执行)
- 注册第二个setTimeout
第一轮微任务
- Promise.resolve('res1').then(res1Callback1) res1Callback1 回调执行
- res1Callback2 回调执行
第二轮宏任务 第二轮任务在第一个 setTimeout 回调函数中
- log('st1')
第二轮微任务
- Promise.resolve().then(innerLog) innerLog 回调执行
第三轮宏任务 第三轮任务在第二个 setTimeout 回调函数中
- log('st2')
第三轮微任务
无
验证
第一轮
- 同步代码执行
- 注册第一个 setTimeout
- 注册第二个 setTimeout
任务2跳过 第二轮
-
触发宏任务,由于 setTimeout 回调是一个匿名监听函数,所以这里展示的是匿名
-
触发微任务,微任务也是在同一个任务中触发,属于同一个迭代,并且和函数调用同级。
第三轮
- 触发宏任务
结果
输出结果如下
js
start
res
then1
st1
inner log
st2
浏览器中常见的宏任务和微任务
宏任务(Macrotasks) :
script
(整体脚本,即一开始执行的主程序代码)setTimeout
setInterval
requestAnimationFrame
- I/O 操作(例如网络请求、文件读写)
- UI 渲染(浏览器环境中,每一帧的渲染被认为是一个宏任务)
微任务(Microtasks) :
Promise
的.then()
、.catch()
和.finally()
回调MutationObserver
(用于监控DOM树变化的API)process.nextTick()
(Node.js环境特有的,用于在当前事件循环的下一次循环中优先执行回调)Promise.resolve().then()
或者使用原生Promise的微任务队列特性queueMicrotask()
API(一个更通用的方式来调度微任务)