关键点
进程线程、执行栈、Task队列、微任务宏任务、eventLoop、nextTick
什么是线程?什么是进程?
很多人都知道JS是单线程执行的,提到线程那也要带上进程
进程是指CPU在运行指令及加载和保存上下文所需的时间,在应用层面上来讲就是一个程序
在浏览器中,一个网页也就是一个进程。
进程是由若干个线程构成的,例如一个网页需要由JS线程、UI渲染线程、http请求线程等等组成,从而构建出整个网页
在构建网页的过程中,各个线程各司其职,但也有互相影响的情况。比如在运行JS线程的时候,可能会影响UI渲染,因为JS如果在UI还在渲染的时候操作dom,就会导致UI渲染受阻。
类比到JS中就是,两个变量,一个函数做加法,一个函数做减法,如果分成多线程同时执行的话就会导致结果混乱,其他语言类似java为了避免这种情况会加速锁住一种操作执行另一种,而在JS单线程中就是依次执行
什么是执行栈?
执行栈其实就是一个存储函数调用的一种栈结构,先进后出
如同图中所示,执行js代码,首先会执行一个main()即主函数,再执行我们的代码,来到console,压入执行栈,发现函数bar,压入执行栈,发现函数foo,压入执行栈,随后由栈顶依次执行抛出,执行栈情况,线程走完
其实上面的过程,本质上就是同步代码的执行方式,执行函数,找到新函数,压入栈中,再找到新函数,再压入栈中,发现没有需要引用的函数了,再从栈顶依次执行栈中的函数
但是遇到异步代码该怎么办呢?
Task队列、微任务、宏任务
当我们遇到异步操作,比如网络请求时,需要花费时间去获取数据,而JS又是单线程,总不可能把线程停在这儿,等接口返回之后再执行剩下的函数吧?一次请求1s,一个网页50次请求,那岂不是停留50s的时间等异步请求数据回来?显然是不现实的。
所以需要有一个地方,对异步操作,做一个储存,就是Task队列。碰到异步代码就将其塞到队列中,让它自己去请求数据。等执行栈中的同步代码跑完了,再去队列中取异步代码执行。
所以从本质上来讲Js中的异步还是同步行为,只是有个Task队列作为缓冲区罢了,最后还是需要一次执行。
并且在异步代码中还会因为任务源的不同分为 微任务 和 宏任务
在浏览器环境下,微任务、宏任务一般包括:
-
微任务
promise、async/await、MutationObserver等
-
宏任务
script、setTimeout、setInterval等
JS引擎在执行时,会将微任务和宏任务分别放进自己特有的队列中。
因为script标签中的代码整体可看作一个大的宏任务,执行宏任务时发现有promise这样的微任务,拿出来塞到微任务队列中,发现有setTimeout这样的宏任务,塞到宏任务队列中,等当前宏任务执行完毕,就去微任务队列依次执行微任务,微任务执行完之后,本次宏任务执行完毕,页面渲染。再依次执行宏任务队列,来到下一轮时间循环。。。
以代码为例
setTimeout(() => {
console.log(1)
new Promise(resolve => {
console.log(2)
resolve()
}).then(() => {
console.log(3)
})
})
new Promise(resolve => {
resolve()
console.log(4)
}).then(() => {
console.log(5)
})
new Promise(resolve => {
resolve()
console.log(6)
}).then(() => {
console.log(7)
})
console.log(8)
// 4 6 8 5 7 1 2 3
从上往下执行,发现setTimeout,塞到宏任务队列。执行console.log(4),发现promise.then,将then()里的代码塞到微任务队列,执行console.log(6),还是将.then()里的代码塞到微任务队列队尾,执行console.log(8)。执行栈中同步代码走完了。
再依次执行微任务队列中的两个微任务,执行console.log(5)、console.log(7),到此本次宏任务结束,页面首次渲染
随后js引擎继续执行,从宏任务队列中拿出第一个宏任务塞入执行栈中,执行同步代码console.log(1)、console.log(2),发现微任务。。。
以上步骤就是eventLoop的重点流程
vue中nextTick实现原理
与普通的web项目同理,vue也是根据事件循环,在每一次宏任务执行完及微任务队列为null的时候去更新视图,而修改vue的数据层大多数都是同步操作,所以尝尝会有能拿到数据层中最新的数据,但拿不到最新的dom的情况。
vue中提供nextTick方法,其实就是抛出个钩子,让开发能在本次eventLoop之后能拿到最新的dom元素从而进行某些操作,所以nextTick的实现原理和用setTimeout差不多。
但又因为setTimeout(() => {}, 0)并不是真正的无延时调用,而是0.04ms的简化,所以nextTick增加了两个执行效率更高的api:setImmediate、MessageChannel,当这两个api都不能用的时候才用setTimeout来兜底
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (
typeof MessageChannel !== 'undefined' &&
(isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]')
) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
问题
微任务内嵌套微任务,嵌套的微任务会在下一次事件循环的宏任务中执行吗?
不会