浅析JS中的eventLoop

239 阅读5分钟

关键点

进程线程、执行栈、Task队列、微任务宏任务、eventLoop、nextTick

什么是线程?什么是进程?

很多人都知道JS是单线程执行的,提到线程那也要带上进程

进程是指CPU在运行指令及加载和保存上下文所需的时间,在应用层面上来讲就是一个程序

在浏览器中,一个网页也就是一个进程。

进程是由若干个线程构成的,例如一个网页需要由JS线程、UI渲染线程、http请求线程等等组成,从而构建出整个网页

在构建网页的过程中,各个线程各司其职,但也有互相影响的情况。比如在运行JS线程的时候,可能会影响UI渲染,因为JS如果在UI还在渲染的时候操作dom,就会导致UI渲染受阻。

类比到JS中就是,两个变量,一个函数做加法,一个函数做减法,如果分成多线程同时执行的话就会导致结果混乱,其他语言类似java为了避免这种情况会加速锁住一种操作执行另一种,而在JS单线程中就是依次执行

什么是执行栈?

1670d2d20ead32ec.gif

执行栈其实就是一个存储函数调用的一种栈结构,先进后出

如同图中所示,执行js代码,首先会执行一个main()即主函数,再执行我们的代码,来到console,压入执行栈,发现函数bar,压入执行栈,发现函数foo,压入执行栈,随后由栈顶依次执行抛出,执行栈情况,线程走完

其实上面的过程,本质上就是同步代码的执行方式,执行函数,找到新函数,压入栈中,再找到新函数,再压入栈中,发现没有需要引用的函数了,再从栈顶依次执行栈中的函数

但是遇到异步代码该怎么办呢?

Task队列、微任务、宏任务

当我们遇到异步操作,比如网络请求时,需要花费时间去获取数据,而JS又是单线程,总不可能把线程停在这儿,等接口返回之后再执行剩下的函数吧?一次请求1s,一个网页50次请求,那岂不是停留50s的时间等异步请求数据回来?显然是不现实的。

所以需要有一个地方,对异步操作,做一个储存,就是Task队列。碰到异步代码就将其塞到队列中,让它自己去请求数据。等执行栈中的同步代码跑完了,再去队列中取异步代码执行。

所以从本质上来讲Js中的异步还是同步行为,只是有个Task队列作为缓冲区罢了,最后还是需要一次执行。

并且在异步代码中还会因为任务源的不同分为 微任务宏任务

在浏览器环境下,微任务、宏任务一般包括:

  • 微任务

    promise、async/await、MutationObserver等

  • 宏任务

    script、setTimeout、setInterval等

JS引擎在执行时,会将微任务和宏任务分别放进自己特有的队列中。

image.png

因为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:setImmediateMessageChannel,当这两个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)
  }
}

问题

微任务内嵌套微任务,嵌套的微任务会在下一次事件循环的宏任务中执行吗?

wecom-temp-d70f249d596b2bfa3d2cf32bfe00fa8b.png

不会