深入理解JS事件循环

214 阅读4分钟

JS 事件循环

抛出问题

  • JS是单线程,如何做到不阻塞(异步)?
  • JS中的 macrotask 和 microtask 是什么,有什么区别?
  • 事件循环过程是怎样的?

单线程 异步

单线程是指JS引擎中负责解析JS代码的线程只有一个(主线程),即每次只能做一件事,而我们知道一个Ajax请求,主线程在等待它请求过程会去做其他事,浏览器在事件表中注册Ajax的回调函数,相应返回后回调函数被添加到任务队列中等待执行,不会造成线程阻塞,所以说JS处理Ajax的方式是异步

调用栈 任务队列

为了更好的理解调用栈和任务队列,我也引用一张图加以说明

alt 属性文本

调用栈就是一个栈结构,栈中包含了当前执行函数的执行上下文信息,函数执行完毕,它的执行上下文会从栈中弹出

引用一张网上的图来说调用栈的执行过程

alt 属性文本

1,执行函数a()先入栈
2,a()中执行函数b(),函数b()入栈
3,执行函数b(),console.log('b')入栈 4,输出b,console.log('b')出栈
5,函数b()执行完成,出栈
6,console.log('a')入栈,执行,输出a,出栈
7,函数a执行完成,出栈

事件循环

从规范理解,浏览器至少有一个事件循环,一个事件循环至少有一个任务队列(macrotask),每个任务都有自己的分组,浏览器会为不同的任务设置优先级

macrotask & microtask

macrotask: 代表一个个离散的、独立的工作单元, 浏览器完成一个宏任务,在下一个宏任务执行开始前,会对页面进行重新渲染

具体的macrotask有下面一些:

  • 整体的js代码
  • 事件回调
  • XHR回调
  • 定时器(setTimeout/setInterval)
  • IO 操作
  • UI 渲染

microtask: 微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务, 如果存在微任务,浏览器会清空微任务之后再重新渲染

具体的microtask有下面一些:

  • Promise.then()的回调
  • MutationObserver
  • DOM变化

事件处理过程

关于macrotask和microtask的处理过程,下面这种图介绍的非常清晰

alt 属性文本

一次事件循环的步骤包括:

  1. 检查macrotask队列是否为空,非空则到2,为空则到3
  2. 执行macrotask中的一个任务
  3. 继续检查microtask队列是否为空,若有则到4,否则到5
  4. 取出microtask中的任务执行,执行完成返回到步骤3
  5. 执行视图更新

上面都是一些理论,下面通过代码感受一下

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');

alt 属性文本

执行过程:

  1. 全局代码(script)入栈,打印script start
  2. setTimeout callback压入macrotask队列
  3. 第一个Promise.then压入microtask任务队列
  4. 继续执行全局代码的最后一行,打印script end
  5. 全局代码(macrotask)执行完
  6. 执行microtask队列,执行Promise.then的第一回调,打印promise1
  7. 第一个Promise.then的回调返回undefined,Promise状态变为fullfill,触发下一个then回调
  8. Promise的第二个then压入microtask队列
  9. 由于microtask队列没有清空,继续执行microtask,打印promise2
  10. microtask队列为空,如果有UI渲染,会去执行UI渲染
  11. 开启下一轮的事件循环,执行下一个macrotask任务,也就是setTimeout的回调,打印setTimeout

上述过程不断重复就是所谓的事件循环

UI渲染的时机

回顾上面的事件循环示意图,UI渲染发生在本轮事件循环的microtask队列执行完之后,也就是说任务的执行绘影响视图的渲染时机,通常浏览器以每秒60帧的速率刷新页面,大概16.7ms渲染一帧,所以如果要让用户觉得顺畅,单个macrotask任务及其相关的microtask任务最好控制在16.7ms内完成

总结

  1. 事件循环是JS实现异步的核心
  2. 每轮事件循环分为3个步骤:
    a) 执行macrotask任务队列的一个任务
    b) 执行完当前microtask队列的所有任务
    c) UI 渲染