js事件循环机制和ui渲染
事件循环
任务队列
所有的任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行;而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列( Event Queue )的机制来进行协调。
同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入 Event Queue
。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop
(事件循环)。
在事件循环中,每进行一次循环操作称为tick
,通过阅读规范可知,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤可以总结如下:
- 在此次 tick 中选择最先进入队列的任务( oldest task ),如果有则执行(一次)
- 检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtask Queue
- 更新 render
- 主线程重复执行上述步骤
可以用一张图来说明下流程:
那么,什么是 microtasks ?规范中规定,task分为两大类, 分别是 Macro Task (宏任务)和 Micro Task(微任务), 并且每个宏任务结束后, 都要清空所有的微任务,这里的 Macro Task也是我们常说的 task 。
(macro)task
主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
microtask
主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)
示例:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
- 整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 script start
- 遇到 setTimeout,其回调函数被分发到
宏任务 Event Queue
中 - 遇到 Promise,其 then函数被分到到
微任务 Event Queue
中,记为 then1,之后又遇到了 then 函数,将其分到微任务 Event Queue 中,记为 then2 - 遇到 console.log,输出 script end
至此,Event Queue 中存在三个任务,如下表:
宏任务 | 微任务 |
---|---|
setTimeout | then1 |
- | then2 |
- 执行微任务,首先执行then1,输出 promise1, 然后执行 then2,输出 promise2,这样就清空了所有微任务
- 此时,所有的mircotask执行完毕,本轮事件循环结束,UI 开始 render,当 UI render 完毕,开始下一轮事件循环.
- 执行 setTimeout 任务,输出 setTimeout, 至此,输出的顺序是:script start, script end, promise1, promise2, setTimeout
UI渲染
根据HTML Standard,一轮事件循环执行结束之后,下轮事件循环执行之前开始进行 UI render
。即:macro-task任务执行完毕,接着执行完所有的micro-task任务后,此时本轮循环结束,开始执行UI render。UI render完毕之后接着下一轮循环。
💬深入理解JavaScript事件循环机制 (原文)
💬Javascript事件循环机制以及渲染引擎何时渲染UI (补充与渲染UI相关)
💬从多线程到Event Loop全面梳理 (扩展阅读:通过 进程、线程 的角度来解释单线程的JS为什么拥有 异步 的能力)