事件循环

2 阅读4分钟

js是单线程导致js需要异步,事件循环(又叫消息循环)是实现异步的方式

浏览器分为多个进程,其中包含渲染进程,渲染进程有一个主线程和多个子线程,主线程需要进行HTML解析,CSS解析,JS执行,渲染页面等

JS是运行在渲染主线程中的,所以JS是单线程的,而单线程就意味着如果发起耗时请求比如IO请求,网络请求就会导致主线程被阻塞,页面渲染,HTML解析等全都无法继续执行,所以需要异步操作

JS发起请求,然后将请求给子线程让子线程处理,主线程则不等待继续往下执行,等到子线程请求处理完之后浏览器将回调包装成任务放在消息队列中排队进行

chrome源码中开启了一个不停止的for循环,每次从消息队列中拿到队头任务执行,没有就休眠

消息队列: 渲染主线程持续执行同步代码(打印,计算属性等),Promise的回调在状态改变时会被立刻放在微队列中,计时器回调则会被放在延时队列中(注意计时器,promise请求本身都是同步的 ,也就是立刻执行,但是他们的回调不是),这些队列就是消息队列,在以前被称为宏队列,里面的任务就是以前的宏任务

注意:任务本身是没有优先级的,同一队列先进先出,但是队列有优先级

w3c官方规定不同的任务可以在不同队列,同类型任务必须在同一个队列,规定一定有一个微队列,优先级为最高,即渲染主线程中的同步代码执行完之后立刻就是微队列,之后是其他队列,浏览器并没有明确指明,但是处于浏览器优先保障用户体验的原则,浏览器一般是交互队列优先级高于延时队列

*注意: await/async的机制是同步执行async中的代码直到遇见await,await会暂停当前函数的执行,并将函数中剩余部分包装成一个微任务,当await后的promise状态变为fulfilled的时候该微任务会被加入微队列

示例:

console.log('1');
​
async function fn() {
  console.log('2');
  await Promise.resolve();
  console.log('5');
  await Promise.resolve().then(() => {
    console.log('6');
  });
  console.log('7');
}
​
fn();
​
Promise.resolve().then(() => {
  console.log('4');
});
​
console.log('3');

得到1235467

注意: 事件循环的顺序是:

  1. 所有队列均为空
  2. 全局脚本就是第一个宏任务(除了微队列其他队列统称宏队列)
  3. 从宏队列取出一个任务,里面的代码作为同步代码放进渲染主线程执行,过程中产生的微任务放进微队列,产生的宏任务放进其他队列
  4. 执行结束后执行微队列中所有代码
  5. 这时候一次事件循环就快结束了,在下一次循环前会检查是否有需要 样式计算,回流,重绘,合成 的,有的话执行(注意CSSOM和DOM的改变是立刻执行的,但是布局的改变和重新绘制是事件循环末尾的渲染阶段才会进行)(注意这个渲染阶段并不是每次事件循环都执行,而是一帧中只会有一次,在帧缓冲信号快要到来的时候执行,把所有要执行的修改执行,绘制新一帧的画面写入后缓冲区,在渲染阶段开始到下一帧之间的这一小段事件中如果恰好又发生了样式的修改会积累到下一帧)
  6. 从宏队列取出下一个宏任务,重复这个过程

每个事件循环的时间往往远小于一帧,注意浏览器会节流,即通常情况下他不会立刻执行本轮触发的回流,重绘等操作而是一直计算直到帧缓冲信号快来了再生成最终画面,在帧缓冲信号发出后写入帧缓冲区,即一般情况下一帧只会回流/重绘一次,但是也有可能比如获取实际高度这样的操作会强制进行回流