深入理解Vue的nextTick

249 阅读3分钟

前备知识

  1. Event Loop相关知识
  2. Promise有三种状态,一旦决议就不再发生改变,对已决议的Promise对象添加then回调函数,会立即将此回调放入微任务队列,等待事件循环执行(不同于Event:如果你错过了它,再去监听,是得不到结果的。)(参考
  3. JS修改数据是同步的,JS修改Dom也是是同步的。
  4. Vue中修改数据是同步的,但DOM修改是异步的。
    • 异步修改的原因:避免不必要的计算和dom操作(参考

    • 异步修改的方式:将所有的DOM操作缓存到一个队列中,再在异步函数的回调中依次执行

    • 异步修改的时机:

      1. 如果环境支持Promise:构造已决议的Promise,将队列放到then回调中,在当前循环的微任务阶段执行
      2. 不支持Promise但支持MutationObserver:构造节点,监听节点变化并将队列任务放到回调中,手动修改节点,这样监听函数也会在当前循环的微任务中执行
      3. 以上均不支持,将队列任务放入setImmediate回调,在当前循环后的第n个宏任务中执行
      4. 以上均不支持,将队列任务放入setTimeout|0回调,在当前循环后的第n个宏任务中执行
    • 异步修改的实现:修改完数据之后,将修改dom的操作放入nextTick的回调中,等待异步触发

nextTick(cb)的实现

  1. 将回调函数放入一个全局队列
  2. 如果是第一次放入,将队列注册到异步回调中,注册方式同 [前备知识->3->异步修改时机]
  3. 在当前事件循环的微任务中或当前循环后的第n个宏任务中触发回调

PS

  1. 如果在nextTick调用之后,再改变响应式数据(如下),在nextTick回调中,通过DOM访问不到更新的值,因为手动调用nextTick在前,会被先放入队列,先执行,响应式数据的修改DOM操作还未执行,此时DOM还没更新,但是数据已经更新
this.$nextTick(() => {
   console.log(this.$refs.message.innerText) // 此处打印为 old-value, 因响应式数据触发的dom操作还未执行
   console.log(this.message) // 此处打印为 new-value
})
this.message = 'new value'
  1. nextTick的回调函数执行时,只能(一定程度)保证在DOM为最新的DOM,但是页面可能还是原页面,渲染并没有完成,因为渲染工作是在微任务之后进行的
    • JS引擎线程渲染引擎线程是互斥的(虽然是两个线程,但是会互相阻塞)
    • Event Loop运行机制是一个单独的线程,用于各线程之间通信,维护了一些任务队列
    • 一次事件循环
      • 先拿一个宏任务的回调,执行同步代码(可能没有)
      • 检查微任务队列,执行,清空
      • 唤起GUI线程(可能多次循环唤起一次)
        • 执行requestAnimationFrame所有回调
        • 再次清空微任务(由GUI线程执行,队列是由Event Loop维护的)
        • 渲染页面
        • 退出GUI线程
      • 进入下一次循环