精学JS:宏任务 & 微任务的运行机制

2023-11-17

首先分析宏任务和微任务的运行机制,并针对日常开发中遇到的各种宏任务&微任务的方法,结合一些例子来看看代码运行的顺序逻辑,把这部分知识点重新归纳和梳理。

  在日常开发中,例如 setTimeout和 promise都是经常会使用到的 JS方法。当这些方法变多了之后,再结合 JS的异步编程代码混合使用,最终的执行顺序也经常会让开发者迷惑,因此要把这些问题搞清楚,这部分还是有必要好好学习一下。

在开始前请先思考一下:

  1. 宏任务和微任务分别有哪些方法?
  2. 宏任务和微任务互相嵌套,执行顺序是什么样的?

代码执行顺序(一)

开始讲解正式内容之前,我们先看一段代码,算是开胃的前菜,如果之前对这部分知识稍有了解,一般都应该可以回答正确。

console.log("begin");
setTimeout(() => {
    console.log('setTimeout');
}, 0);
new Promise((resolve)=>{
    console.log('promise');
    resolve();
}).then(()=>{   
    console.log("then1");
}).then(()=>{
    console.log("then2");
})
console.log('end');
/* 
执行结果:  
begin
promise
end
then1
then2
setTimeout
*/

 其实这个就涉及了 JavaScript事件轮询中的宏任务和微任务,如果答对了,说明基本思路是没问题的。那么这里我就直接给出结论,宏任务和微任务的执行顺序基本是,在 EventLoop中,每一次循环称为一次 tick,主要的任务顺序如下:

  1. 执行栈选择最先进入队列的宏任务,执行其同步代码直至结束;
  2. 检查是否有微任务,如果有则执行直到微任务队列为空;
  3. 如果是在浏览器端,那么基本要渲染页面了;
  4. 开始下一轮的循环(tick),执行宏任务中的一些异步代码,例如 setTimeout等。

那么结合这个结论,以及 EventLoop的内容,来看下它们的运转流程效果图。在这里插入图片描述

Call-Stack(调用栈)也就是执行栈,它是一个栈的结构,符合先进后出的机制,每次一个循环,先执行最先入队的宏任务,然后再执行微任务。不管微任务还是宏任务,它们只要按照顺序进入了执行栈,那么执行栈就还是按照先进后出的规则,一步一步执行。

  因此根据这个原则,最先进行调用栈的宏任务,一般情况下都是最后返回执行的结果。那么从上面的代码中可以看到 setTimeout的确最后执行了打印的结果。

宏任务

  如果在浏览器的环境下,宏任务主要分为下面这几个大类:

  1. 渲染事件(比如解析 DOM、计算布局、绘制);
  2. 用户交互事件(比如鼠标点击、滚动页面、放大缩小等);
  3. setTimeoutsetInterval等;
  4. 网络请求完成、文件读写完成事件。

为了让这些任务在主线程上执行,页面进程引入了消息队列和事件循环机制,我们把这些消息队列中的任务称为宏任务。宏任务基本上满足了日常的开发需求,而对于时间精度有要求的宏任务就不太能满足了,比如渲染事件、各种 I/O、用户交互的事件等,都随时有可能被添加到消息队列中,JS代码不能准确掌控任务要添加到队列中的位置,控制不了任务在消息队列中的位置,所以很难控制开始执行任务的时间。

  为了方便理解,你可以看看下面的这段代码。

const callBack2 = ()=>{
    console.log(2);
}
const callBack = ()=>{
    console.log(1);
    setTimeout(callBack2, 0);
}
setTimeout(callBack(), 0);

在上面这段代码中,我的目的是想通过 setTimeout来设置两个回调任务,并让它们按照前后顺序来执行,中间也不要再插入其他的任务。但是实际情况我们难以控制,比如在你调用 setTimeout来设置回调任务的间隙,消息队列中就有可能被插入很多系统级的任务。如果中间被插入的任务执行时间过久的话,那么就会影响到后面任务的执行了。所以说宏任务的时间粒度比较大,执行的间隔是不能精确控制的。这就不适用于一些高实时性的需求了,比如后面要讲到的监听 DOM变化。

微任务

  在理解了宏任务之后,下面我们就可以来看看什么是微任务了。微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

        我们知道当 JavaScript 执行一段脚本的时候,V8会为其创建一个全局执行上下文,同时 V8引擎也会在内部创建一个微任务队列。这个微任务队列就是用来存放微任务的,因为在当前宏任务执行的过程中,有时候会产生多个微任务,这时候就需要使用这个微任务队列来保存这些微任务了。不过这个微任务队列是给 V8引擎内部使用的,所以你是无法通过 JavaScript直接访问的。

  那么微任务是怎么产生的呢?在现代浏览器里面,产生微任务有两种方式。

  1. 使用 MutationObserver监控某个 DOM节点,或者为这个节点添加、删除部分子节点,当 DOM节点发生变化时,就会产生 DOM变化记录的微任务。
  2. 使用 Promise,当调用 Promise.resolve()或者 Promise.reject()的时候,也会产生微任务。
     

