浅谈事件循环、微任务和宏任务

810 阅读7分钟

浅谈事件循环、微任务和宏任务

前言

最近在学习异步任务的时候,看到了两个新的名词:宏任务微任务。好家伙,这一下就触碰到我的知识盲区了。于是我查看了许多资料:别人写的博客啊、MDN官方文档啊(包括英文原文和中文的文章)。当然,不同的文章之间内容又有矛盾,MDN官文文档又十分官方。所以,在不断反复观看资料后,将我的理解总结到这篇文章中,如果有不正确的地方还望大家纠正!

在讲事件循环前

在谈事件循环之前,我们先要了解以下内容:

JS 是单线程的,不像 Java、C++这种语言,JS 需要等前一个任务完成后才会去执行下一个任务。而有些代码需要等待一定的时间,盲目等待会影响用户体验。所以JS 任务在理解上可分为同步任务(synchronous)和异步任务(asynchronous)

JS 代码运行的时候,实际上有以下部分在工作:

  • 执行上下文的集合
  • 执行上下文栈(函数调用栈)
  • 主线程
  • 一组可能创建用于执行 worker 的额外的线程集合
  • 任务队列
  • 微任务队列

❗️ 敲重点了,记住,后面考:任务(task)和微任务(microtask)是保持各自 独立 的队列的(MDN 中有所提及)

任务 (宏任务)VS 微任务

任务

一个任务就是指计划由标准机制来执行的任何 JavaScript(看不懂是吧,因为是MDN的原话)

你可以理解为我们写的整一个 JS 代码就是一个任务,当然还有:在控制台敲的代码、setTimeout / setInterval、事件(click事件啊等等)

❗️ 敲重点了:在当前迭代轮次中,只会执行任务队列中最早的可运行的任务,而其他可运行的任务只能放在下一轮迭代中

你可以这么理解:当你运行代码时那一刻起,你的第一个任务就开始了,当程序看到类似setTimeout / setInterval这种任务(宏任务)时,会放入任务队列(task queue)中,但这些类似setTimeout / setInterval的代码相当于 创建了一个新的任务, 它们不会再本轮事件循环迭代中执行,而是等到下一轮去执行

在以下时机,任务会被添加到任务队列:

  • 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个script元素中运行代码,我们一开始执行的代码就是一个任务)。
  • 触发了一个事件,将其回调函数添加到任务队列时。
  • 执行到一个由 setTimeout()setInterval() 创建的 timeout 或 interval,以致相应的回调函数被添加到任务队列时。

微任务

A microtask is a short function which is executed after the function or program which created it exits and only if the JavaScript execution stack is empty, but before returning control to the event loop being used by the user agent to drive the script's execution environment.

这是 MDN 原文,大家可以自行翻译(毕竟大家看完原文之后的理解都不一样)

微任务并没有大家想的这么复杂,只是光有任务(宏任务),还差了点,微任务可以更好的满足用户的体验

使用微任务的主要目的:确保任务顺序的一致性(后续会有相应的例子)

微任务和任务(宏任务)有些不同:

  • 每次当一个任务退出前且执行上下文(执行栈)为空的时候,微任务队列中的每一个微任务会依次被执行。
  • 不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。(MDN原话)

上面的第二点你可以这么理解:在一条队伍中,新来的任务想插队,不好意思,排后面去;新来的微任务想要插队,好吧来吧

微任务相当于说是比任务有高一点的权限:通过将代码安排在下一次事件循环开始 之前 运行, 而不是 必须要等到下一次开始 之后 才执行

例子:

微任务到底能干嘛?上面讲到微任务能确保任务顺序的一致性,下面我们来看看 MDN 的一个例子

customElement.prototype.getData = url => {
  if (this.cache[url]) {
    // 同步代码
    this.data = this.cache[url];
    this.dispatchEvent(new Event("load"));
  } else {
    // 异步代码(微任务)
    fetch(url).then(result => result.arrayBuffer()).then(data => {
      this.cache[url] = data;
      this.data = data;
      this.dispatchEvent(new Event("load"));
    )};
  }
};
element.addEventListener("load", () => console.log("Loaded data"));
console.log("Fetching data...");
element.getData();
console.log("Data fetched");

结果:

image-20221030131406079.png 修改一下:

