Vue $nextTick原理

发布时间 2023-11-17 16:07:29作者: 柯基与佩奇

image

作用:vue 更新 DOM 是异步更新的,数据变化,DOM 的更新不会马上完成,nextTick 的回调是在下次 DOM 更新循环结束之后执行的延迟回调。

实现原理:nextTick 主要使用了宏任务和微任务。根据执行环境分别尝试采用
Promise:可以将函数延迟到当前函数调用栈最末端
MutationObserver:是 H5 新加的一个功能,其功能是监听 DOM 节点的变动,在所有 DOM 变动完成后,执行回调函数
setImmediate:用于中断长时间运行的操作,并在浏览器完成其他操作(如事件和显示更新)后立即运行回调函数
如果以上都不行则采用 setTimeout 把函数延迟到 DOM 更新之后再使用,原因是宏任务消耗大于微任务,优先使用微任务,最后使用消耗最大的宏任务。

vue2.5 以前的版本:nextTick 实质是产生一个回调函数加入到 task(宏任务)或者 microtask(微任务),当前栈执行完后调用该回调函数,起到了异步触发的作用

vue2.5 以后全部使用微任务实现,原因是使用宏任务会产生一些问题
由于浏览器的事件循环机制,引擎在每一个宏任务执行完毕,从队列中取下一个宏任务执行之前,会将这个宏任务下的微任务队列拿出来依次执行,因此微任务的执行时间早于宏任务。每个 task 执行完后都会触发 UI 的重新渲染,在 microTask 中完成数据更新,当前 task 结束就能拿到最新的 UI 了,如果再新建一个 task,UI 渲染就会进行两次

总结一下它的流程就是

把回调函数放入 callbacks 等待执行
将执行函数放到微任务或者宏任务中
事件循环到了微任务或者宏任务,执行函数依次执行 callbacks 中的回调

再回到开头说的 setTimeout,可以看出来 nextTick 是对 setTimeout 进行了多种兼容性的处理,宽泛的也可以理解为将回调函数放入 setTimeout 中执行;不过 nextTick 优先放入微任务执行,而 setTimeout 是宏任务,因此 nextTick 一般情况下总是先于 setTimeout 执行,可以在浏览器中尝试一下

最后验证猜想;当前宏任务执行完成后;优先执行两个微任务;最后再执行宏任务。

this.$nextTick()使用情景:

在 Vue 生命周期的 created 和 mounted 钩子函数进行的 DOM 操作一定要放在 Vue.nextTick()的回调函数中。
原因:是 created()钩子函数执行时 DOM 其实并未进行渲染。

在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的 DOM 结构的时候,这个操作应该放在 Vue.nextTick()的回调函数中。
原因:Vue 异步执行 DOM 更新,只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变,如果同一个 watcher 被多次触发,只会被推入到队列中一次。

先看下官方文档的说明:

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。

nextTick 就是将回调函数放到队列里去,保证在异步更新 DOM 的 watcher 后面,从而获取到更新后的 DOM。
结合 src/core/util/next-tick 源码再进行分析。
首先是定义执行任务队列方法

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

按照推入 callbacks 队列的顺序执行回调函数。
然后定义 timerFunc 函数,根据当前环境支持什么方法来确定调用哪个异步方法
判断的顺序是: Promise > MutationObserver > setImmediate > setTimeout
最后是定义 nextTick 方法:

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();
  }
  if (!cb && typeof Promise !== "undefined") {
    return new Promise((resolve) => {
      _resolve = resolve;
    });
  }
}

其实 nextTick 就是一个把回调函数推入任务队列的方法。
了解到这里也差不多了,再深入的话可以说 vue 中数据变化,触发 watcher,watcher 进入队列的流程,可以另一篇文章(https://juejin.cn/post/6934539800527503368)