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

5,528 阅读13分钟

调用栈 Call Stack

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

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

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

  • 每调用一个函数,解释器就会把该函数的执行上下文添加到调用栈并开始执行;
  • 正在调用栈中执行的函数,如果还调用了其他函数,那么新函数也会被添加到调用栈,并立即执行;
  • 当前函数执行完毕后,解释器会将其执行上下文清除调用栈,继续执行剩余执行上下文中的剩余代码;
  • 但分配的调用栈空间被占满,会引发”堆栈溢出“的报错。
调用栈 Call Stack参考文章:

1、juejin.cn/post/696902…

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

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

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

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

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

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

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

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

同步任务和异步任务

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

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

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

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

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

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

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

img-blog.csdnimg.cn/20210629235…

任务入队

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

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

  • js引擎线程:用于解释执行js代码、用户输入、网络请求等;
  • GUI渲染线程:绘制用户界面,与JS主线程互斥(因为js可以操作DOM,进而会影响到GUI的渲染结果);
  • http异步网络请求线程:处理用户的get、post等请求,等返回结果后将回调函数推入到任务队列;
  • 定时触发器线程setIntervalsetTimeout等待时间结束后,会把执行函数推入任务队列中;
  • 浏览器事件处理线程:将clickmouse等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放入宏任务队列中,因此事件循环是从第一个宏任务开始的;
  • 如果在执行微任务的过程中,产生新的微任务添加到微任务队列中,也需要一起清空;微任务队列没清空之前,是不会执行下一个宏任务的。

作者:欧怼怼 链接:juejin.cn/post/696902… 来源:掘金

image-20210630122406690

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

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

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

参考文章:

1、juejin.cn/post/696902…

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、详解async、await

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

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

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

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

图片

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

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

image.png

五星提醒必看文章:

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

mp.weixin.qq.com/s?__biz=MzA…

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

blog.csdn.net/ch834301/ar…

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

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

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

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

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

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