V8是如何实现微任务的?

基于V8的UI线程架构模型,JavaScript延伸了很多新技术,其中应用最广泛的就是宏任务微任务

一、宏任务

宏任务:就是指消息队列中等待被主线程执行的任务。

每个宏任务在执行时,V8都会重新创建调用栈,随着宏任务中函数的调用,调用栈也会随之变化,最终,该宏任务执行结束时,整个调用栈又会被清空,接着执行下一个宏任务。

常见宏任务:定时器,各种事件。

二、微任务

微任务:微任务可以看做是一个需要异步执行的函数,执行时机是在主函数执行结束之后,当前宏任务结束之前

V8为什么要引入微任务???

由于主线程执行消息队列中宏任务的时间颗粒度太粗了,无法胜任一些对精度和实时性要求较高的场景,所以引入微任务在实时性和效率之间做一个有效的平衡。同时,使用微任务可以让我们改变异步编程的方式,以同步的方式编写异步代码。

常见微任务:Promise,async/await,MutationObserver

微任务相关知识:

微任务知识栈

微任务是基于消息队列,事件循环,UI线程架构,堆栈的,同时基于微任务,又可以延伸出promise,generaor,协程,async/await。

三、主线程-调用栈

调用栈是一种数据结构,用来管理主线程上执行的函数的调用关系。

1
2
3
4
5
6
7
8
9
/**
* 调用栈分析
*/

function foo (cb) {
cb()
}
function bar () {}
foo(bar)

未标题-1

由于调用栈空间在内存中是连续的,所以通常会限制调用栈的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 栈溢出
*/

function foo(){
foo()
}
foo()

/**
* 解决栈溢出
*/

function foo(){
setTimeout(()=>{
foo()
},0)
}
foo()

当函数嵌套过深时,调用栈中的执行上下文会堆积在栈中,最终导致栈溢出

当然我们可以用setTimeout来解决栈溢出问题。原理就是将同步函数调用改成异步函数调用,这样foo函数就会被封装成宏任务,添加到消息队列中,主线程每次在执行消息队列中的宏任务时,都会重新创建调用栈,这样就不会存在栈溢出问题。

但是,这又存在一个问题,如果某些宏任务的执行时间过久,会影响到后面宏任务的执行。因为宏任务的执行时间是不可控的,我们不知道多要多久才能执行结束。

四、微任务解决宏任务执行时机不可控问题

微任务会在当前的宏任务快要执行结束时才执行,这样可以比较精确的控制回调函数的执行时机。

那么,V8如何实现微任务的???

V8会为每个宏任务维护一个微任务队列。当V8执行一段JS代码时,会为这段代码创建一个全局执行上下文,微任务队列就存放在该执行上下文中。当通过Promise.resolve等创建一个微任务时,该微任务就会被V8添加进微任务队列中,等这段代码快要执行结束时,V8会先检查微任务队列中是否有任务要执行,有的话,就按顺序执行微任务,最后销毁该执行上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 分析代码执行过程中消息队列,调用栈,微任务队列变化
*/

function bar(){
console.log('bar') // 2
Promise.resolve().then(str => console.log('micro-bar')) // 5
setTimeout(str => console.log('macro-bar'), 0) // 8
}
function foo() {
console.log('foo') // 1
Promise.resolve().then(str => console.log('micro-foo')) // 4
setTimeout(str => console.log('macro-foo'), 0) // 7
bar()
}
foo()
console.log('global') // 3
Promise.resolve().then(str => console.log('micro-global')) // 6
setTimeout(str => console.log('macro-global'), 0) // 9
1
2
3
4
5
6
7
8
/**
* 分析代码执行过程
*/

function foo(){
return Promise.resolve().then(foo)
}
foo()

当在微任务中循环触发新的微任务时,会导致页面成卡死状态,但并不会栈溢出。因为当前宏任务始终无法执行结束,导致后面的宏任务无法执行,同时,取出微任务,执行又添加微任务,当前微任务执行结束退出,又重复之前操作,这样页面就体现出卡死的状态,同时并不会栈溢出。

参考链接

李兵《图解 Google V8》:https://time.geekbang.org/column/intro/100048001