通过 DOM节点变化产生的微任务或者使用 Promise产生的微任务都会被 JS 引擎按照顺序保存到微任务队列中。现在微任务队列中有了微任务,那么接下来就要看看微任务队列是何时被执行的。

  通常情况下,在当前宏任务中的 JavaScript快执行完成时,也就是在 JavaScript引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。

  如果在执行微任务的过程中,产生了新的微任务,一样会将该微任务添加到微任务队列中,V8引擎一直循环执行微任务队列中的任务,直到队列清空才算执行结束。也就是说在执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行,这点是需要注意的。

以上就是微任务的工作流程,从上面的分析我们可以得出如下几个结论:

  1. 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
  2. 微任务的执行时长会影响当前宏任务的时长。比如一个宏任务在执行过程中,产生了 10个微任务,执行每个微任务的时间是 10ms,那么执行这 10 个微任务的时间就是 100ms,也可以说这 10个微任务让宏任务的执行时间延长了 100ms。
  3. 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。
     

MutationObserver是用来监听 DOM变化的一套方法,而监听 DOM变化一直是前端工程师经常要做的事情之一。

  虽然监听 DOM的需求是比较频繁的,不过早期页面并没有提供对监听的支持,所以那时要观察 DOM是否变化,唯一能做的就是轮询检测。比如使用 setTimeout或者 setInterval来定时检测 DOM是否有改变。这种方式简单粗暴,但是会遇到两个问题:如果时间间隔设置过长,DOM变化响应不够及时;反过来如果时间间隔设置过短,又会浪费很多无用的工作量去检查 DOM,会让页面变得低效。

  从 DOM 4 开始,W3C推出了 MutationObserver。MutationObserver API可以用来监视 DOM的变化,包括属性的变更、节点的增加、内容的改变等。因为上面我们分析过,在两个任务之间,可能会被渲染进程插入其他的事件,从而影响到响应的实时性。这时候,微任务就可以上场了,在每次 DOM节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加进当前的微任务队列中。这样当执行到检查点的时候,V8引擎就会按照顺序执行微任务了。

综上所述,MutationObserver采用了“异步 + 微任务”的策略:

  • 通过异步操作解决了同步操作的性能问题;
  • 通过微任务解决了实时性的问题。

代码执行顺序(二)

通过上面的原理学习,请你接着看下面的代码,它的执行结果是什么样的呢?

