从浅到深解析vue的nextTick函数

85 阅读1分钟

基础使用

nextTick,常用来当响应式数据变化后,想要基于更新后的dom进行一些操作。

<body>
    <div id="app">
        <p id="text">{{ text }}</p>
    </div>

    <script>
        const vm = new Vue({
            el: '#app',
            data: {
                text: 'hello',
            },
            mounted() {
                this.text = 'hello world'
                this.$nextTick(() => {
                    // nextTick输出 hello world
                    console.log('nextTick输出', document.getElementById('text').innerText)
                })
                // 直接输出 hello
                console.log('直接输出', document.getElementById('text').innerText)
            }
        })
    </script>

</body>

为什么不能立即拿到dom呢,因为vue的视图更新是微任务也就是异步的。

当你修改一个响应式数据后,数据的变化并不会立即的变化在dom上,vue会开启一个异步更新队列,在下一个事件更新tick中执行视图更新。nextTick确保在dom更新后,立即执行你的代码。

源码分析

  1. 在initGloabalApI函数中,将nextTick放到vue对象上。在renderMixin函数中,将nextTick函数绑定到vue的原型上
export function initGlobalAPI(Vue: GlobalAPI) {
    Vue.nextTick = nextTick
}
export function renderMixin(Vue: typeof Component) {
  Vue.prototype.$nextTick = function (fn: (...args: any[]) => any) {
    return nextTick(fn, this)
  }
}
  1. 将回调函数cb推入到callbacks队列中,然后执行timeFunc(), 这个函数会立即开启一个异步更新队列
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e: any) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

3.通过Promise.resolve().then开启一个微任务,执行flushCallbacks函数

const p = Promise.resolve()
timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
 }

4 依次遍历执行nextTick中被推入callbacks的cb

function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
  1. 对于你自己使用的nextTick来说,这个cb是你自己的回调。而对于vue响应式数据变化来说,这个cb是flushSchedulerQueue,用来执行视图更新
/**
 * Flush both queues and run the watchers.
 */
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(sortCompareFn)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  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 (__DEV__ && 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)
  cleanupDeps()

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