本篇文章较长,让网络飞一会再看~
本文结构
- 带着问题看这篇文章
- JS Runtime的几个概念
- Event Loop事件循环
- UI Rendering Task
- 可视化:event loop和rendering
- Micro Task微任务
-Event Loop执行顺序
本文共计:5957字20图
预计阅读时间:16min00s
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
})
setTimeout(() => {
console.log(6);
})
console.log(7);
// 结果:1475236
堆一大块内存区域(通常是非结构化的),对象被分配在堆中
JS运行时包含了一个消息队列,每个消息队列关联着一个用于处理这个消息的回调函数。(队列的特点是先进先出)
通常,task queue中的任务被称为:macrotask 宏任务.
以下几种异步API的回调属于宏任务:
alert
或者同步 XHR
,但应该尽量避免使用它们,例外的例外也是存在的[1](但通常是实现导致的错误而非其它原因)。每个消息被完整的执行后,其他消息才会被执行。
优点:当一个函数执行时,它不会被抢占,只有在它运行完毕后才会去运行其他代码,才能修改这个函数操作的数据。
缺点:当一个消息需要太长时间才能处理完,浏览器就无法处理用户交互,eg.滚动和点击,这也是性能较差的网页“卡顿现象”的原因。
因此良好的操作方式是:缩短单个消息处理时间,在可能的情况下尽量将一个消息裁剪成多个消息。以保证浏览器 60 frames per second
的流畅渲染,即每个消息处理时间 < 1000ms/60=16ms,
event loop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。
setTimeout
中的第二个参数n是指 消息被加入消息队列的最小延迟setTimeout 0
的作用:将回调立即放入消息队列,而不是0s内立即执行// demo
function bar(){
debugger
console.log('bar')
foo()
}
function foo(){
debugger
console.log('foo')
setTimeout(function(){
debugger
console.log('setTimeout')
},1000)
}
(function all(){
debugger
console.log('anounymous')
bar()
})()
原理图
postMessage:
// eg. 当一个窗口可以获得另一个窗口的引用时,例如targetWindow = window.opener
otherWindow.postMessage(message, targetOrigin, [transfer]);
otherWindow:其他窗口的引用:
message:要发送到其他窗口的数据,会被结构化克隆算法[6]序列化
targetOrigin:用来指定哪些窗口能接收到消息事件
transfer:一串和message 同时传递的 `Transferable`[7] 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
结构化克隆算法:
用于克隆复杂对象
不能克隆:Error、Symbol、Function对象、DOM节点
不能克隆:属性的描述符、RegExp对象的 lastIndex字段、原型链上的属性
Transferable对象:
一个抽象接口,代表可以在不同可执行上下文中传递的对象。(抽象:没有定义任何属性和方法)
不同执行上下文:例如主线程和webworker之间。
ArrayBuffer 、MessagePort 和 ImageBitmap 实现于此接口。
接收消息:
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event)
{
// event.data:传递来的对象
// event.origin:消息发送方窗口的origin
// event.source:对消息发送窗口的引用
}
具体来讲,如果js runtime 的 call stack 一直不能清空,例如event loop将一个耗时的回调放进了call stack,会导致浏览器主线程被占用,无法执行render相关的工作,用户交互的事件也被添加在消息队列等待调用栈清空得不到执行,因此无法响应用户的操作,造成阻塞渲染的“卡顿”现象。
在event loop处理消息队列时,我们提倡要缩短单个消息处理时间,在可能的情况下尽量将一个消息裁剪成多个消息,rendering task 可以在消息之间执行,以保证保证UI Rendering调用的频率能达到 60 frames per second
(UI Rendering Task执行次数通常是每秒60次,但在大多数遵循W3C建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。),即每次event loop处理消息执行回调所占用的时间 小于 16.67 毫秒。
看下面这段代码,先 append 一个元素再设置display=none去隐藏这个元素,不必担心这个元素会闪现,因为这两行代码会在某一次event loop中执行,只有这两行代码执行完,并且清空了当前调用栈,才有可能执行下一次UI Render task
document.body.appendChild(el)
el.style.display='none'
下面这段代码,重复的显示隐藏一个元素,看起来开销很大,但其实在RenderingTask期间,只会取最终结果来渲染,
button.addEventListener ('click,()=>{
box style. display='none';
box style. display ='block';
box style. display ='none';
box style. display ='block';
box style. display='none';
box style. display ='block';
box style. display ='none';
box style. display ='block';
box style. display ='none';
})
<iframe>
里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命demo1:requestAnimationFrame优化动画的一个例子
// 使用RAF
function callback(){
moveBoxForwardOnePixel();
requestAnimationFrame(callback)
}
callback();
// 使用setTimeout
function callback(){
moveBoxForwardOnePixel();
setTimeout(callback,0)
}
效果:
demo2:用RAF控制动画执行顺序,需求是box元素的水平位置变化:1000→500
button addEventListener ('click,()=>{
box.style.transform = 'translateX(1000px)'
box.style.transition= 'transform 1s ease-in-out'
box.style.transform = 'translateX(500px)'
})
//由于上述代码会一起执行,
//因此渲染时,1000px会被忽略,浏览器会取500作为最终值,在下一帧渲染,
//因此上述代码的效果是:元素位移0->500
//换一种写法
button addEventListener ('click,()=>{
box.style.transform = 'translateX(1000px)'
box.style.transition= 'transform 1s ease-in-out'
requestAnimationFrame(()=>{
box.style.transform = 'translateX(500px)'
})
})
// 上述代码,1000的初始值是有效的,
//但是在下一次的rendering task期间,由于RAF先执行,因此500将1000覆盖
//最终渲染的效果还是元素位移:0->500
//如何令500在下下一次渲染再生效?嵌套调用RAF
button addEventListener ('click,()=>{
box.style.transform = 'translateX(1000px)'
requestAnimationFrame(()=>{
requestAnimationFrame(()=>{
box.style.transition= 'transform 1s ease-in-out'
box.style.transform = 'translateX(500px)'
})
})
})
间隔调用setTimeout的效果:导致浪费
以前的动画仓库的处理方式:setTimeout(animFrame, 1000/60)
但是这种处理方式不稳定,可能会不准确,因为
微任务,microtask,也叫jobs。
一些异步任务执行完成后,其回调会依次进入microtask queue,等待后续被调用,这些异步任务包括:
event loop中任务的执行顺序:
两个重点:
一个直观的例子:
Promise.resolve().then(()=>{
console.log('microtask 1')
})
Promise.resolve().then(()=>{
console.log('microtask 2')
})
console.log('sync code')
setTimeout(()=>{
console.log('macro task 1')
Promise.resolve().then(()=>{
console.log('microtask 3')
})
},0)
setTimeout(()=>{
console.log('macro task 2')
},0)
//结果:
//sync code 同步代码优先执行
//microtask 1 同步代码执行完后,调用栈清空,优先执行 microtask
//microtask 2 同上
//macro task 1 调用栈清空,microtask queue清空,此时可以执行一个位于队首的macro task,执行期间新增一个microtask
//microtask 3 调用栈清空后,由于存在microtask,因此优先执行microtask
//macro task 2 最后执行macro task,清空task queue
流程图
demo1:调用栈未清空,不执行microtask
在控制台中执行一段代码,会当做同步代码来处理。listener1执行后,微任务队列+1,但是因为是同步执行的代码,所以会立即执行listener2,微任务队列+1,所以顺序是listener1,listener2,microtask1,microtask2
demo2:调用栈清空后,microtask 优先于 macro task执行
同步执行两个setTimeout,会将 listener1和listener2加入到task queue,同步代码执行就结束。先执行listener1,将microtask1加入微任务队列,listener1执行完后,调用栈清空,即使这时候task queue还有listener2,也会先执行所有微任务,将所有微任务清空后,再执行listener2,因此输出顺序是 listener1,microtask1,listener2,microtask2
demo3:同demo2
用户点击事件
由于点击事件会被添加到task queue,因此,这个 demo3 的结果和 demo2 结果相同
demo4:同demo1
js调用click()事件
由于是在代码中手动执行click,所以会同步执行两个listener,因此demo4和demo1结构相同。
demo5:micro 优先于 macro执行
demo6:综合实例
// 浏览器中执行
console.log(1);
setTimeout(() => {
console.log(2);// callback2,setTimeout属于宏任务
Promise.resolve().then(() => {
console.log(3)// callback3,Promise.then属于微任务
});
});
new Promise((resolve, reject) => {
console.log(4)// 这里的代码是同步执行的
resolve(5)
}).then((data) => {
console.log(data);// callback5,Promise.then属于微任务
})
setTimeout(() => {
console.log(6);// callback6,setTimeout属于宏任务
})
console.log(7);
// 结果:1475236
// 逻辑:
147是同步执行,同步代码执行完后的queue:
task queue:callback2,callback6
microtask:callback5
此时调用栈已清空,优先执行微任务callback5,调用栈清空
再执行callback2,调用栈清空
此时的queue:
task queue:callback6
microtask:callback3
优先执行微任务callback3,调用栈清空
最后执行callback6
demo7:综合实例
console.log('main start');
setTimeout(() => {
//cb1
console.log('1');
Promise.resolve().then(() => {
//cb2
console.log('2')
});
}, 0);
Promise.resolve().then(() => {
//cb3
console.log('3');
Promise.resolve().then(() => {
//cb4
console.log('4')
});
});
console.log('main end');
//结果:
// main start,main end,3412
main start 和 main end同步执行,同步代码执行完后,调用栈清空,此时的queue:
task queue:cb1
microtask queue:cb3
先执行微任务cb3,执行完后,调用栈清空,此时的queue:
task queue:cb1
microtask queue:cb4
先执行微任务cb4,执行完后,调用栈清空,此时的queue:
task queue:cb1
microtask queue:空
最后执行cb1,然后执行cb2
rendering task的执行顺序
在上面的event loop执行机制中,没有提到rendering task,是因为rendering task是由浏览器自行去决定何时运行的,与当前设备的屏幕刷新率等因素相关,确定的是:
Animation queue
中,在UI Rendering 期间,会清空 Animation queue,与 microtask 不同的是,如果清空 Animation queue 期间,有新的 animation task 被加入到 queue 中,此次 rendering task 执行期间,不会处理新的animation task。macrotask、microtask、animation task的区别,可以看在下面的动图中横向对比:
[1]
例外的例外也是存在的: https://stackoverflow.com/questions/2734025/is-javascript-guaranteed-to-be-single-threaded/2734311#2734311
[2]
html5的规范: https://www.w3.org/TR/html5/webappapis.html#event-loops
[3]
官方文档: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
[4]
官方文档: http://docs.libuv.org/en/v1.x/design.html
[5]
postMessage: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
[6]
结构化克隆算法: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[7]
Transferable
: https://developer.mozilla.org/zh-CN/docs/Web/API/Transferable
[8]
HTML规范: https://www.w3.org/TR/html5/webappapis.html#event-loops
[9]
NodeJS Event Loop 文档: https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/#what-is-the-event-loop
[10]
mdn相关文档: https://developer.mozilla.org/zh-CN/docs/Glossary/Call_stack
[11]
Jake Archibald在JSConf.Asia的演讲视频【In The Loop】,很值得看:: https://www.youtube.com/watch?v=8aGhZQkoFbQ&feature=emb_title
[12]
Philip Roberts在JSConf的演讲视频【What the heck is the event loop anyway】,很值得看: https://www.youtube.com/watch?v=8aGhZQkoFbQ&feature=emb_title
[13]
Philip Roberts做的Event Loop可视化网站: http://latentflip.com/loupe/
[14]
JS Runtime运行时 - MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
- END -