Vue2中 this.$nextTick、Vue3中nextTick

591 阅读1分钟

背景

当我们对某项数据进行频繁更新时,会引起DOM的更新,会出现很严重的性能问题,vue中使用nextTick优化这个问题。

简单理解:每次数据变化之后不是立刻去执行DOM更新,而是把数据变化的动作缓存起来,在合适的时机只执行一次DOM更新操作,就需要设置一个合适的时间间隔。

理解nextTick的原理前需要理解两块前置知识

  • Vue响应式原理
    • Object.defineProperty
  • 浏览器事件循环机制
    • 宏任务,微任务

定义

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

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

解析:

在外层定义了三个变量,callbacks,pending,timerFunc,callbacks,其实就是队列;在nextTick的外层定义变量就形成了一个闭包,所以我们每次调用$nextTick的过程其实就是在向callbacks新增回调函数的过程。

callbacks新增回调函数后又执行了timerFunc函数,pending用来标识同一个时间只能执行一次。 timerFunc函数的定义:

export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  //判断1:是否原生支持Promise
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  //判断2:是否原生支持MutationObserver
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  //判断3:是否原生支持setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  //判断4:上面都不行,直接用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

解析:

上述出现了好几个isNative函数,这是用来判断所传参数是否在当前环境原生就支持;例如某些浏览器不支持Promise,虽然我们使用了垫片(polify),但是isNative(Promise)还是会返回false。

代码其实是做了四个判断,对当前环境进行不断的降级处理, 尝试使用原生的Promise.then、MutationObserver和setImmediate,上述三个都不支持最后使用setTimeout;

降级处理的目的都是将flushCallbacks函数放入微任务(判断1和判断2)或者宏任务(判断3和判断4),等待下一次事件循环时来执行。

MutationObserver是Html5的一个新特性,用来监听目标DOM结构是否改变,也就是代码中新建的textNode;如果改变了就执行MutationObserver构造函数中的回调函数,不过是它是在微任务中执行的。

flushCallbacks;nextTick不顾一切的要把它放入微任务或者宏任务中去执行。

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

对源码的解析

源码主要分为两部分:

  • 判断当前环境能使用的最合适的API并保存异步函数
  • 调用异步函数执行回调队列
环境判断

主要判断用哪个宏任务或微任务。 判断顺序如下

  • Promise
  • MutationObserver
  • setImmediate
  • setTimeout
export let isUsingMicroTask = false // 是否启用微任务开关
const callbacks = [] // 回调队列
let pending = false // 异步控制开关,标记是否正在执行回调函数

// 该方法负责执行队列中的全部回调
function flushCallbacks () {
  // 重置异步开关
  pending = false
  // 防止nextTick里有nextTick出现的问题
  // 所以执行之前先备份并清空回调队列
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // 执行任务队列
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let timerFunc // 用来保存调用异步任务方法
// 判断当前环境是否支持原生 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 保存一个异步任务
  const p = Promise.resolve()
  timerFunc = () => {
    // 执行回调函数
    p.then(flushCallbacks)
    // ios 中可能会出现一个回调被推入微任务队列,但是队列没有刷新的情况
    // 所以用一个空的计时器来强制刷新任务队列
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 不支持 Promise 的话,在支持MutationObserver的非 IE 环境下
  // 如 PhantomJS, iOS7, Android 4.4
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 使用setImmediate,虽然也是宏任务,但是比setTimeout更好
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 以上都不支持的情况下,使用 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

环境判断结束就会得到一个延迟回调函数timerFunc

nextTick()

主要逻辑:

  • 把传入的回调函数放进回调队列callbacks
  • 执行保存的异步任务timeFunc,就会遍历callbacks执行相应的回调函数
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 把回调函数放入回调队列
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    // 如果异步开关是开的,就关上,表示正在执行回调函数,然后执行回调函数
    pending = true
    timerFunc()
  }
  // 如果没有提供回调,并且支持 Promise,就返回一个 Promise
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

vue.nextTick的应用场景

  • created生命周期中操作DOM
  • 修改数据,获取DOM值
  • v-show/v-if由隐藏变为显示

Vue3中的nextTick的原理

nextTick接受一个函数为参数,同时会创建一个微任务,把参数fn赋值给p.then(fn),在队列的任务完成后,fn就执行了。

几个维护队列的方法,执行顺序是这样的: queueJob-->queueFlush-->flushJobs-->nextTick参数的fn

vue.nextTick(callback)、vue.$nextTick(callback)的区别

  • vue.nextTick(callback)当数据发生变化,更新后执行回调
  • vue.$nextTick(callback)当dom发生变化,更新后执行的回调