手把手带你彻底掌握,任务队列、事件循环、宏任务、微任务

开发 前端
在 JavaScript 运行的时候,主线程会形成一个栈,这个栈主要是解释器用来最终函数执行流的一种机制。通常这个栈被称为调用栈 Call Stack ,或者执行栈( Execution Context Stack )。

 [[412788]]

调用栈 Call Stack

正式阐述任务队列与事件循环,大概了解一下 JavaScript 是如何运行的:

在 JavaScript 运行的时候,主线程会形成一个栈,这个栈主要是解释器用来最终函数执行流的一种机制。通常这个栈被称为调用栈 Call Stack ,或者执行栈( Execution Context Stack )。

调用栈,顾名思义是具有LIFO(后进先出,Last in First Out)的结构。调用栈内存放的是代码执行期间的所有执行上下文。

  • 每调用一个函数,解释器就会把该函数的执行上下文添加到调用栈并开始执行;

  • 正在调用栈中执行的函数,如果还调用了其他函数,那么新函数也会被添加到调用栈,并立即执行;

  • 当前函数执行完毕后,解释器会将其执行上下文清除调用栈,继续执行剩余执行上下文中的剩余代码;

  • 但分配的调用栈空间被占满,会引发”堆栈溢出“的报错。

调用栈 Call Stack参考文章:

1、 juejin.cn/post/696902… [1]

2、 blog.csdn.net/ch834301/ar… [2]

1. 为何需要有任务队列与循环事件

1、JavaScript 是 单线程的 :一次只能运行一个任务。通常,这没什么大不了的,但是现在想象你正在运行一个耗时 30 秒的任务,比如请求数据、定时器、读取文件等等。在此任务中,我们等待 30 秒才能进行其他任何操作(默认情况下,JavaScript 在浏览器的主线程上运行,因此整个用户界面都停滞了),后面的语句就得一直等着前面的语句执行结束后才会开始执行 。

都到 2021 年了,没有人想停留在一个速度慢,交互反应迟钝的网站。

2、浏览器每个渲染进程都有一个主线程,并且主线程非常繁忙,既要处理 DOM,又 要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。要让这 么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务, 这个统筹调度系统就是我们今天要讲的消息队列和事件循环系统。

(不清楚浏览器渲染时候,进程线程如何运行的同学,等我下一篇文章总结一下,后期我会加入文章链接)

3、要想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制。

4、能够接收其他线程发送的消息呢,一个通用模式是使用消息队列。

同步任务和异步任务

因此, JavaScript 将所有执行任务分为了同步任务和异步任务。

其实我们每个任务都是在做两件事情,就是 发起调用 和 得到结果 。

而同步任务和异步任务最主要的差别就是,同步任务发起调用后,很快就可以得到结果,而异步任务是无法立即得到结果,比如请求接口,每个接口都会有一定的响应时间,根据网速、服务器等等因素决定,再比如定时器,它需要固定时间后才会返回结果。

因此,对于同步任务和异步任务的执行机制也不同。

同步任务的执行,其实就是跟前面那个案例一样,按照代码顺序和调用顺序,支持进入调用栈中并执行,执行结束后就移除调用栈。

而异步任务的执行,首先它依旧会进入调用栈中,然后发起调用,然后解释器会将其 响应回调任务 放入一个 任务队列 ,紧接着调用栈会将这个任务移除。当主线程清空后,即所有同步任务结束后,解释器会读取任务队列,并依次将 已完成的异步任务 加入调用栈中并执行。

这里有个重点,就是异步任务不是直接进入任务队列的,等执行到异步函数(任务)的回调函数推入到任务队列中。

img-blog.csdnimg.cn/20210629235… [3]

任务入队

这里还有一个知识点,就是关于任务入队。

任务进入任务队列,其实会利用到浏览器的其他线程。虽然说 JavaScript 是单线程语言,但是浏览器不是单线程的。而不同的线程就会对不同的事件进行处理,当对应事件可以执行的时候,对应线程就会将其放入任务队列。

  • js引擎线程:用于解释执行js代码、用户输入、网络请求等;

  • GUI渲染线程:绘制用户界面,与JS主线程互斥(因为js可以操作DOM,进而会影响到GUI的渲染结果);

  • http异步网络请求线程:处理用户的get、post等请求,等返回结果后将回调函数推入到任务队列;

  • 定时触发器线程 : setInterval 、 setTimeout 等待时间结束后,会把执行函数推入任务队列中;
  • 浏览器事件处理线程 :将 click 、 mouse 等UI交互事件发生后,将要执行的回调函数放入到事件队列中。

 

 

