什么是事件循环
事件循环是一种调度机制,负责协调渲染主线程和消息队列之间的执行顺序,是浏览器实现异步行为的核心机制
本文将从“为什么需要异步”出发,深入理解事件循环的原理、运行过程和应用实例。异步代码复杂,直接使用同步的设计不行吗,我们看看代码
<div class='container'></div>
<script>
let name = 'joe'
setInterval(() => {
name = Math.random()
document.querySelector('.container').innerHTML = name
}, 2000)
document.querySelector('.container').innerHTML = name
</script>
这样就会先执行赋值,然后到了setInterval,就会等到2秒钟,才会设置页面的文字,所以2秒钟页面时空白的,这样就会造成用户极差的体验。
这个时候主线程会处于阻塞状态,页面无法响应用户输入,不能执行渲染更新,所以我们需要异步执行,会直接执行document.querySelector('.container').innterHtml = name,等到2秒之后再执行setInterval里面的东西。
那异步究竟是怎么实现的?那就是要说到我们的重点:事件循环,简单的来说,所有的代码执行都会被包装成一个一个的事件,然后放进去一个队列,通过不断的遍历队列来执行事件从而达到事件循环这个实现
事件循环是怎么样运行
先说一些前置知识,浏览器是一个多进程多线程的程序,每个浏览器页面都是一个进程,然后整个大概分为
- 浏览器进程:负责浏览器的那些固定的功能,例如前进后退、保存页面、右键呼出
- 网络进程:负责网络通信这些
- 渲染进程:这里就是主要为了渲染页面执行页面的东西,这里有一个渲染主线程
渲染主线程干了一个什么事:解析html,css,计算样式、布局、处理涂成、绘制页面,执行js代码,执行回调函数等等这些都是它干的,为了不阻塞页面,所以就需要异步,为了异步就用到了事件循环,具体怎么执行呢,如图所示:
- 浏览器会把各种的要执行的任务分类,就是消息队列是有多个类型的(不止图里面的3种类型),每个类型的队列里面的都必须是相同类型的任务
- 然后注意:渲染主线程的任务没有优先级的,就是先进先出,但是任务的队列是有优先级的,规范确立:
-
- 微任务队列为最高优先级,其他任务队列会根据不同浏览器的规则而优先级不同
- chrom的Blink内核把用户交互队列的优先级排在延时队列前面。
- 渲染主线程会根据任务队列的优先级依次获取任务
-
- 先执行整体代码
- 然后清空微任务
- 然后进入下一轮循环,获取优先级最高的非空任务队列中取出下一个任务执行
- 然后再次清空微任务
- 重复c、d的步骤直到所有任务队列为空,主线程进入休眠状态
- 那我们再想想既然渲染主线程是从消息队列拿到任务去执行,那任务的来源是哪里:
-
- 主线程负责注册异步任务
- 异步任务封装成一个合适主线程使用的任务是依赖其他线程的(定时器线程、网络线程)
- 任务准备好后,通过事件循环将任务加入到渲染主线程
setTimeout(() => {
console.log('1111')
})
console.log('222')
像上面的代码,执行结果是:'222' '111'
- 我们看到渲染主线程执行在timer.js的代码
- 然后执行到定时器的时候,通知了定时器线程,就继续往下执行222了
- 定时器线程就会去计算时间,到时间了移交任务队列,然后通知主线程
- 主线程再执行打印出111
所以主线程也并非生成所有的消息队列的任务,而是委托部分耗时的异步任务分给其他专门的线程,别的线程处理好了,放到消息队列了,我们主线程就负责拿走执行
实战一下
就是那么简单,既然看懂了,那么就来实战一下到底行不行
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.log('microtask');
});
console.log('end');
答案:
- start
- end
- microtask
- timeout
解析:
- 整个所有代码就是一个初始任务,开始执行
- 打印start
- 遇到setTimeout,委托给其他线程,其他线程会把setTimeout放到延时任务队列
- 继续执行代码,碰到.then放到微任务队列
- 打印end
- 清空微任务队列,打印microtask
- 清空延时任务队列,打印timeout
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
Promise.resolve().then(() => {
console.log(3);
}).then(() => {
console.log(4);
});
console.log(5);
答案:
- 1
- 5
- 3
- 4
- 2
解析:
- 整个所有代码就是一个初始任务,开始执行
- 打印1
- 遇到setTimeout,委托给其他线程,其他线程会把setTimeout放到延时任务队列
- 继续执行代码,碰到.then放到微任务队列
- 打印5
- 清空微任务队列,打印第一个.then的3,同时产生一个新的微任务放到微任务队列
- 清空微任务队列,打印第二个.then的4
- 清空延时任务队列,打印2
async function asyncFunc() {
console.log('A');
await Promise.resolve();
console.log('B');
}
console.log('C');
asyncFunc();
console.log('D');
答案:
- C
- A
- D
- B
解析:
- 整个所有代码就是一个初始任务,开始执行
- 打印C
- 执行asyncFunc方法
-
- 打印A
- 执行Promise.resolve();
- 把.then放到微任务队列(注意async/await语法题这里
await表达式会暂停当前 async 函数的执行,并将后续代码作为微任务排入队列,就像是调用了.then()一样。
- 打印D
- 清空微任务队列,打印B
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('microtask1');
});
}, 0);
setTimeout(() => {
console.log('timeout2');
}, 0);
答案:
- timeout1
- microtask1
- timeout2
解析:
- 整个所有代码就是一个初始任务,开始执行
- 到setTimeout1,委托给其他线程,其他线程会把setTimeout放到延时任务队列
- 到setTimeout2,委托给其他线程,其他线程会把setTimeout放到延时任务队列
- 清空延时任务队列执行tiemout1回调:
-
- 打印timeout1
- 遇到.then放到微任务队列
- 立刻清空微任务队列
- 打印microtask1
- 清空延时任务队列执行tiemout2打印timeout2
要注意:每轮事件循环只执行一个宏任务接着会立即清空所有微任务,再执行下一个微任务,因此你会看到微任务“插队”在两个 setTimeout 之间。
<div id="box"></div>
<script>
document.getElementById('box').innerHTML = 'loading...';
setTimeout(() => {
while (true) {} // 死循环阻塞
}, 0);
Promise.resolve().then(() => {
document.getElementById('box').innerHTML = 'ready';
});
</script>
答案:
- loading
- ready