EventLoop的小记 -- 微任务是否造成页面阻塞

650 阅读3分钟

Why

Javascript 是一门单线程语言,也就是同一时间只能执行一个任务。在浏览器环境中存在很多异步任务,比如Promise、事件回调、setTimeout等这些异步任务。这些任务执行时间是不定的,满足一定的条件后就会调用这些异步回调函数。所以就需要一套机制来管理这些异步任务,不至于页面的阻塞。也就是Eventloop机制

简介

其实 eventloop 就是一个不断循环的读取异步任务的机制,首先他有一套既定的规则:

  1. 首先检查同步任务有没有执行完(也就是调用栈是否为空)。
  2. 如果为空的话,就去检查micro task (微任务)队列是否有任务,如果有的话就一次性把micro task (微任务)执行完为止(包括micro task (微任务)执行过程中再次生成微任务),直到micro task (微任务)队列为空再去检查宏任务 macro task (宏任务)队列。每执行完一个 macro task (宏任务)还要去检查micro task (微任务)队列是否为空,不为空的话先去把micro task 队列清空。如此反复。

任务

上面说了micro taskmacro task 就是异步任务的分类。创建它们的API也是不一样的比如:

创建micro task (微任务)的有PromiseMutationObserver

创建 macro task (宏任务) 的有:setTimeoutsetIntervalrequestAnimationFrame MessageChannel还有一些IO事件等(这里rAF有争议,但它绝对不是微任务)

任务表现差异

任务都是在调用栈中执行的,不断什么任务,单个任务是不能被中断的。

关于micro task (微任务)与macro task (宏任务) 上面说了它们的执行顺序不一样,也就是微任务的优先级高。但是还有一个最重要的不同任务表现区别,也就是这篇文章最想要记录的 — 微任务会阻塞页面的渲染,给用户造成卡顿的感觉EventLoop类似的规则如下:

image.png

可以看出直到把micro task (微任务)队列情况才去执行渲染,有关视频可以去事件循环的进一步探索 - Erin Zimmer - JSConf EU 2018

我也做了一个demo验证上面的规则,确实如此:Demo跳转,这个demo其实就是在15秒之内一直执行、创建微任务or宏任务。在这期间去点击页面交互、看页面是否及时渲染。结果发现只有 promise 才会造成页面的卡顿,其他都不会(基于这个原因我更倾向把rAF规律为宏任务)。此外setTimeout还有最小4ms的限制(第一次执行除外)。也就容易理解React 调度器scheduler 为什么用MessageChanel来实现还有Vue的nextTick 因此MessageChannel 是一对一通信,没有像setTimeout受到4ms限制,还有rAF受到浏览器渲染频率限制(16.6ms)。

image.png

部分代码如下

const button2 = document.querySelector('.btn2');
button2.addEventListener('click', () => {
    // 可以交互, 不会造成页面卡顿
    let pre = performance.now();
    let count = 0;
    const fn = () => {
      count++;
      if (count % 250 === 0) {
        console.log('count', count);
      }
      if (performance.now() - pre < 1000 * 15) {
        setTimeout(() => {
          fn();
        }, 0);
      }
    };
    fn();
});

不同宿主环境

JavaScript 有不同的运行时环境,比如浏览器、Node、Web Worker 等各种环境也为JS 提供创建异步任务的API。上面说的是浏览器环境,比如Node环境还有 setImmediate process.nextTick 等,不同环境Eventloop也有差别。这里不做重点介绍。

Reference

  1. 规范文档