一文彻底搞懂JS事件循环机制(Event-loop)之宏任务/微任务

533 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第10天,点击查看活动详情

引言

JS事件循环

首先需要明确的是js是单线程的语言,就像我们去银行办理业务,需要取号进行排号,同理js任务也要一个一个顺序执行。因此按照此理论如果一个任务耗时过长,那么后一个任务也必须等着。

那么问题来了,假如我们想看一些头条消息,但是消息包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来才能展示下面的内容么,显然这样不符合常理,影响用户体验,因此js将任务分为两类:

  • 同步任务
  • 异步任务

举例分析:

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

如果按照同步顺序执行的话答案应该是script start, setTimeout, promise1, promise2, script end

但是正确答案却是:script start, script end, promise1, promise2, setTimeout

原理图分析

看过官方文档的读者,对这张图应该已然相当熟悉了,下面便就下图进行分析

cmd-markdown-logo

解读:

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,这便是常说的Event Loop(事件循环)。

宏任务(macrotask) 和 微任务(microtask)

宏任务(macrotask)

常见的宏任务

  • 主代码块
  • setTimeout
  • setInterval
  • setImmediate ()-Node
  • requestAnimationFrame ()-浏览器

举例分析:

console.log('script start');

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

console.log('script end');

正确答案是:script start, script end, setTimeout

分析:JS运行主线程,碰到setTimeout放入宏任务队列等待执行,之后继续运行主线程,主线程执行完毕之后,去宏任务队列读取对应的函数,进入主线程执行。

微任务(microtask)

常见微任务

  • process.nextTick ()-Node
  • Promise.then()
  • catch
  • finally
  • Object.observe
  • MutationObserver 举例分析:
console.log('script start');

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

正确答案是:script start, script end, promise1, promise2

分析:JS运行主线程,碰到Promise放入微任务队列等待执行,之后继续运行主线程,主线程执行完毕之后,去微任务队列读取对应的函数,进入主线程执行。

注意点

  • setTimeout是一个宏任务,它的事件回调在宏任务队列,Promise.then()是一个微任务,它的事件回调在微任务队列,二者并不是在一个任务队列
  • 当执行到script脚本的时候,js引擎会为全局创建一个执行上下文,在该执行上下文中维护了一个微任务队列,当遇到微任务,就会把微任务回调放在微队列中,当所有的js代码执行完毕,在退出全局上下文之前引擎会去检查该队列,有回调就执行,没有就退出执行上下文,这也就是为什么微任务要早于宏任务。

练习

setTimeout(() => {
  console.log("1");
  setTimeout(() => {
    console.log("2");
  }, 500);
  new Promise(resolve => {
    resolve();
    console.log("3");
  }).then(() => {
    console.log("4");
  });
}, 500);

setTimeout(() => {
  console.log("5");
}, 500);

console.log("6");

正确答案是:6, 1, 3, 4, 5, 2

分析:

  1. 首先执行主线程,执行过程中遇到 setTimeout(宏任务),将其分配到宏任务队列中,继续执行主线程,执行过程中遇到第二个 setTimeout(宏任务),将其追加到宏任务队列中,继续执行主线程,首先输出6,主线程执行完毕。
  2. 去宏任务队列读取对应的函数,进入主线程执行,接着输出1,遇到一个 setTimeout(宏任务),将其分配到新的宏任务队列中,遇到一个 promise (微任务),但是构造函数中的代码为同步代码,接着输出3,则then 之后的任务加入到微任务队列中去,则第一个setTimeout(宏任务)执行完毕。
  3. 遵循事件循环机制,执行第一个setTimeout(宏任务)中碰到的promise (微任务),接着输出4
  4. 继续从第一个宏任务队列读取函数,接着输出5
  5. 执行第一个setTimeout(宏任务)中包含的setTimeout(宏任务),接着输出2

总结

JavaScript是一种单线程语言,所有任务都在一个线程上完成。一旦遇到大量任务或者遇到一个耗时的任务,比如加载一个高清图片,网页就会出现"假死",因为JavaScript停不下来,也就无法响应用户的行为。为了防止主线程的阻塞,JavaScript 有了 同步 和 异步 的概念。所以 JavaScript 便使用一套机制去处理同步和异步操作,那就是事件循环 (Event Loop)。

  • 所有同步任务都在主线程上依次执行,形成一个执行栈(调用栈),异步任务则放入一个任务队列
  • 当执行栈中任务执行完,再去检查微任务队列里的微任务是否为空,有就执行,如果执行微任务过程中又遇到微任务,就添加到微任务队列末尾继续执行,把微任务全部执行完
  • 微任务执行完后,再到任务队列检查宏任务是否为空,有就取出最先进入队列的宏任务压入执行栈中执行其同步代码
  • 然后回到第2步执行该宏任务中的微任务,如此反复,直到宏任务也执行完,如此循环

结语

本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力。