ECMAScript 中没有 Event Loops
规范的定义,Event Loops
是在 HTML Strandard
中定义的,规范中定义了浏览器何时进行渲染更新,了解Event Loop有助于性能优化
先来看一道题,说出输出结果
new Promise(resolve => { |
什么是事件驱动?
所谓的事件驱动,就是将一切抽象为事件。IO操作完成是一个事件,用户点击一次鼠标是事件,Ajax 完成了是一个事件,一个图片加载完成是一个事件
“任务队列”中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入”任务队列”,等待主线程读取
事件驱动的的实现过程主要靠事件循环完成。进程启动后就进入主循环。主循环的过程就是不停的从事件队列里读取事件。如果事件有关联的handle(也就是注册的callback),就执行handle。一个事件并不一定有callback
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在”任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。
执行栈中的代码,总是在读取”任务队列”之前执行
事件循环 Event Loop
JS 是单线程的,同一时间,只能做同一件事,为了协调事件、用户交互、脚本、UI渲染和网络处理等行为,防止主线程阻塞,Event Loop方案应运而生,事件循环是JS实现异步的方式,也是JS的执行机制。
触发一次click事件,进行一次ajax请求,背后都有 event loop 在运作
任务队列 Task Queue
事物循环通过任务队列的机制进行协调,一个任务列表(task queue) 包含多个任务(task),每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,遵循先进先出的原则
task 任务源举例
- DOM操作任务源:用来操作相应的DOM,例如一个元素以非阻塞的方式插入文档
- 用户交互任务源:对用户的交互做出反应,例如键盘鼠标的输入,响应用户操作的事件(如click)
- 网络任务源:用来响应网络活动
- History traversal任务源:当调用 history.back()等类似API时,将任务插进队列
task 任务源非常宽泛,比如 ajax 的 onload,click 事件,基本上我们经常绑定的各种事件都是task任务源,还有数据库操作(IndexedDB ),需要注意的是setTimeout、setInterval、setImmediate也是task任务源。
JS的异步运行机制
- 所有任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个”任务队列”(task queue)。系统把异步任务放到”任务队列”之中,然后继续执行后续的任务。
- 一旦”执行栈”中的所有任务执行完毕,系统就会读取”任务队列”。如果这个时候,异步任务已经结束了等待状态,就会从”任务队列”进入执行栈,恢复执行。
- 主线程不断重复上面的第三步。
Tick
事件循环中,每一次循环称为 tick, 每一次tick的任务如下:
- 选择最先进入队列的任务 task,如果有则执行
- 检查是否存在 Microtask,如果存在则不停的执行,直至清空 microtask 队列
- 更新render
- 重复以上步骤
宏任务 -> 所有微任务 -> 宏任务
宏任务 macrotask 与 微任务 microtask
异步任务分为 宏任务task
(macrotask) 与 微任务 microtask
,不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop
将它们依次压入执行栈中执行
task
宏任务: script代码执行、setTimeout、setInterval、I/O、UI交互事件、postMessage、requestAnimationFrame、MessageChannel、setImmediate(Node.js环境)microtask
微任务: Promise.then、MutaionObserver、process.nextTick(Node.js环境)
为什么 Promise
属于 microtask
?
Promise 的定义是在ECMAScript规范中定义的,而不在HTML规范中,所以 Promise 可以采用宏任务机制或微任务机制来实现,但使用微任务机制实现已经是一个普遍的共识
console.log('script start'); |
- JS在执行过程中是单线程的,所有任务可以看做是放在两个队列中,执行队列 与 事件队列
- 执行队列是同步的,事件列列是异步的,而微任务 microtask 是处在两个列表之间
当JS执行时,优先执行同步代码,遇到异步代码,会根据任务类型存到对应的异步事件队列中(宏任务放入事件列表,微任务放入执行队列之后,事件队列之前),当执行完同步代码之后,就会执行位于执行列表之后的微任务,然后再执行事件列表中的宏任务
一个浏览器上下文总有一个 event loop 去协调
- 浏览器可以有多个 event loop,browsing contexts和web workers就是相互独立的
- 所有同源的browsing contexts可以共用event loop
什么是浏览器上下文 Browsing contexts
浏览器上下文是一个将 Document 对象呈现给用户的环境。在一个 Web 浏览器内,一个标签页或窗口常包含一个浏览上下文,如一个 iframe 或一个 frameset 内的若干 frame。
执行环境(Runtime) 与 执行引擎(Engine)
执行引擎
执行引擎指虚拟机,对于 Node和Chrome来说是V8,对于Safari来说是 JavaScript Core,对于Firefox来说是SpiderMonkey,对于执行引擎来说,他们需要实现ECMAScript标准
执行环境
执行环境是指浏览器、node、Ringo等,而Event loop是执行环境的执行机制
什么是异步与同步?
一般操作分为发出调用和得到结果两步,发出调用立即得到结果为同步,发出调用,不能立即得到结果,需要额外的操作才能得到结果为异步
同步就是调用之后一直等待,直到返回结果
异步则是调用之后不能直接拿到结果,通过一系列手段才能拿到结果,等待结果的时间可以介入其它任务
event loop、轮询、事件 就是实现异步的方法
为什么JS是单线程的?
为了保证交互效果的一致性,如一个线程在某个DOM节点上添加内容,另一个线程同时将这个DOM节点删除,这里浏览器无法判断优先级
为了利用多核CPU的计算能力,H5提出了Web Worker标准,允许JS创建多个线程,但子线程完全受主线程控制,且不能操作DOM,所以这个新标准并没有改变JS是单线程的本质。
更新渲染(Update the rendering)
每一次事件循环,浏览器都可能会去更新渲染,渲染的基本流程如下:
- 处理 HTML 标记并构建DOM树
- 处理 CSS 标记并构建CSSOM树,将DOM树与CSSOS树合并成一个渲染树
- 根据渲染树来布局,计算每个节点的几何信息
- 将各个节点绘制在屏幕上
绘制是在等待css样式全部加载完成之后进行的,所以样式的加载快慢是首屏呈现快慢的关键点之一
小结
- 渲染时机是在 microtask 之后,事件队列之前执行
- 在一次事件循环中多次修改同一dom,只有最后一次才进行绘制
- 如果希望在每轮事件循环中呈现变动,可以使用 requestAnimationFrame
Nodejs环境JS运行机制
- Node的Event Loop分阶段,阶段有先后,依次是
- expired timers and intervals,即到期的setTimeout/setInterval
- I/O events,包含文件,网络等等
- immediates,通过setImmediate注册的函数
- close handlers,close事件的回调,比如TCP连接断开
- 同步任务及每个阶段之后都会清空microtask队列
- 优先清空next tick queue,即通过process.nextTick注册的函数
- 再清空other queue,常见的如Promise
- V8引擎解析JavaScript脚本。
- 解析后的代码,调用Node API。
- libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
- V8引擎再将结果返回给用户。
执行栈 -> process.nextTick -> 任务队列 -> setImmediate
process.nextTick(function A() { |
setImmediate(function (){ |
一次事件循环中,如果有多个 process.nextTick 会在执行栈之后,一次性全部执行,多个setImmediate 则可能需要多次循环依次执行
process.nextTick 是在本次事件循环之初触发,且不用检查任务队列,所以在时间上更快,执行效率更高,但是如果process.nextTick 事件太多,执行时长过长也会阻塞事件循环
console.log(1) |
阅读参考: