事件循环机制的作用
事件循环机制是js的一种执行机制,他是一种可以实现
异步编程的机制。
因为JavaScript是单线程的,单线程意味着所有任务需要排队执行。但是有一些API(比如:定时器和Ajax等)是需要等待一定的时间才能得到结果的,如果它们按顺序执行,会造成很大的效率问题。因此,JavaScript采取了异步编程
,凡是需要等待的操作
,都会交给浏览器或Node.js进行挂起,等同步代码执行完毕后再执行异步操作。这个过程就需要用到事件循环机制
来进行操作的调度。
事件循环机制的原理
事件循环机制将所有代码分为同步任务和异步任务,异步任务又分为宏任务和微异步。事件循环机制就是围绕同步任务、宏任务、微任务的执行时机展开的。
同步任务在调用栈中执行,异步任务在任务队列中执行。
调用栈
执行上下文
执行上下文是用来描述当前代码执行位置所特有的变量、函数的作用域链、this值和其他相关信息。
执行上下文的组成:
- 变量对象:它包含了当前执行上下文中定义的所有变量、函数声明以及函数形参
- 作用域链: 作用域链是一系列
变量的引用列表
,它决定了执行上下文中变量和函数的查找规则,也就是作用域查找规则。作用域链使得JavaScript引擎能够知道在哪里找到当前执行代码所需的变量。 - this: 在JavaScript中,this关键字的值取决于当前执行上下文。
执行上下文的分类:
- 全局执行上下文: 这是默认的、最外层的执行上下文,每个JavaScript程序都有且仅有一个全局执行上下文。
- 函数执行上下文: 当函数被调用时,JavaScript引擎会为该函数创建一个新的执行上下文。
- Eval函数执行上下文: 当使用 eval 函数执行代码时也会创建一个执行上下文,不过现代JavaScript开发中一般不推荐使用 eval,eval() 函数会将传入的字符串当做 JavaScript 代码进行执行,其性能和安全性不高。
调用栈的使用
JavaScript代码在主线程上执行,当主线程执行到同步任务的时候是立即执行的,同步代码的执行是通过调用栈
的方式来管理的。
调用栈管理同步代码的方式:
- 全局执行上下文入栈: 当JavaScript引擎开始执行代码时,它首先会将全局脚本作为一个初始的执行上下文压入调用栈。压入之后全局执行上下文作为栈顶元素从头开始执行同步代码。
- 函数执行上下文入栈: 当执行过程中遇到函数调用时,引擎会创建一个新的函数执行上下文并将它压入调用栈顶部。函数作为栈顶元素从头开始执行同步代码。
- 嵌套函数执行上下文入栈:如果函数执行过程中遇到函数调用,引擎也会创建一个新的函数执行上下文将他压入栈顶。然后执行该嵌套函数的同步代码。
- 执行上下文出栈:当函数执行完毕(执行到return语句或函数体末尾)时,对应函数的执行上下文就会出战,控制权返回给之前的执行上下文,从函数调用后的下一行代码继续执行。
- 执行结束:当全局执行上下文执行到最后一行并出栈后就表示该段代码的同步任务执行完成,
eg
function foo(a) {
var b = a * 2;
bar(b);
}
function bar(c) {
console.log(c * 3);
}
var x = 10;
foo(x);
// 此处调用栈的变化如下:
// 全局上下文入栈
// foo(x)调用时,foo的执行上下文入栈
// foo内部调用bar(c),bar的执行上下文入栈
// bar执行完毕,bar的执行上下文出栈
// foo执行完毕,foo的执行上下文出栈
// 继续执行全局上下文中的后续代码
执行期间遇到的异步任务都扔到任务队列中即可,继续向下执行同步任务。
任务队列
宏任务和微任务
异步任务分为宏任务和微任务:
- 常见的宏任务
整体代码(<script>)
:整个脚本代码块的执行可以视为一个宏任务。setTimeout / setlnterval
:定时器回调函数属于宏任务。setlmmediate
(Node.js环境): Node.js独有的宏任务接口,表示下一次事件循环迭代开始时执行的任务。I/O回调
:如网络请求、文件读写等异步I/O操作完成后的回调函数。UI渲染
:浏览器环境中的渲染操作,如重绘(repaint)和回流(reflow) 。MessageChannel
:使用MessageChannel通信时收回调。MutationObserver
:虽然MutationObserver的行为更像是微任务,但它实际上是在U渲染之后执行的,可以看作是宏任务的一部分(因为它依赖于UI更新)。
- 常见的微任务
Promise.then/catch/finally
:Promise对象的回调函数
。(Promise构造函数的参数中的代码是同步的,.then回调函数的内容是异步的)MutationObserver 回调
:虽然它在宏任务之后执行,但其回调属于微任务。PromiseReactionJob
:更底层的Promise处理过程。process.nextTick
(Node.js环境):Node.js环境提供的微任务接口,确保回调函数在当前宏任务结束前执行。async function和await
:async function内部的await表达式之后的代码块会在下一个微任务中执行。
任务队列的使用
JavaScript代码在主线程上执行,当主线程遇到异步任务时,将异步任务挂起,并放入任务队列中等待执行。任务队列分为宏任务队列
(setTimeout、setInterval、I/O、UI rendering)和微任务队列
(如Promise.then、MutationObserver、process.nextTick),宏任务放入宏任务队列,微任务放入微任务队列。
当主线程执行完全局的同步代码
的时候,也将所有的异步任务都分别归类至不同的任务队列。
-
当
全局的同步代码
执行完的时候,先检查微任务队列
,如果有微任务,则取出并执行,直至微任务队列清空。 -
清空微任务队列后,查看是否有必要进行渲染(浏览器环境下),有必要的话就进行渲染。
-
渲染完毕后,主线程从
宏任务队列
中取出第一个任务执行。 -
执行第一个宏任务的时候,会先执行该宏任务中的同步代码(调用栈操作),执行过程中如果是微任务,放置到微任务队列队尾(还是之前的微任务队列);如果是宏任务,放置到宏任务队列队尾(还是之前的宏任务队列),同步代码执行完之后该宏任务出栈。
-
检查微任务队列,如果有微任务,则取出并执行,直至微任务队列清空。
-
清空微任务队列后,决定是否进行渲染。
-
执行下一个宏任务。
-
之后循环执行即可,直至宏任务队列和微任务队列清空。
注意:自始至终都只有一个宏任务队列和一个微任务队列,所以如果宏任务队列中还有宏任务 那该子宏任务只能放置到宏任务队列尾部等待前面已经排队的宏任务都执行完之后才能被执行。
事件循环机制执行总结
事件循环机制的执行流程如下:
- 初始化:当JavaScript引擎启动时,首先会创建全局执行上下文并压入
调用栈
。初始化任务队列,包括微任务队列和宏任务队列 - 执行同步代码:使用调用栈执行同步代码,遇到宏任务放入宏任务队列,遇到微任务放入微任务队列。
- 执行微任务:当全局上下文中的所有同步代码执行完毕后,会首先处理所有待执行的微任务
- 渲染(浏览器环境):清空微任务队列后,浏览器会尝试进行渲染操作,更新页面视图。
- 执行宏任务:渲染之后,事件循环会从宏任务队列中取出一个宏任务来执行。 每个宏任务内部也会先执行其同步代码,同样遇到宏任务放入宏任务队列,遇到微任务放入微任务队列。
- 执行微任务:如果上一步宏任务中产生微任务,微任务队列就不为空,需要处理处理所有待执行的微任务
- 渲染
- 执行宏任务
- 循环迭代:事件循环不断在宏任务队列和微任务队列之间切换,执行任务,直至两个队列都为空。
浏览器执行的时候,所有的js代码都会被解析到<script>
标签中,而<script>
标签又是一个宏任务,所以整个事件循环机制可以看作是在解析一个全局的宏任务。