原文连接javascript
对于浏览器而言,有多个线程协同合做,以下图。具体细节能够参考一帧剖析。java
对于常说的JS单线程引擎也就是指的 Main Therad
。node
注意以上主线程的每一块未必都会执行,须要看实际状况。 先把 Parse HTML
-> Composite
的过程称为渲染管道流 Rendering pipeline
。git
浏览器内部有一个不停的轮询机制,检查任务队列中是否有任务,有的话就取出交给 JS引擎
去执行。github
例如:web
const foo = () => console.log("First");
const bar = () => setTimeout(() => console.log("Second"), 500);
const baz = () => console.log("Third");
bar();
foo();
baz();
复制代码
过程:segmentfault
一些常见的 webapi
会产生一个 task
送入到任务队列中。api
script
标签XHR
、addEventListener
等事件回调setTimeout
定时器每一个 task
执行在一个轮询中,有本身的上下文环境互不影响。也就是为何,script
标签内的代码崩溃了,不影响接下来的 script
代码执行。promise
pop
,便于 JSer
的世界观改用 shift
)while(true) {
task = taskQueue.shift();
execute(task);
}
复制代码
input event
、 setTimeout
的 callback
可能维护在不一样的队列中。 代码若是操做 DOM
,主线程还会执行渲染管道流。伪代码修改以下:while(true) {
+ queue = getNextQueue();
- task = taskQueue.shift();
+ task = queue.shift();
execute(task);
+ if(isRepaintTime()) repaint();
}
复制代码
button.addEventListener('click', e => {
while(true);
});
复制代码
点击 button
产生一个 task
,当执行该任务时,一直占用主线程卡死,该任务没法退出,致使没法响应用户交互或渲染动态图等。浏览器
改换执行如下代码
function loop() {
setTimeout(loop, 0);
}
loop();
复制代码
看似无限循环执行 loop
,setTimeout
到时后产生一个 task
。执行完 loop
即退出主线程。使得用户交互事件和渲染可以得以执行。
正由于如此,setTimeout
和其余 webapi
产生的 task
执行依赖任务队列中的顺序。 即便任务队列没有其余任务,也不能作到 0秒
运行,setTimeout
定时器到时间 cb
入任务队列,在轮询取出 task
给引擎执行,最少大约 4.7ms
。
function callback() {
moveBoxForwardOnePixel();
requestAnimationFrame(callback);
}
callback()
复制代码
换成 setTimeout
function callback() {
moveBoxForwardOnePixel();
- requestAnimationFrame(callback);
+ setTimeout(callback, 0);
}
callback()
复制代码
对比,能够发现 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
处吗?button.addEventListener('click', () => {
box.style.transform = 'translateX(1000px)';
box.style.transition = 'transform 1s ease-in-out';
box.style.transform = 'translateX(500px)';
});
复制代码
结果:从 0px
移动到 500px
处。因为回调任务的代码块是同步执行的,浏览器不在意中间态。
button.addEventListener('click', () => {
box.style.transform = 'translateX(1000px)';
box.style.transition = 'transform 1s ease-in-out';
- box.style.transform = 'translateX(500px)';
+ requestAnimationFrame(() => {
+ box.style.transform = 'translateX(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';
requestAnimationFrame(() => {
- box.style.transform = 'translateX(500px)';
+ requestAnimationFrame(() => {
+ box.style.transform = 'translateX(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
不符合规范,错误的放在渲染管道流以后执行。
DOMNodeInserted
初衷被设计用来监听 DOM
的改变。
DOMNodeInserted
。document.body.addEventListener('DOMNodeInserted', () => {
console.log('Stuff added to <body>!');
});
for(let i = 0; i < 100; i++) {
const span = document.createElement('span');
document.body.appendChild(span);
span.textContent = 'hello';
}
复制代码
理想 for 循环完毕后,DOMNodeInserted
回调执行一次。 结果:执行了 200
次。添加 span
触发 100
次,设置 textContent
触发 100
。 这就让使用 DOMNodeInserted
会产生极差的性能负担。 为了解决此等问题,建立了一个新的任务队列叫作微任务 Microtasks
。
常见微任务
微任务是在一次事件轮询中取出的 task
执行完毕,即 JavaScript
运行栈(stack)中已经没有可执行的内容了。 浏览器紧接着取出微任务队列中全部的 microtasks
来执行。
loop
会怎样?function loop() {
Promise.resolve().then(loop);
}
loop();
复制代码
你会发现,它跟以前的 while
同样卡死。
如今咱们有了3个不一样性质的队列
task
执行,若是产生new task
入队列。task
执行完毕等待下一次轮询取出next task
。microtask
,若是产生new microtask
,入队列,等待执行,直到队列清空。while(true) {
queue = getNextQueue();
task = queue.shift();
execute(task);
+ while(microtaskQueue.hasTasks()) {
+ doMicrotask();
+ }
if(isRepaintTime()) repaint();
}
复制代码
rAF queue
每一帧渲染管道流开始以前一次性执行完全部队列中的 rAF callback
,若是产生new rAF
等待下一帧执行。while(true) {
queue = getNextQueue();
task = queue.shift();
execute(task);
while(microtaskQueue.hasTasks()) {
doMicrotask();
}
- if(isRepaintTime()) repaint();
+ if(isRepaintTime()) {
+ animationTasks = animationQueue.copyTasks();
+ for(task in animationTasks) {
+ doAnimationTask(task);
+ }
+
+ repaint();
+ }
}
复制代码
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');
});
复制代码
点击按钮会是怎么样的顺序呢?
来分析一下,以上代码块为一个 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
连接跳转吗?
const nextClick = new Promise(resolve => {
link.addEventListener('click', resolve, { once: true });
});
nextClick.then(event => {
event.preventDefault();
// handle event
});
复制代码
若是是代码点击呢?
link.click();
复制代码
暂不揭晓答案,欢迎评论区讨论。
rAF
callback
node 不须要一直轮询有没有任务,清空全部队列就结束。
常见任务队列 task queue
常见微任务 microtask queue
process.nextTick
执行优先级高于 Promise
。
while(tasksAreWaiting()) {
queue = getNextQueue();
while(queue.hasTasks()) {
task = queue.shift();
execute(task);
while(nextTickQueue.hasTasks()) {
doNextTickTask();
}
while(promiseQueue.hasTasks()) {
doPromiseTask();
}
}
}
复制代码
script tag
DOM
相似 node