You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
document.body.addEventListener('DOMNodeInserted',()=>{console.log('Stuff added to <body>!');});for(leti=0;i<100;i++){constspan=document.createElement('span');document.body.appendChild(span);span.textContent='hello';}
事件循环--JSConf 分享整理
原文链接
对于浏览器而言,有多个线程协同合作,如下图。具体细节可以参考一帧剖析。
对于常说的JS单线程引擎也就是指的
Main Therad
。注意以上主线程的每一块未必都会执行,需要看实际情况。
先把
Parse HTML
->Composite
的过程称为渲染管道流Rendering pipeline
。浏览器内部有一个不停的轮询机制,检查任务队列中是否有任务,有的话就取出交给
JS引擎
去执行。例如:
过程:
任务队列 Tasks Queue
一些常见的
webapi
会产生一个task
送入到任务队列中。script
标签XHR
、addEventListener
等事件回调setTimeout
定时器每个
task
执行在一个轮询中,有自己的上下文环境互不影响。也就是为什么,script
标签内的代码崩溃了,不影响接下来的script
代码执行。pop
,便于JSer
的世界观改用shift
)input event
、setTimeout
的callback
可能维护在不同的队列中。代码如果操作
DOM
,主线程还会执行渲染管道流。伪代码修改如下:点击
button
产生一个task
,当执行该任务时,一直占用主线程卡死,该任务无法退出,导致无法响应用户交互或渲染动态图等。改换执行以下代码
看似无限循环执行
loop
,setTimeout
到时后产生一个task
。执行完loop
即退出主线程。使得用户交互事件和渲染能够得以执行。正因为如此,
setTimeout
和其他webapi
产生的task
执行依赖任务队列中的顺序。即使任务队列没有其他任务,也不能做到
0秒
运行,setTimeout
定时器到时间cb
入任务队列,在轮询取出task
给引擎执行,最少大约4.7ms
。requestAnimationFrame
换成
setTimeout
对比,可以发现
setTimeout
移动明显比rAF
移动快很多(3.5倍左右)。意味着
setTimeout
回调过于频繁,这并不是一件好事。渲染管道流不一定发生在每个
setTimeout
产生的task
之间,也可能发生在多个setTimeout
回调之后。由浏览器决定何时渲染并且尽可能高效,只有值得更新才会渲染,如果没有就不会。
如果浏览器运行在后台,没有显示,浏览器就不会渲染,因为没有意义。大多数情况下页面会以固定频率刷新,
保证
60FPS
人眼就感觉很流畅,也就是一帧大约16ms
。频率高,人眼看不见无意义,低于人眼能发现卡顿。在主线程很空闲时,
setTimeout
回调能每4ms
左右执行一次,留2ms
给渲染管道流,setTimeout
一帧内能执行大概3.5次
。3.5ms * 4 + 2ms = 16ms
。setTimeout
调用次数太多3-4次
,多于用户能够看到的,也多于浏览器能够显示的,大约3/4是浪费的。很多老的动画库,用
setTimeout(animFrame, 1000 / 60)
来优化。但
setTimeout
并不是为动画而生,执行不稳定,会产生飘移或任务过重会推迟渲染管道流。requestAnimationFrame
正是用来解决这些问题的,使一切整洁有序,每一帧都按时发生。推荐使用
requestAnimationFrame
包裹动画工作提高性能。它解决这个setTimeout
不确定性与性能浪费的问题,由浏览器来保证在渲染管道流之前执行。0px
移动到1000px
处,再到500px
处吗?结果:从
0px
移动到500px
处。由于回调任务的代码块是同步执行的,浏览器不在乎中间态。结果:依然从
0px
移动到500px
处。这是因为在
addEventListener
的task
中同步代码修改为1000px
。在渲染管道流中的计算样式执行之前,需要执行
rAF
,最终的样式为500px
。500px
。button.addEventListener('click', () => { box.style.transform = 'translateX(1000px)'; box.style.transition = 'transform 1s ease-in-out'; + getComputedStyle(box).transform; box.style.transform = 'translateX(500px)'; });
getComputedStyle
会导致强制重排,渲染管道流提前执行,多余操作损耗性能。Edge
和Safari
的rAF
不符合规范,错误的放在渲染管道流之后执行。微任务 Microtasks
DOMNodeInserted
初衷被设计用来监听DOM
的改变。DOMNodeInserted
。理想 for 循环完毕后,
DOMNodeInserted
回调执行一次。结果:执行了
200
次。添加span
触发100
次,设置textContent
触发100
。这就让使用
DOMNodeInserted
会产生极差的性能负担。为了解决此等问题,创建了一个新的任务队列叫做微任务
Microtasks
。常见微任务
微任务是在一次事件轮询中取出的
task
执行完毕,即JavaScript
运行栈(stack)中已经没有可执行的内容了。浏览器紧接着取出微任务队列中所有的
microtasks
来执行。loop
会怎样?你会发现,它跟之前的
while
一样卡死。现在我们有了3个不同性质的队列
task
执行,如果产生new task
入队列。task
执行完毕等待下一次轮询取出next task
。microtask
,如果产生new microtask
,入队列,等待执行,直到队列清空。rAF queue
每一帧渲染管道流开始之前一次性执行完所有队列中的rAF callback
,如果产生new rAF
等待下一帧执行。点击按钮会是怎么样的顺序呢?
来分析一下,以上代码块为一个
task 0
。task 0
执行完毕后,webapi
监听事件。click
事件,task queue
中入队task 1
、task 2
。task 1
执行,Microtask queue
入队Microtask 1
。console
输出Listener 1
。task 1
执行完毕。microtask
(目前只有Microtask 1
),取出执行,console 输出Microtask 1
。task 2
执行,Microtask queue
入队Microtask 2
。console
输出Listener 2
。task 2
执行完毕。microtask
,取出Microtask 2
执行,console 输出Microtask 2
。答案:
Listener 1
->Microtask 1
->Listener 2
->Microtask 2
如果你答对了,那么恭喜你,超越了
87%
的答题者。button.addEventListener('click', () => { Promise.resolve().then(() => console.log('Microtask 1')); console.log('Listener 1'); }); button.addEventListener('click', () => { Promise.resolve().then(() => console.log('Microtask 2')); console.log('Listener 2'); }); + button.click();
思路一样分析
task 0
执行到button.click()
等待事件回调执行完毕。Listener 1
,Microtask queue
入队Microtask 1
。console
输出Listener 1
。Listener 2
,Microtask queue
入队Microtask 2
。console
输出Listener 2
。click
函数return
,结束task 0
。microtask
,取出Microtask 1
执行,console 输出Microtask 1
。Microtask 2
执行,console 输出Microtask 2
。答案:
Listener 1
->Listener 2
->Microtask 1
->Microtask 2
在做自动化测试时,需要小心,有时会产生和用户交互不一样的结果。
以下代码,用户点击,会阻止
a
链接跳转吗?如果是代码点击呢?
暂不揭晓答案,欢迎评论区讨论。
node
rAF
callback
node 不需要一直轮询有没有任务,清空所有队列就结束。
常见任务队列
task queue
常见微任务
microtask queue
process.nextTick
执行优先级高于Promise
。web worker
script tag
DOM
类似
node
参考
The text was updated successfully, but these errors were encountered: