lastModified: 2024-08-29
前言
最近因为个人原因, 打算从现在的公司辞职了. 要再找工作就要面试(虽然可能没有机会也说不定), 就需要背背一些八股文, 看看一些工作里没有用到, 但面试可能用到的东西.
Event Loop
Event Loop 是什么?
Event Loop 是JavaScript中的一种机制, 用于管理和执行异步任务. 它不断地从消息队列中取出任务并执行, 确保程序不会被阻塞. Event Loop 的核心是消息循环, 它使得JavaScript能够处理异步事件和操作, 是实现异步编程的关键.
以上是来源于网络关于Event Loop的简单介绍.
Event Loop本身的实现是多个优先级不同的队列, 调度程序会按照优先级来决定执行哪个队列中的什么任务.
由于js现在是一个多端语言, 即nodejs和浏览器, 所以Event Loop也有所区别. nodejs的事件循环是通过libuv实现的, 而浏览器的则是自己实现的.
比如chromium的Blink/V8, 火狐的Gecko/SpiderMonkey. Edge是基于chromium的, 所以和chromium的Event Loop是一样的.
以下实验的环境为:
- Edge 126
国产手机的自带浏览器有可能会页面崩溃.
宏任务与微任务
在大部分的文章中, 都是说浏览器具有宏任务和微任务, 但是在实际代码中, 浏览器只有微任务和任务的说法. 如果下载V8或者webkit的源码, 会发现压根就搜索不到macrotask这个词, 只有microtask.
不过这个概念并不影响理解, 而且我也不会C++, 也读不懂C++的源码, 所以这里依然以宏任务和微任务来理解. 不过就算是宏任务, 在现代浏览器中, 也分为了很多种类型. 比如setTimeout, setInterval这样的定时任务, 以及ajax和用户事件之类的异步任务.
宏任务与微任务的执行顺序
微任务这个概念是在ES6 Promise出现的时候流行的, 在此之前只有MutaionObserver是浏览器原生支持的微任务.
举个例子:
MutaionObserver
javascript
const div = document.createElement('div');
document.body.appendChild(div);
const observer = new MutationObserver(() => {
out.innerHTML += '<p>mutaion</p>';
});
observer.observe(div, { childList: true });
const mc = new MessageChannel();
mc.port1.start();
mc.port1.addEventListener('message', () => {
out.innerHTML += '<p>message channel</p>';
});
mc.port2.postMessage('sss');
div.innerHTML = '<p>hello</p>';
我们可以看到, 就算是后执行MutaionObserver也是先输出的是mutation, message channel, 这说明MutationObserver的是在MessageChannel之前触发的,也就是所谓的微任务. 这里为什么不使用setTimeout呢? 是因为setTimeout会有一个最小延时, 浏览器会等待这个延时结束之后再执行setTimeout的回调.
关于微任务
微任务的优先级非常高, 在当前函数执行完成后, 会立刻执行微任务队列的任务. 并且这个过程中, 如果微任务本身又在队列中添加微任务, 这个队列就会一直无法执行完.
同时, 微任务执行早于浏览器的渲染(虽然不太准确), 也就是说, 微任务会在浏览器渲染之前执行, 渲染之后再执行微任务. 除了只需要GPU渲染的以外, 比如transform动画, 滚动之类的, 其他的渲染就会停止. 同时因为微任务队列长期未清空, 所以用户操作的回调也无法执行, 比如点击事件.
被卡住的渲染
javascript
const div = document.createElement('div');
const button = document.createElement('button');
button.innerHTML = 'click';
button.onclick = () => {
div.innerHTML = '<p>click</p>';
run();
};
document.body.appendChild(button);
document.body.appendChild(div);
div.className = 'ani';
div.innerHTML = '<p>hello</p>';
let i = 0;
function run() {
if (i++ < 10000000) {
Promise.resolve().then(run);
}
}
会被卡住的动画
css
.ani {
position: absolute;
animation: lefts 5s linear infinite;
}
@keyframes lefts {
0% {
left: 0;
}
100% {
left: 700px;
}
}
点击上面的按钮, 我们会发现, 页面卡死了, 点击和选择都无法响应, 同时动画也停止了执行. 等过了一阵后, 界面上的文字才会变成修改后的值, 同时动画发生了跳帧然后继续执行.
宏任务中, 相互之间的执行顺序
点击事件和MessageChannel
微任务会在浏览器渲染之前执行(虽然不太准确), 上面做了一个验证, 那么宏任务之间有没有顺序问题呢?
目前能够直接调用的常用宏任务:
- setTimeout
- setInterval
- MessageChannel
- XMLHttpRequest
- 用户操作事件
如何验证这个东西, 确实是不太容易, 我们无法保证每一步都是在同一个任务队列执行. 不过前面提到了微任务会阻塞宏任务, 那么也可以利用这一点, 让所有的宏任务按照能够验证的方式执行.
我们验证一下用户操作事件和MessageChannel的顺序:
点击事件和MessageChannel的执行顺序
javascript
const button = document.createElement('button');
button.innerText = 'Click me';
let start = 0;
function run() {
if (Date.now() - start < 10000) {
Promise.resolve().then(run);
}
}
const mc = new MessageChannel();
mc.port1.start();
mc.port1.addEventListener('message', () => {
out.innerHTML += 'MessageChannel!<br />';
});
let flag = false;
button.onclick = () => {
if (flag) {
out.innerHTML += 'Clicked!<br />';
return;
}
start = Date.now();
flag = true;
mc.port2.postMessage({});
run();
};
document.body.appendChild(button);
我们快速点击两下按钮, 会发现输出的顺序是: click -> message channel. 在代码中, 第一次执行的时候是先触发了message channel的postMessage, 然后再次点击的时候因为微任务卡住了, 没有响应. 如果是顺序执行, 那么理论上应该是先执行message channel, 再执行click.
这个执行结果代表点击事件的优先级是高于message channel的.
点击事件和XMLHttpRequest
点击事件和xhr的执行顺序
javascript
const button = document.createElement('button');
button.innerText = 'Click me';
let start = 0;
function run() {
if (Date.now() - start < 2000) {
Promise.resolve().then(run);
}
}
function xhr() {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://dog.ceo/api/breeds/image/random');
xhr.onload = () => {
out.innerHTML += 'XHR!<br />';
};
xhr.send();
}
let flag = false;
button.onclick = () => {
if (flag) {
out.innerHTML += 'Clicked!<br />';
return;
}
start = Date.now();
flag = true;
xhr();
setTimeout(() => {
run();
}, 100);
};
document.body.appendChild(button);
这里使用setTimout延迟了微任务的执行, 否则会导致xhr的请求无法发出. 第一次点击后间隔一秒再次点击按钮:
从结果上能看到, xhr的也是在点击回调之后执行了, 这一点可以从控制台的网络/Network模块看到. 点击后请求立即发了出去, 但是在响应后, 依然是点击事件的回调先进行了执行.
网络没有排队, 但是显示顺序依然是click -> xhr.
由此证明了点击事件的优先级是比XMLHttpRequest高的.
MessageChannel和Timeout
MessageChannel和Timeout
javascript
const button = document.createElement('button');
button.innerText = 'Click me';
let start = 0;
function run() {
if (Date.now() - start < 2000) {
Promise.resolve().then(run);
}
}
const mc = new MessageChannel();
mc.port1.start();
mc.port1.addEventListener('message', () => {
out.innerHTML += 'MessageChannel!<br />';
});
let flag = false;
button.onclick = () => {
if (flag) {
return;
}
start = Date.now();
flag = true;
setTimeout(() => {
out.innerHTML += 'Timeout!<br />';
});
mc.port2.postMessage({});
run();
};
document.body.appendChild(button);
这里是按照进入队列的顺序执行的, 哪个先插入就先输出. 因此可以认为两者执行优先级是一样的, 也代表Timeout的优先级是比点击事件低的.
规范的描述
前端规范组织中, 关于fetch, dom, 事件循环等浏览器API是由一个叫做whatwg的组织指定的.
其中描述了关于任务队列的一些信息.
有说到存在多个任务队列的事情, 因此结合各种网络文章可以认为:
- 浏览器执行过程中存在多个队列, 比如交互队列, 延时队列
- 任务没有优先级, 但是队列有
- 上面的实验证明, 交互队列的优先级在宏任务中是最高的
原先规范中貌似是有宏任务的说法的, 但是后来随着浏览器复杂度越来越高, 就移除了. 现在只有微任务和任务的概念.
结论
本文通过对Event Loop机制的探讨, 不仅阐述了其在JavaScript异步编程中的重要性, 还通过一系列实验验证了不同类型的异步任务(包括宏任务与微任务)之间的执行顺序。研究发现:
- 微任务相较于宏任务拥有更高的优先级, 在每个宏任务执行完毕后立即得到处理, 这有助于确保某些关键操作能及时完成.
- 宏任务内部也有其自身的执行队列顺序, 用户交互的队列要比其他宏任务队列先执行.
- Timeout与MessageChannel的执行顺序取决于它们进入队列的时间, 表明两者在优先级上处于同一级别.
这个内容本身基本都是理论内容, 平时业务开发也基本用不上的东西. 不过理解这个过程还是挺有意思, 为此还稍微翻了翻webkit和v8/blink的源码(虽然没看懂多少), 算是一次有趣的体验?