事件循环 EventLoop
事件循环指的是 js
代码所在运行环境(浏览器、nodejs
)编译器的一种解析执行规则。事件循环不属于 js
代码本身的范畴,而是属于 js
编译器的范畴
js代码可以理解为是一个人在公司中具体做的事情, 而 事件循环 相当于是公司的一种规章制度。两者不是一个层面的概念。
事件循环又叫事件队列,两者是同一个概念
JS
是单线程的,有一个 主进程 和 调用栈,主进程就是JS的运行时进程,调用栈是解释器(比如浏览器中的 JavaScript
解释器)追踪函数执行流的一种机制
同步任务:即主线程上的任务,按照顺序由上至下依次执行,当前一个任务执行完毕后,才能执行下一个任务
异步任务:进入异步处理模块,执行完毕后会产生一个回调函数放入任务队列,当主线程上的任务执行完后,事件队列中的任务其进入主线程中执行
代码执行特点
所有代码都要经过
JS
调用栈执行代码从上到下执行,遇到错误停止执行
先执行同步代码,再执行异步代码
同步代码,当前一个任务执行完毕后,才能执行下一个任务,调用栈执行后直接出栈
异步代码,放到异步处理模块,等到合适(例如延时结束、获取到数据返回)的时候回调函数放入任务队列,调用栈中的任务执行完,再放事件队列中的任务进入
微任务执行时机要比宏任务早
微任务在
DOM
渲染前触发,宏任务在DOM
渲染后触发
异步代码
- 宏任务(macro-task)
script、setTimeout、SetInterval、setImmediate(node)、requestAnimationFrame、Ajax、fetch、I/O流、UI渲染
- 微任务(micro-task)
process.nextTick(node)、Promise、Object.observer(废弃)、MutationObserver
事件循环执行机制
进入到
script
标签,就进入第一次事件循环,执行宏任务顺序从上向下执行当前上下文
遇到同步代码,立即执行
遇到异步代码,交给对应的异步处理模块,模块处理完之后,宏任务放入宏任务队列,微任务放入微任务队列
执行完所有同步代码,函数调用栈清空,开始执行任务队列
先执行微任务代码
微任务代码执行完毕,本次队列清空
寻找下一个宏任务,重复步骤一,以此反复,直到清空所有任务
这种不断重复的执行机制,叫做事件循环
当执行任务队列时,可以认为重新开了一个空的宏任务队列和空的微任务队列,将新产生的异步任务最终放入新的任务队列,当前任务队列执行完成后,当前宏队列和微队列就清除,然后再去执行新的微任务队列,新的宏任务队列, 新开微队列、新开宏队列。。。一直循环下去,直到任务队列全部为空。
浏览器中
浏览器内核是多线程的,有很多模块,事件队列就是浏览器的一个事件处理模块 ,其中有一个很重要的 webcore
模块, 对JS中的大多异步操作进行处理
当 JS
的调用栈遇到异步操作时,会将任务交给浏览器的异步处理模块,这些模块都是一个独立的线程,例如:
DOM操作
会交给webcore
中的DOM Binding
模块处理Ajax请求
会交给webcore
中的network
模块处理setTimeout等定时器
会交给webcore中
的timer
模块处理
Node中
Node
是单线程,在处理 EventLoop
上与浏览器稍微有些不同
setImmediate
为一次EventLoop
执行完毕后调用process.nextTick
为下一个宏任务开始之前执行
易错点
Promise
本身是一个同步的代码(只是容器),只有它后面调用的then()
方法里面的回调才是微任务
new Promise(resolve=>{
console.log(1)
resolve()
}).then(_=>{
console.log(2)
})
console.log(3)
// 1 3 2
2
3
4
5
6
7
8
9
await
右边的表达式还是会立即执行,表达式之后的代码才是微任务。await
微任务可以转换成等价的promise
微任务分析
console.log(1)
async function async1() {
await async2()
console.log(2)
}
async function async2() {
console.log(3)
}
async1()
// 1 3 2
2
3
4
5
6
7
8
9
10
11
script
标签本事是一个宏任务,当页面出现多个script
标签的时候,浏览器会把script
标签作为宏任务来解析
<script lang="js">
console.log(1)
setTimeout(()=>{
console.log(2)
})
</script>
<script lang="js">
console.log(3)
</script>
<!-- 1 3 2 -->
2
3
4
5
6
7
8
9
10
11
代码执行分析
console.log(1)
setTimeout(function f() {
console.log(2)
}, 5000)
console.log(3)
2
3
4
5
6
7
首先,这段代码的执行上下文进入调用栈,通俗的说,执行上下文就是标志当前作用域的,当作用域的语句执行完就会出栈
然后,
console.log(1)
进入调用栈,因为他并不是异步语句,直接打印1
,此时栈内剩下执行上下文然后,函数进入调用栈,发现他是异步(坏人,上交国家)的,需要交给其他模块处理,于是,立刻将其出栈,并把它送到浏览器的
timer
模块。timer
模块发现送来一个setTimeout
函数,参数是5
秒,心想,又有犯人(function f(){console.log(2)}
)来了,这次是关5
秒。此时,调用栈中还是只有执行上下文然后,
console.log(3)
进入调用栈,不是异步,直接打印3
,此时栈内剩下执行上下文然后,主进程发现程序语句执行完了,而且异步的函数中也没有使用作用域内的某个变量,那就说明这个作用域没事事了,执行上下文出栈,内存清空,等待异步内容
5
秒过去了,此时,timer
模块想,时间到了,犯人(function f(){console.log(2)}
)你得出狱了,出去吧!于是,setTimeout
中的回调函数被关了五秒终于被放出来了,然后,他得回家啊,家是哪儿?JS的调用栈啊,那是函数的归宿,怎么回去呢? 来的时候是直接仍过来的,回去只有走路(事件队列)了,发现这条路的出口只有在家里没人进(调用栈没有语句进栈)的时候才开,而且路很窄,一次只能过一个人,不过运气好的是前面没有人,不用排队,然后就走,走到路口,路口还开着,运气真好, 然后就走出了那条路,站到了家门口(还没进去)然后,
f
是一个函数,那他就是一个作用域,也是其内部语句的执行上下文,所以 标志作用域的 执行上下文f
进栈。最后,
console.log(2)
进栈,不是异步的,立即执行,然后f
作用域也执行完了,出栈、清除内存,结束。