聊一聊 nextTick

96 阅读2分钟

vue 原型上面有一个$nextTick方法,它的主要用途是用来获取组件更新后的DOM节点。 它的实现原理是在它的内部实现了一个名为nextTick的方法。用来处理组件Dom的异步更新。

vue2.0

  1. 首先它内部创建了一个callbacks数组,可以称它为回调队列(类似于js的异步更新队列 callback Queue);
  2. 每次调用nextTick方法时,会在callbacks里面push一个方法,这个方法会执行传入的回调函数。
  3. 接下来会判断一个初始值为falsepending变量,它标志将来是否需要遍历 callbacks数组。

如果pendingtrue,就会跳过。如果pending的值为false, 则将pending设置为true,然后执行timerFunc函数,它的内部是实现是以异步的方式(微任务或者宏任务)调用 flushCallbacks方法。它主要作用是遍历执行callbacks数组的每一个方法,也就是我们调用nextTick时传入的回调方法,同时也会将callbacks清空,pending设置为false

timeFunc函数的默认方式是用Promise实现异步,如果当前环境不支持,就会做降级处理,使用MutationObserver。这两个都是微任务,如果还是不支持,就使用setImmediate,最后使用 setTimeout,这两个是宏任务。

  1. 当组件内的一个响应式数据发生变化时,会触发它收集到的依赖,也就是Wather实例(watch选项,computed选项,$watch方法,组件更新函数),但是并不会立即去执行这些依赖的run方法执行回调函数,而是调用queueWatcher方法把这些Watch实例放在queue更新队列,同时会做去重处理(watcher.id),(这个时候也有可能队列正在刷新,flushing == true),如果正在刷新队列,根据id从后往前查找第一个小于正在执行的Wather.id,并放到它的后面这个方法内部会执行 nextTick方法,它传入的回调函数(flushSchedulerQueue)就是去刷新这个队列,执行 run 方法,执行回调函数。
  2. 但是并不是每个响应式数据发生变化时都会调用nextTick。它会判断一个初始值为falsewaiting变量(它表示是否正在等待刷新这个队列); 如果waitingtrue,表示正在等在刷新着异步更新队列,不需要重新去刷新,就会跳过。如果waitingfalse,则将waiting = true,则执行nextTick,等待将来某个时候时候刷新队列。
  3. flushSchedulerQueue函数执行时(刷新队列),会将初始值为falsewaiting变量设置为true,表示是否正在刷新队列,同时也会更新id进行排序操作。
  4. 如果在响应式数据发生变化之前,手动调用了nextTick方法,它的回调函数会放在回调队列callbacks的首位 ,里面是不能获取组件更新后的DOM节点,因为响应式数据发生变化之后内部执行nextTick时,它的回调函数flushSchedulerQueue会依次放到到回调队列callbacks里,在我们手动调用的后面,所以没办法获取组件更新后的 DOM 节点。
  5. 为什么队列需要从小到大(Wather.id)排序?
  • 父组件先于子组件更新,因为父组件肯定先于子组件创建。
  • 组件自定义的watcher将先于渲染watcher执行,因为自定义watcher先于渲染watcher创建。
  • 如果组件在父组件执行wtcher期间destroyed了,它的watcher集合可以直接被跳过。
class Watch {
    run () {
        if (this.active) {
          const value = this.get()
          if (
            value !== this.value ||
            // Deep watchers and watchers on Object/Arrays should fire even
            // when the value is the same, because the value may
            // have mutated.
            isObject(value) ||
            this.deep
          ) {
            // set new value
            const oldValue = this.value
            this.value = value
            if (this.user) {
              try {
                this.cb.call(this.vm, value, oldValue)
              } catch (e) {
                handleError(e, this.vm, `callback for watcher "${this.expression}"`)
              }
            } else {
              this.cb.call(this.vm, value, oldValue)
            }
          }
        }
      },
      teardown () {
        if (this.active) {
          // remove self from vm's watcher list
          // this is a somewhat expensive operation so we skip it
          // if the vm is being destroyed.
          if (!this.vm._isBeingDestroyed) {
            remove(this.vm._watchers, this)
          }
          let i = this.deps.length
          while (i--) {
            this.deps[i].removeSub(this)
          }
          this.active = false
         }
      }
}
  Vue.prototype.$destroy = function () {
    var vm = this;
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy');
    vm._isBeingDestroyed = true;
    // remove self from parent
    var parent = vm.$parent;
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm);
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown();
    }
    var i = vm._watchers.length;
    while (i--) {
      vm._watchers[i].teardown();
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--;
    }
    // call the last hook...
    vm._isDestroyed = true;
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null);
    // fire destroyed hook
    callHook(vm, 'destroyed');
    // turn off all instance listeners.
    vm.$off();
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null;
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null;
    }
  };
}

vue3.0

3.0版本对这个方法做了不一样的处理。它没有做任何的降级处理,默认支持Promise,通过Promise的链式操作(.then),确保在组件更新以后执行$nextTick传入的回调方法,看上去更好理解一点。

如果先执行queueFlush,是链式操作,如果先执行$nextTick,则是微任务队列。

代码如下:

const resolvedPromise = /*#__PURE__*/ Promise.resolve();
let currentFlushPromise = null;

function nextTick(fn) {
    const p = currentFlushPromise || resolvedPromise;
    return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
...
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy)),
...

// 更新队列将isFlushing, 未来发生。
function queueFlush() {
    if (!isFlushing && !isFlushPending) {
        isFlushPending = true;
        currentFlushPromise = resolvedPromise.then(flushJobs);
    }
}