在这里插入图片描述

 

 

2. 任务队列与循环事件到底是个啥

1、消息(任务)队列

消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。

在任务队列中,其实还分为 宏任务队列(Task Queue)**和**微任务队列(Microtask Queue) ,对应的里面存放的就是 宏任务 和 微任务 。

首先,宏任务和微任务都是异步任务。

补充个知识点:1、常见的宏任务:script(整体代码) setTimeout setInterval I/O UI交互事件 postMessage MessageChannel setImmediate(Node.js 环境) 2、常见的微任务:Promise.then Object.observe MutaionObserver process.nextTick(Node.js 环境)

2、事件循环系统

事件循环系统就是在监听并执行消息队列中的任务

3. 任务队列与循环事件具体如何使用

事件循环 Event Loop

其实宏任务队列和微任务队列的执行,就是事件循环的一部分了,所以放在这里一起说。

事件循环的具体流程如下:

  1. 从宏任务队列中,按照 入队顺序 ,找到第一个执行的宏任务,放入调用栈,开始执行;

  2. 执行完 该宏任务 下所有同步任务后,即调用栈清空后,该宏任务被推出宏任务队列,然后微任务队列开始按照入队顺序,依次执行其中的微任务, 直至微任务队列清空为止 ;

  3. 当微任务队列清空后,一个事件循环结束;

  4. 接着从宏任务队列中,找到下一个执行的宏任务,开始第二个事件循环,直至宏任务队列清空为止。

这里有几个重点:

  • 当我们第一次执行的时候,解释器会将整体代码 script 放入宏任务队列中,因此事件循环是从第一个宏任务开始的;
  • 如果在执行微任务的过程中,产生新的微任务添加到微任务队列中,也需要一起清空;微任务队列没清空之前,是不会执行下一个宏任务的。

4. 详解宏任务(如: setTimeout() )

为了协调这些任务有条不紊地在主线程上执行,页面进程引入了消息队列和事件循环机制, 渲染进程内部会维护多个消息队列,比如(延迟执行队列和普通的消息队列)。然后主线程采用 一个 for 循环,不断地从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任 务称为宏任务。

  • 当我们第一次执行的时候,解释器会将整体代码 script 放入宏任务队列中,因此事件循环是从第一个宏任务开始的;
  • 如果在执行微任务的过程中,产生新的微任务添加到微任务队列中,也需要一起清空;微任务队列没清空之前,是不会执行下一个宏任务的。

参考文章:

1、 juejin.cn/post/696902… [5]

5. 详解微任务(如:promise、MutationObserver)

微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束 之前。

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

也就是说每个宏任务都关联了一个微任务队列。那么接下来,我们就需要分析两个重要的时 间点——微任务产生的时机和执行微任务队列的时机。我们先来看看微任务是怎么产生的?在现代浏览器里面,产生微任务有两种方式。第一种方式是使用 MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修 改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产 生 DOM 变化记录的微任务。第二种方式是使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也 会产生微任务。

好了,现在微任务队列中有了微任务了,那接下来就要看看微任务队列是何时被执行的。通常情况下,在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退 出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任 务队列,然后按照顺序执行队列中的微任务。WHATWG 把执行微任务的时间点称为检查 点。当然除了在退出全局执行上下文式这个检查点之外,还有其他的检查点,不过不是太重 要,这里就不做介绍了。如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中, V8 引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束。也就是说在执行 微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继 续执行。

Demo案例:

该示意图是在执行一个 ParseHTML 的宏任务,在执行过程中,遇到了 JavaScript 脚本, 那么就暂停解析流程,进入到 JavaScript 的执行环境。从图中可以看到,全局上下文中包 含了微任务列表。在 JavaScript 脚本的后续执行过程中,分别通过 Promise 和 removeChild 创建了两个微 任务,并被添加到微任务列表中。接着 JavaScript 执行结束,准备退出全局执行上下文, 这时候就到了检查点了,JavaScript 引擎会检查微任务列表,发现微任务列表中有微任 务,那么接下来,依次执行这两个微任务。等微任务队列清空之后,就退出全局执行上下 文。

 

 

