调用栈 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
其实宏任务队列和微任务队列的执行,就是事件循环的一部分了,所以放在这里一起说。
事件循环的具体流程如下:
-
从宏任务队列中,按照 入队顺序 ,找到第一个执行的宏任务,放入调用栈,开始执行;
-
执行完 该宏任务 下所有同步任务后,即调用栈清空后,该宏任务被推出宏任务队列,然后微任务队列开始按照入队顺序,依次执行其中的微任务, 直至微任务队列清空为止 ;
-
当微任务队列清空后,一个事件循环结束;
-
接着从宏任务队列中,找到下一个执行的宏任务,开始第二个事件循环,直至宏任务队列清空为止。
这里有几个重点:
- 当我们第一次执行的时候,解释器会将整体代码
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 引擎会检查微任务列表,发现微任务列表中有微任 务,那么接下来,依次执行这两个微任务。等微任务队列清空之后,就退出全局执行上下 文。
注意点:
微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。微任务的执行时长会影响到当前宏任务的时长。比如一个宏任务在执行过程中,产生了 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
的值赋值为变量!
具体案例请参考下面五星文章哦,
五星提醒必看文章:
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)
(前提:不同任务存在的情况下,没有就不执行)---