我们以往常看的八股,事件循环包括微任务和宏任务,但浏览器事件循环真的只有这两种任务吗?
什么是事件循环
首先明确一个概念,事件循环是用来干啥的,Event Loop(事件循环)是用来协调事件、用户交互、脚本、渲染、网络的一种浏览器内部机制。 Event Loop 在浏览器内也分几种:
- window event loop
- worker event loop
- worklet event loop
我们这里主要讨论的是 window event loop。也就是浏览器一个渲染进程内主线程所控制的 Event Loop。
task queue(任务队列)
一个 Event Loop 有一个或多个 task queues。一个 task queue 是一系列 tasks 的集合。 注:一个 task queue 在数据结构上是一个集合,而不是队列,因为事件循环处理模型会从选定的 task queue 中获取第一个可运行任务(runnable task),而不是使第一个 task 出队。上述内容来自 HTML 规范。这里让人迷惑的是,明明是集合,为啥还叫“queue”啊 T.T
task(宏任务)
一个 task 可以有多种 task sources (任务源),有哪些任务源呢?来看下规范里的 Gerneric task sources DOM 操作任务源,比如一个元素以非阻塞的方式插入文档 用户交互任务源,用户操作(比如 click)事件 网络任务源,网络 I/O 响应回调 history traversal 任务源,比如 history.back() 除此之外还有像 Timers (setTimeout、setInterval 等)、IndexDB 操作也是 task source。
microtask(微任务)
一个 event loop 有一个 microtask queue,不过这个 “queue” 它确实就是那个 FIFO 的队列。 规范里没有指明哪些是 microtask 的任务源,通常认为以下几个是 microtask promises MutationObserver Object.observe process.nextTick (这个东西是 Node.js 的 API,暂且不讨论)
Event Loop 处理过程
- 在所选 task queue中约定必须包含一个可运行任务。如果没有此类 task queue,则跳转至下面 microtasks 步骤。
- 让 taskQueue中最老的 task (oldestTask) 变成第一个可执行任务,然后从 taskQueue 中删掉它。
- 将上面 oldestTask 设置为 event loop 中正在运行的 task。
- 执行 oldestTask。
- 将 event loop 中正在运行的oldestTask设置为 null。这时我们这个oldestTask已经执行完了
- 执行 microtasks 检查点(也就是执行 microtasks 队列中的任务
- 这时候js第一轮event loop结束,现在要判断是否需要去渲染,如果当前是 window event loop 且 task queues 里没有 task 且 microtask queue 是空的,同时渲染时机变量 hasARenderingOpportunity 为 false,那就开始更新渲染,但不满足这个条件,则继续回到第1个步骤
- 更新渲染。
大体上来说,event loop 就是不停地找 task queues 里是否有可执行的 task ,如果存在即将其推入到 call stack (执行栈)里执行,并且在合适的时机更新渲染。
再然后,那么更新渲染又是怎样的呢
- 遍历当前浏览上下文中所有的 document ,必须按在列表中找到的顺序处理每个 document 。
- 渲染时机(Rendering opportunities):如果当前浏览上下文中没有到渲染时机则将所有 docs 删除,取消渲染(此处是否存在渲染时机由浏览器自行判断,根据硬件刷新率限制、页面性能或页面是否在后台等因素)。
- 如果当前文档不为空,设置hasARenderingOpportunity 为 true 。
- 不必要的渲染(Unnecessary rendering):如果浏览器认为更新文档的浏览上下文的呈现不会产生可见效果且文档的 animation frame callbacks 是空的,则取消渲染。
- 如果文档的浏览上下文是顶级浏览上下文,则刷新该文档的自动对焦候选对象。
- 处理 resize 事件,传入一个 performance.now() 时间戳。
- 处理 scroll 事件,传入一个 performance.now() 时间戳。
- 处理媒体查询,传入一个 performance.now() 时间戳。
- 运行 CSS 动画,传入一个 performance.now() 时间戳。
- 处理全屏事件,传入一个 performance.now() 时间戳。
- 执行 requestAnimationFrame 回调,传入一个 performance.now() 时间戳。
(requestAnimationFrame并没有在宏任务和微任务里) - 执行 intersectionObserver 回调,传入一个 performance.now() 时间戳。
- 对每个 document 进行绘制。
- 更新 ui 并呈现。
- 去执行 idle period(requestIdleCallback)。
下图是v8提供的一个比较清晰的事件循环机制图解
所以在做动画时,setTimeout比requestAnimationFrame执行更提前
首先,浏览器渲染有个渲染时机(Rendering opportunity)的问题,也就是浏览器会根据当前的浏览上下文判断是否进行渲染,它会尽量高效,只有必要的时候才进行渲染,如果没有界面的改变,就不会渲染。
按照规范里说的一样,因为考虑到硬件的刷新频率限制、页面性能以及页面是否存在后台等等因素,有可能执行完 setTimeout 这个 task 之后,发现还没到渲染时机,所以 setTimeout 回调了几次之后才进行渲染,此时设置的 marginLeft 和上一次渲染前 marginLeft 的差值要大于 1px 的。requestAnimationFrame 帧动画不同之处在于,每次UI渲染之前都会调用,此时设置的 marginLeft 和上一次渲染前 marginLeft 的差值为 1px 。所以这样一来requestAnimationFrame做的动画会比setTimeout更加流畅
各种题型
基础题
console.log(1);
setTimeout(function () {
console.log(2);
}, 0);
Promise.resolve()
.then(function () {
console.log(3);
})
.then(function () {
console.log(4);
});
// 1 -> 3 -> 4 -> 2
中阶题目
console.log("begins");
setTimeout(() => {
console.log("setTimeout 1");
Promise.resolve().then(() => {
console.log("promise 1");
});
}, 0);
new Promise(function (resolve, reject) {
console.log("promise 2");
setTimeout(function () {
console.log("setTimeout 2");
resolve("resolve 1");
}, 0);
}).then((res) => {
console.log("dot then 1");
setTimeout(() => {
console.log(res);
}, 0);
});
// "begins";
// "promise 2";
// "setTimeout 1";
// "promise 1";
// "setTimeout 2";
// "dot then 1";
// "resolve 1";
进阶题
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
// "script start";
// "async1 start";
// "async2";
// "promise1";
// "script end";
// "async1 end";
// "promise2";
// "setTimeout";
面试官想知道的
- 宏任务和微任务基本的运行机制(很多人都知道)
- 宏任务和微任务有哪些(很多人都知道)
- requestAnimationFrame和requestAnimationFrame属于是微任务还是宏任务(加分点)
- 渲染的过程(加分点)