【Vue】探索 nextTick

本文基于 VUE 2.6.11 源码。Vue 3.0 的实现略有变化,但思路仍可借鉴2.6。文章末尾会讲一下3.0相关变化。

关于nextTick,我一直有很多疑惑:

  1. 这个函数是怎么做到在dom更新之后执行回调的?

  2. 响应式数据修改中,有一个将所有修改归并,并统一修改的现象,如何实现的?

    有响应式数据a,b
    a = '修改a';
    nextTick(打印a、b对应的dom)
    b = '修改b'// 控制台打印出的a、b对应DOM均为修改后的状态,这又是怎么做到的?
    复制代码

我们一一解答:

问题1:

这个事说起来挺有意思,和调用顺序有关,如果调用顺序恰当,确实能在dom更新前先执行回调,比如:

data() {
    return {
      content: 'before'
    }
  },
  mounted() {
    this.test()
  },
  methods: {
  	test() {
        this.nextTick(() => {
          console.log(this.$refs.box.innerHTML) // 在修改响应式数据前调用
        })
        this.content = 'after'
        this.nextTick(() => {
          console.log(this.$refs.box.innerHTML)
        })
  	}
  }
// 打印结果为:
// before
// after

复制代码

好,现在我们明白了这东西和顺序有关,那就需要某种数据结构来保存调用顺序。

咱们去看源码:github.com/vuejs/vue/b…

按照 ①—> ⑥ ,的顺序阅读注释

let pending = false // 防止timerFunc函数被重复执行
const callbacks = [] // ① 这个是保留nextTick(callback)中回调函数的数组
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是异步调用,不做控制的话timerFunc函数可能被重复调用,从这里我们也能看出,timerFunc在异步队列中等待时,callbacks数组会不断积累调用nextTick时传入的函数。
    timerFunc() // ③ 这个函数用于触发异步调用,把flushCallbacks函数丢进异步队列,代码看④
  }
  // 下面的代码忽略,和我们要关注的问题无关
  // $flow-disable-line
  // if (!cb && typeof Promise !== 'undefined') {
  //   return new Promise(resolve => {
  //     _resolve = resolve
  //   })
  // }
}

timerFunc = () => { // ④,这个函数负责把flushCallbacks推入异步调用栈,你不用管它是用setTimeout还是promise.then或MutationObserver或setImmediate
    setImmediate(flushCallbacks) // 看下面??
}

function flushCallbacks () { // ⑤ 把数组callbacks中的函数取出,依次调用
  pending = false // 下面会把callbacks复制一份并清空,因此无需防止重复调用timerFunc
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]() // ⑥ 执行callbacks中保存的函数!
  }
}
复制代码

好,现在我们知道这个 **按调用顺序保存回调函数 **是怎么实现的了。

但此时会引出一个新问题,看下面??????

修改响应式数据,是怎么和nextTick关联的?我明明没调用啊!

这个和响应式原理有关,我们去看看源码:

github.com/vuejs/vue/b…

我们都知道,vue的响应式原理是类似于发布订阅模式的,每一个响应式数据,都有对应的**’事件中心’**,其中会有一堆观察者watcher来注册,等待数据变化时的通知,那么我们模拟一下:

this.content = 'after'会触一次通知,此时watcher.update()被调用。

update函数又调用了另一个函数queueWatcher

queueWatcher执行了这一行代码 nextTick(flushSchedulerQueue)flushSchedulerQueue负责执行收集到的所有watcher。

OK,找到在哪里调用nextTick了!

源码如下:

github.com/vuejs/vue/b…

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.会在依赖变化时被调用
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)// 调用调用调用调用调用调用调用调用调用 
    }
  }
复制代码

github.com/vuejs/vue/b…

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher) // ⚠️⚠️⚠️把所有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 > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue) // 这就是调用nextTick的地方
    }
  }
}
复制代码

问题2:响应式数据修改中,有一个将所有修改归并,并统一修改的现象,如何实现的?

收集时用一个队列存储watcher,修改时清空watcher。

收集依赖于函数queueWatcher,就在上一个问题末尾处??????????????????,看带⚠️的注释。

连续执行依赖于函数提到的flushSchedulerQueue函数,我们看下源码:

github.com/vuejs/vue/b…

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  // ⚠️一次性执行所有收集到的watcher⚠️
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index] 
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}
复制代码

小结

源码分析到此结束,响应式原理的源码不在这里继续深入,再写就偏题了。

有兴趣的可在vue2.6源码中搜索以下内容,搭配相关文章,自行探索:

export function defineReactive // src/core/observer/index.js,用于构件响应式对象

// src/core/observer/index.js
set: function reactiveSetter (newVal) {    
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()   // setter 拦截赋值,触发一次通知
    }

// src/core/observer/dep.js
notify () { 
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // 调用update函数,和上文形成闭环
    }
  }
复制代码

在Vue3.0中,NextTick的实现有变化吗?

有!摘录一段源码。可以看到,使用promise.then()存储&连接各个回调函数,保证调用顺序。或者直接返回一个promise,让使用者利用 async / await 进行控制,这很原生,妙啊~

// https://github.com/vuejs/vue-next/blob/44996d1a0a2de1bc6b3abfac6b2b8b3c969d4e01/packages/runtime-core/src/scheduler.ts#L42
export function nextTick(
  this: ComponentPublicInstance | void,
  fn?: () => void
): Promise<void> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

复制代码

关于nextTick是基于宏任务或是微任务

​ 这个是和运行环境相关的。在源码中,具体的实现被封装为函数timerFunc后才使用。所以,代码层面,nextTick函数也不关心timerFunc的实现究竟是微任务还是宏任务。在 Vue 3 中,2.6的实现方式已被放弃,统一使用promise.then(),构件链状结构实现。

感兴趣的可阅读2.6.11源码:github.com/vuejs/vue/b…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享