前言

​ 这个礼拜是我秋招的第一场面试,面的是图森未来,十分感谢图森未来给的我面试机会,也感谢面试官对我的一些指导,真的非常感谢(猛男落泪~),面完过后非常后悔最后没有问面试官要微信。感谢面试的小哥哥,人真的非常好,面试体验真的非常好,面试官全程把主动权放在我这儿,没有机械式的一问一答,当我说到一个点的时候,会延伸到另一个知识点,小哥哥就顺势得问下去,全程还让我不要紧张,最后还指导我面试技巧,真的是面试体验非常好。

​ 回归正题,在面试的时候,面试官问到一个 你知道$nextTick的原理吗 说实话,之前看到过这个,但是当时没有深究,只记得vue的异步更新策略,具体是通过js的事件循环机制来实现的,(然后小哥哥就让我讲一下事件循环机制)参考文章

正文

在了解原理前,先搞懂下面这道面试题:

setTimeout(() => {
  console.log('真的在300ms后打印吗?')
}, 300)

上面这段代码答案是什么呢?我们先不急着说出答案,先了解一些简单的概念

什么是进程: 进程是CPU分配资源的最小单位(是能拥有资源和独立运行的最小单位)

什么是线程: 线程是CPU调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

比如:

浏览器是多进程的: 在浏览器中,每打开一个tab页面,其实就是新开了一个进程,在这个进程中,还有ui渲染线程,js引擎线程,http请求线程,所以浏览器是一个多进程

js是单线程: js是作为浏览器的脚本语言,主要是实现用户和浏览器的交互,以及操作DOM;这决定了它只能是单线程的,否则会带来很多复杂的同步问题。举个例子:如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,此时浏览器就会一脸茫然,不知所措。所以,为了避免复杂性,从一诞生,JavaScript就是单线程。

紧接着上面的话题,js是单线程的,那处理一些异步方法怎么办?事件循环机制就派上用场了。

事件循环机制—Event Loop

Event Loop 分为三个部分:调用栈(call stack)、消息队列(Message Queue)、微任务队列(Microtask Queue)

Event Loop 开始时,会从全局栈开始,一行一行执行,遇到函数调用会函数压入调用栈内,被压入的函数叫做帧(Frame),函数返回后,会从调用栈弹出。

function func1(){
    console.log(1)
}
function func2(){
    console.log(2)
    func1()
    console.log(3)
}
func2()
//输出2 1 3

js中的异步操作:fecthsetIntervalsetTimeout中的回调函数,会入队到消息队列中成为消息。当调用栈清空的时候,会将消息队列的消息压入调用栈中,执行并弹出。注:下面的代码setTimeout 延时虽然为0,其实还是异步。着是因为HTML5标准规定这个函数的第二个参数不得小于4毫秒,不足会自动增加。

function func1(){
    console.log(1)
}
function func2(){
    setTimeout(()=>{
        console.log(2)
    },0)
    func1()
    console.log(3)
}
func2()
//输出1 3 2

js中的 promiseasyncawait创建的异步操作,会加入到微任务队列中,在调用栈被清空的时候立即执行,并且处理期间新加入的微任务也会一并执行。

var a = new Promise(resolve=>{
    console.log(4)
    resolve(5)
})
function func1(){
    console.log(1)
}
function func2(){
    setTimeout(()=>{
        console.log(2)
    },0)
    func1()
    consoe.log(3)
    p.then(res=>{
        console.log(res)
    }).then(()=>{
        console.log(6)
    })
}
func2()
// 4 1 3 5 6 2

异步更新策略及 nextTick原理

Vue中的nextTick是什么?原理和作用是什么?看一看官方文档的介绍

在下次DOM更新循环结束之后执行的延迟回调。在修改数据之后立即使用该方法,获取更新后的DOM。

也可以理解为: 当页面中的数据发生改变,就会把该任务放到一个异步队列中,中有在当前任务空闲时才会进行DOM渲染,当DOM渲染完成后,该函数就会自动执行。

结合上面讲的微任务理解,microtask 在这次循环中是一直取一直取,直到清空microtask队列,而microtask则是一次循环取一次,一次就是一次tick,因此当触发数据的setter,vue在microtask 建立一个cb事件,在循环到下一次tick的时候会取自动执行这个事件。

结合源码我们再去品一下:当触发某个数据的setter方法后,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有Watch对象。触发Watch对象的update实现。我们来看一下update具体是如何实现的。(这里的Dep、Watcher就是Vue响应式的基础了,后面有章节会讲到,这里只需要理解state变化更新的时候,调用update函数更新)

/*调度者接口,当依赖发生改变的时候进行回调 */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      /*同步则执行run直接渲染视图*/
      this.run()
    } else {
      /*异步推送到观察者队列中,下一个tick时调用。*/
      queueWatcher(this)
    }
  }

从代码中可以看到,当state变化的时候会调用queueWatcher(this)函数,这也是vue异步更新队列的方式。那么我们跟着去看看queueWatcher做了什么

/*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
export function queueWatcher (watcher: Watcher) {
  /*获取watcher的id*/
  const id = watcher.id
  /*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

从queueWatcher代码中看出Watch对象并不是立即更新视图,而是被push进了一个队列queue,此时状态处于waiting的状态,这时候会继续会有Watch对象被push进这个队列queue,等到下一个tick运行时将这个队列queue全部拿出来run一遍,这些Watch对象才会被遍历取出,更新视图。同时,id重复的Watcher不会被多次加入到queue中去。这也解释了同一个watcher被多次触发,只会被推入到队列中一次。

image-20200719135203739

从图中和之前讲的Event loop来总结一下:vue为了避免频繁的操作DOM,采用异步的方式更新DOM。这些异步操作会通过nextTick函数将这些操作以cb的形式放到任务队列中(以微任务优先),当每次tick结束之后就会去执行这些cb,更新DOM。

总结

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

最后更新: 2020年09月21日 15:48

原始链接: http://www.kweku.top/2020/07/19/10.nextTick/