customElement.prototype.getData = url => {
  if (this.cache[url]) {
    // 异步代码(微任务)
    queueMicrotask(() => {
      this.data = this.cache[url];
      this.dispatchEvent(new Event("load"));
    });
  } else {
    // 异步代码(微任务)
    fetch(url).then(result => result.arrayBuffer()).then(data => {
      this.cache[url] = data;
      this.data = data;
      this.dispatchEvent(new Event("load"));
    )};
  }
};

这样一来,无论数据有没有缓存,结果都是:

Fetching data
Data fetched
Loaded data

好了,了解完任务微任务接下来我们就说说事件循环

事件循环(event loop)

事件循环分 浏览器事件循环Node.js 事件循环,而下面我们讲的主要的 浏览器 的事件循环

代码的执行,需要靠 事件循环 来驱动,事件循环会做以下事情:

执行代码、收集事件、将任务放入队列中以便后续在合适的时候执行回调、执行处于等待中的 JavaScript 任务(宏任务)、然后是微任务、然后在开始下一次循环之前执行一些必要的渲染和绘制操作。

好了,休息一下,咱们下来看看其他文章中错误的说法:

image-20221031111434379.png

首先,我们确实有同步和异步的概念,但在 MDN 官方文档中并没有出现 同步/异步任务的概念,同步/异步任务只是我们便于理解而提出的,所以根本不存在说 ‘异步任务分宏任务和微任务’

大家如果忘记了,可以往上翻翻看看任务(宏任务)的说法,它既包括同步的:比如说script标签/整体代码(可能里面只有console.log)、控制台的代码(可能里面也只有console.log);当然也有异步的:setTimeout / setInterval

  1. 有的文章说,先执行微任务,再执行任务(宏任务)

    这样说不太准确:因为确实是我们先看到微任务(如Promise.then)的结果,而后再看到任务(宏任务)中异步代码(像setTimeout / setInterval)的结果

    但是!但是!但是!在单次的事件循环迭代中,是先执行任务(宏任务)的,没有任务,哪来的宏任务?你想想,我写 js 代码,但我不执行(相当于没有任务),那那些微任务还能有结果吗?显然是没有得到

下面先给大家看两张图(大家先看一下就好):

image-20220905182157013.png image-20221031102902079.png

接着,我们来看看大概的流程:

  1. 执行一个宏任务(一开始是整体代码)
  2. 执行宏任务中的同步代码(不断的入栈出栈)
  3. 执行中如果遇到其他 任务(可理解为宏任务(task)中的异步代码,如setTimeout / setInterval等),则放进任务队列(task queue)中;如果遇到 微任务(如 Promise.then)则放入微任务队列(microtask queue)
  4. 等宏任务中的同步代码执行完后,也就是执行栈为空时,且在当前任务(宏任务)结束之前,会将微任务队列(microtask queue)中的每一个微任务依次执行(即使中途有微任务加入)
  5. 然后在开始下一次循环之前执行一些必要的渲染和绘制操作
  6. 回到第一步,执行任务队列(task queue)中的任务

例子

接下来我们看看一些例子

例1:

console.log('hello event loop')

setTimeout(function(){
    console.log('1');
},100);

new Promise(function(resolve){          
    console.log('2');
    resolve();
}).then(function(){         
    console.log('3');
});        

console.log('4'); 

loupe_事件循环演示1 00_00_00-00_00_301.gif

例2

console.log("Hi!");

setTimeout(function timeout() {
    console.log("Click the button!");
}, 5000);

setTimeout(function timeout() {
    console.log("0s timeout!");
    Promise.resolve(1)
}, 0);


console.log("Welcome to loupe.");

这个例子更好地演示了现有任务(宏任务)再有微任务

loupe_事件循环演示2 00_00_00-00_00_30.gif

例3

$.on('button', 'click', function onClick() {
    setTimeout(function timer() {
        console.log('You clicked the button!');    
    }, 2000);
});

console.log("Hi!");

setTimeout(function timeout() {
    console.log("Click the button!");
}, 5000);

new Promise(function(resolve){          
    console.log('2');
    resolve();
}).then(function(){         
    console.log('3');
});      

console.log("Welcome to loupe.");

loupe_事件循环演示3 00_00_00-00_00_30-16672372827297.gif

最后

这篇文章总结了事件处理、任务(宏任务)和微任务,以及它们的执行流程,最后给大家演示了3个例子,希望有助于大家理解

如果大家能从本文中有所收获,别忘了点赞 收藏 关注 ~