const async1 = async ()=>{
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
const async2 = async ()=>{
    console.log("async2");
}

async1();

setTimeout(() => {
    console.log('timeout');
}, 0);
new Promise ((reslove,reject)=>{
    console.log('promise1');
    reslove();
}).then(()=>{
    console.log('promise2');
}).then(()=>{
    console.log('promise3');
})
console.log('sctipt end');

这段代码除了考察对微任务和宏任务的理解外,也顺带考察了宏任务微任务结合异步编程最后的执行逻辑,这里可以先按照自己的学习思路给出一个答案,之后再拿到浏览器端运行一下结果,对照着自己的答案看是否正确,这里我把答案放最后面了,因为怕会影响思考。

总结

 

/* 
执行结果:   
async1 start
async2
promise1
sctipt end
async1 end
promise2
promise3
timeout
*/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

精学JS:宏任务 & 微任务的运行机制 的相关文章

随机推荐

  • python爬虫学习笔记-CSS(大致了解)

    CSS中文译作 层叠样式表 或者是 级联样式表 是用于控制网页外观处理并允许将网页的表现与内容分离的一种标记性语言 CSS不需要编译 可以直接由浏览器执行 属于浏览器解释型语言 是Web网页开发技术的重要组成部分 那么接下来 继续看下 使用
  • 6种JavaScript判断数组是否包含某个值的方法

    我们在项目开发过程中 经常会要检查一个数组 无序 是否包含一个特定的值 这是一个在JavaScript中经常用到的并且非常有用的操作 下面给出几种实现方式 方式一 利用循环 这种方式是比较老的实现方案 但不可否认的是在浏览器中效率较高 fu
  • 标识符与关键字,常量和变量

    标识符 标识符是有效字符序列 是一个对象的名字 用于标识用户自己定义大的变量 符号常量 函数名 数组名 类型名等 前面学习大的例子中的整型变量num 浮点型变量fnum 字符变量ch等等 均为用户定义的标识符 命名规则 不能是关键字 只能由
  • 使用HTTPS模式建立高效爬虫IP服务器详细步骤

    嘿 各位爬虫小伙伴们 想要自己建立一个高效的爬虫IP服务器吗 今天我就来分享一个简单而强大的解决方案 使用HTTPS模式建立工具 本文将为你提供详细的操作步骤和代码示例 让你快速上手 轻松建立自己的爬虫IP服务器 1 准备工作 在开始之前
  • 漏洞复现----ThinkCMF框架任意内容包含漏洞分析复现

    0x00 简介 ThinkCMF是一款基于ThinkPHP MySQL开发的中文内容管理框架 ThinkCMF提出灵活的应用机制 框架自身提供基础的管理功能 而开发者可以根据自身的需求以应用的形式进行扩展 每个应用都能独立的完成自己的任务
  • Error - Found cycle in the ListNode

    这是我在刷力扣206题时遇到的问题 报错的原因很简单 在我反转链表的时候 先定义了一个新的头结点first 把原来的头结点head放在了新的链表的第一个 然后以此遍历原有链表 把每个链表加到新链表头结点first的后面 形成了链表的反转 但
  • 迷惑度/困惑度/混乱度(preplexity)

    语言模型构造完成后 如何确定好坏呢 目前主要有两种评价方法 实用方法 通过查看该模型在实际应用 如拼写检查 机器翻译 中的表现来评价 优点是直观 实用 缺点是缺乏针对性 不够客观 理论方法 迷惑度 困惑度 混乱度 preplexity 其基
  • 【VS安装记录】Visual Studio 2022安装教程(超详细)

    大家好 我是雷工 由于更换了电脑 很多软件需要重新安装 为了方便学习C 今天有时间安装下Visual Studio 2022 顺便记录安装过程 1 从官网下载并解压软件压缩包 然后打开文件夹 2 双击 visual studio 2022
  • 搭建微服务下统一认证授权服务,鉴权客户端大致流程(基于无状态)

    1 简介 基于无状态令牌 jwt 的认证方案 服务端无需保存用户登陆状态 基于spring security框架 oauth2协议 搭建 基于spring cloud nacos 服务调用使用RestTemplate 前置知识 jwt 也就
  • VUE 常用指令

    目录 Vue概念 同类产品 官网 特点 渐进式框架 入门案例 html 改造入门案例 html MVVM框架 基础语法 运算符 operator 方法 methods Vue解析数据 三种data值的写法 高级用法 v 命令 指令集 双向绑
  • Java中GET请求与POST请求,前端传参与后端接收实例

    此示例以代码方式展现 可直接结合controller层每个接口上方注释与其接口传递参数方式理解 前端传参直接就以apiPost工具来代替 apiPost调用后端接口几种方式 代码 controller层 package com chensi
  • 读写ini配置文件(C++)

    文章目录 1 为什么要使用ini或者其它 例如xml json 配置文件 2 ini文件基本介绍 3 ini配置文件的格式 4 C 读写ini配置文件 5 代码示例 6 配置文件的解析库 文章转载于 https blog csdn net
  • Proteus中继电器详解

    目录 一 引言 二 继电器实物 三 Proteus继电器选择 四 继电器工作原理及Proteus中继电器引脚 五 Proteus中继电器正确接法举例及仿真视频记录 一 引言 我们都知道继电器可以利用小信号控制大功率 有四两拨千斤功效 同时还
  • 一些关于TV的概念

    文章目录 TTX FVP BML MTS BTSC 一些关于TV的属于解释 TTX TTX是一种电视机上的文字广播系统 可以在电视屏幕上显示文字信息 如新闻 天气预报 股票行情 电视剧剧情介绍等 它是通过电视信号的一部分传输的 不需要额外的
  • springboot 2.0.4 关闭程序————我走过的那些坑

    首次接触springboot项目 在本地测试的时候 发现不知道怎么关闭程序 虽然后来不得不用杀死进程的方式解决 但总觉得这种方式太简单粗暴 就准备问问度娘别人都是怎么做的 结果普遍答案是 步骤 第一步 引入依赖
  • 中央和省级产业政策匹配数据(含完整stata代码)

    1 数据来源 国泰安数据库 2 时间跨度 中央产业政策 2001 2020年 和省份产业政策 2006 2020年 3 区域范围 全国 4 指标说明 具体匹配代码详见分享文件中的do文件 匹配流程 1 整理证监会2001年分类标准和证监会2
  • Python 进阶(七): Word 基本操作

    1 概述 Word 是一个十分常用的文字处理工具 通常我们都是手动来操作它 本节我们来看一下如何通过 Python 来操作 Python 提供了 python docx 库 该库就是为 Word 文档量身定制的 安装使用 pip insta
  • openwrt 格式化_如何在路由器上格式化 U 盘、硬盘

    本教程适用于梅林 padavan LEDE openwrt 等固件 以下具体方法都基于 ext4 NTFS 相关错误不做回答 使用ssh连接路由器 把U盘插到路由器上 我们需要在命令行进行以下4步操作 安装fdisk 一般梅林 Padava
  • Keil(MDK-ARM-STM32)系列教程(六)Configuration(Ⅱ)

    写在前面 本文接着上一篇文章 Configuration 进行讲述Configuration后面三项Shortcut Keys快捷键 Text Completion代码完形 Other其他的内容 Shortcut Keys快捷键 Keil软
  • 精学JS:宏任务 & 微任务的运行机制

    首先分析宏任务和微任务的运行机制 并针对日常开发中遇到的各种宏任务 微任务的方法 结合一些例子来看看代码运行的顺序逻辑 把这部分知识点重新归纳和梳理 在日常开发中 例如 setTimeout和 promise都是经常会使用到的 JS方法 当