image-20210630135706952

 

 

注意点:

微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。微任务的执行时长会影响到当前宏任务的时长。比如一个宏任务在执行过程中,产生了 100 个微任务,执行每个微任务的时间是 10 毫秒,那么执行这 100 个微任务的时间就 是 1000 毫秒,也可以说这 100 个微任务让宏任务的执行时间延长了 1000 毫秒。所以 你在写代码的时候一定要注意控制微任务的执行时长。在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都 早于宏任务执行。

参考文章:

1、 time.geekbang.org/column/arti… [6]

6. 详解async、await

async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

ES7 引入了一个新的在 JavaScript 中添加异步行为的方式并且使 promise 用起来更加简单!随着 async  await 关键字的引入,我们能够创建一个隐式的返回一个 promise  async` 函数。但是,我们该怎么做呢?

之前,我们看到不管是通过输入 new Promise(() => {}) , Promise.resolve 或 Promise.reject ,我们都可以显式的使用 Promise 对象创建 promise 。

我们现在能够创建隐式地返回一个对象的异步函数,而不是显式地使用 Promise 对象!这意味着我们不再需要写任何 Promise 对象了。

 

 

图片

 

 

尽管 async 函数隐式的返回 promise 是一个非常棒的事实,但是在使用 await 关键字的时候才能看到 async 函数的真正力量。当我们等待 await 后的值返回一个 resolved 的 promise 时,通过 await 关键字,我们可以暂停异步函数。如果我们想要得到这个 resolved 的 promise 的值,就像我们之前用 then 回调那样,我们可以为被 await 的 promise 的值赋值为变量!

具体案例请参考下面五星文章哦,

 

 

image.png

 

 

五星提醒必看文章:

1、惊艳!可视化的 js:动态图演示 Promises & Async/Await 的过程!

mp.weixin.qq.com/s\?\_\_biz=MzA… [7]

2、惊艳!可视化的 js:动态图演示 - 事件循环 Event Loop

blog.csdn.net/ch834301/ar… [8]

  • 原文地址: dev.to/lydiahallie… [9]

  • 原文作者:Lydia Hallie

一个js函数简单执行流程(简单总结):

一个js函数简单执行流程:

先执行该函数里面的同步方法,全部执行完同步任务以后, 比如:var num=10 , console.log('timeout') 这种步骤

再执行微任务的回调函数,全部执行完微任务的回调函数, 比如:Promise.resolve(5).then(res => res_2).then(res => res_2)

最后执行该函数里面的宏任务的回调函数。比如:setTimeout(() => { console.log('timeout') },0)

(前提:不同任务存在的情况下,没有就不执行)---

 

责任编辑:张燕妮 来源: 高级前端进阶
相关推荐

2020-12-29 08:21:03

JavaScript微任务宏任务

2022-06-13 10:24:47

宏任务微任务前端

2021-01-18 08:24:51

JavaScriptMicrotask微任务

2023-04-06 00:22:19

JavaScrip任务开发

2019-07-06 10:18:07

人工智能

2024-08-20 15:05:42

机器学习多任务多标签模型

2021-08-03 07:40:47

宏任务微任务React

2017-05-02 22:38:44

前端开发JS事件循环机制

2022-09-21 12:01:22

消息队列任务队列任务调度

2021-07-12 09:03:50

Python任务管理器cmd命令

2023-03-29 10:02:36

2023-01-30 09:27:57

开发自动化配置

2022-09-16 08:32:17

Reduxreact

2023-11-13 07:37:36

JS面试题线程

2022-08-11 07:32:51

Starter自动装配

2023-12-15 09:45:21

阻塞接口

2021-08-17 09:55:05

JavaScript MicrotaskPromise

2021-12-15 07:24:57

人工神经网络翻译

2024-10-23 16:02:40

JavaScriptPromiserejection

2022-11-07 18:36:03

组件RPC框架
点赞
收藏

51CTO技术栈公众号