vue中nextTick的原理

598 阅读2分钟

1.定义

  • 在DOM更新完毕之后执行一个延迟的回调,在修改数据后可以立即获取最新dom内容。
  • 是vue的全局api
// 修改数据 
vm.msg = 'Hello' 
// DOM 还没有更新 
Vue.nextTick(function () { // DOM 更新了 

}) 

2.在什么场景使用

在更新data后,想获得最新的dom,则需要使用nextTick方法在里面才能访问最新dom。

3.如何使用

nextTick (cb?: Function, ctx?: Object) 支持两个参数 cb回调方法,ctx上下文。

//调用方式1
$nextTick(()=> {
    //....
}) 
//调用方式2
Vue.nextTick(()=> {
    //....
})

4.异步更新队列的实现原理

4.1更新流程图

image.png

4.2更新原理

  • vue更新dom是异步进行的。只要监听到数据变化,Vue就会开启一个队列,并且缓冲同一事件循环中的所有数据变化。如果同一个watcher被调用多次,最终只会被推入队列一次。
  • 这种缓冲时,去除重复数据的操作,有效的减少不必要的计算与dom的实际操作。
  • 并且nextTick是插入到队列的最后面执行。所以能拿到最新的dom数据。

4.3事件循环(Event Loop)

  • 在js的运行环境中,通常伴随着很多事件的发生,比如用户点击、页面渲染、脚本执行、网络请求等等。为了协调这些事件的处理,浏览器使用事件循环机制。
  • 简要来说,事件循环会维护一个或多个任务队列(task queues),事件作为任务源往队列中加入任务。
  • 有一个持续执行的线程来处理这些任务,每执行完一个就从队列中移除它,这就是一次事件循环。

4.4macrotask宏任务

macrotask是宏任务

  • 每一次事件循环都会在宏队列里,执行一个宏任务。

4.5microtask微任务

microtask是微任务

  • 每一次事件循环都包含一个microtask队列,在循环结束后会依次执行队列中的microtask并移除,然后再开始下一次事件循环。
  • 在执行microtask的过程中后加入microtask队列的微任务,也会在下一次事件循环之前被执行。
  • macrotask总要等到microtask都执行完后才能执行,microtask有着更高的优先级。
  • microtask的这一特性,是做队列控制的最佳选择。
  • vue进行DOM更新内部也是调用nextTick来做异步队列控制。而当我们自己调用nextTick的时候,它就在更新DOM的那个microtask后追加了我们自己的回调函数,从而确保我们的代码在DOM更新后执行,同时也避免了setTimeout可能存在的多次执行问题。

4.6实现microtask方法

  1. Promise
  2. MutationObserver (2.5版本后被移除,HTML5新增的特性,因为在iOS有bug)
  3. Object.observe(废弃)
  4. nodejs中的 process.nextTick.

4.7vue的降级策略

队列控制的最佳选择是microtask,而microtask的最佳选择是Promise。如果当前环境不支持Promise,vue就不得不降级为macrotask来做队列控制了。

vue2.5的源码中,macrotask降级的方案

setTimeout执行的最小时间间隔是约4ms的样子,略微有点延迟。

在vue2.5的源码中,macrotask降级的方案依次是:

  1. setImmediate
  2. MessageChannel (对应的onmessage回调也是microtask,但也是个新API有兼容问题)
  3. setTimeout(最小时间间隔是约4ms)
  4. setImmediate是最理想的方案了(可惜的是只有IE和nodejs支持,有兼容问题) 所以最后的方案是setTimeout了.

5.源码分析

整体流程

watcher.js -> queueWatcher(this) -> nextTick(flushschedulerQueue) -> timeFunc -> flushCallbacks -> watcher.run()

src\core\observer\watcher.js

/* @flow */

import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError,
  noop
} from '../util/index'

import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'
import type { SimpleSet } from '../util/index'

let uid = 0

export default class Watcher {
  vm: Component;
  ...

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
   ...
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    ...
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    ...
  } 
  cleanupDeps () {
   ...
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
   //执行watcher的更新方法
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run() //直接运行
    } else {
      queueWatcher(this) //加入队列
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  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)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  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
    }
  }
}

src\core\observer\scheduler.js

把批量执行watcher需要更新的update方法加入到队列里。

/* @flow */

import type Watcher from './watcher'
import config from '../config'
import { callHook, activateChildComponent } from '../instance/lifecycle'

import {
  warn,
  nextTick,
  devtools,
  inBrowser,
  isIE
} from '../util/index'

export const MAX_UPDATE_COUNT = 100

const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0

/**
 * Reset the scheduler's state.
 */
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

export let currentFlushTimestamp = 0

// Async edge case fix requires storing an event listener's attach timestamp.
let getNow: () => number = Date.now

if (inBrowser && !isIE) {
  const performance = window.performance
  if (
    performance &&
    typeof performance.now === 'function' &&
    getNow() > document.createEvent('Event').timeStamp
  ) {
    getNow = () => performance.now()
  }
}

/**
 * Flush both queues and run the watchers.
 */
 //批量执行队列
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  queue.sort((a, b) => a.id - b.id)

  // 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 (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')
  }
}

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

/**
 * Queue a kept-alive component that was activated during patch.
 * The queue will be processed after the entire tree has been patched.
 */
export function queueActivatedComponent (vm: Component) {
  // setting _inactive to false here so that a render function can
  // rely on checking whether it's in an inactive tree (e.g. router-view)
  vm._inactive = false
  activatedChildren.push(vm)
}

function callActivatedHooks (queue) {
  for (let i = 0; i < queue.length; i++) {
    queue[i]._inactive = true
    activateChildComponent(queue[i], true /* true */)
  }
}

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
 //添加watcher到队列里
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(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)
    }
  }
}

  • src\core\util\next-tick.js
  • 用于使用哪种方式加入异步队列,微任务或 宏任务
/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

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

//定义更新的方法
let timerFunc
 
if (typeof Promise !== 'undefined' && isNative(Promise)) {//优先使用promise
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {//针对ios特殊处理
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  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)) {//如果是node环境使用setImmediate
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout. 最后降级处理
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

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()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

5.总结:

  1. nextTick是在DOM更新完毕之后执行一个延迟的回调,在修改数据后可以立即获取最新dom内容。是vue的全局api。
  2. vue更新dom是异步进行的,只要监听到数据变化,Vue就会开启一个队列,并且缓冲同一事件循环中的所有数据变化。如果同一个watcher被调用多次,最终只会被推入队列一次。这种缓冲时,去除重复数据的操作,有效的减少不必要的计算与dom的实际操作。并且nextTick是插入到队列的最后面执行。所以能拿到最新的dom数据。
  3. 当我们需要在修改数据后想立即最新的dom,就需要使用nextTick方法
  4. nextTick包含两个参数一个是回调方法,一个是上下文。
  5. 在 Vue 生命周期的 created() 钩子函数进行的 DOM 操作一定要放在 Vue.nextTick() 的回调函数中,因为created的时候dom还没有挂载。
  6. nextTick的实现原理,通过调用nextTick()方法,把回调函数插入到callbacks数组末端。然后统一用timeFunc异步方式调用他们,timeFunc有几种方式,优先使用promise插入的微任务队列。利用eventloop机制,在每一次循环结束时,批量清空执行所有微队列,把callbacks所有dom操作方法执行完,最后执行nextTick()方法,把回调函数插入到callbacks数组末端。然后统一用timeFunc异步方式调用他们,timeFunc有几种方式,优先使用promise插入的微任务队列。利用eventloop机制,在每一次循环结束时,批量清空执行所有微队列,把callbacks所有dom操作方法执行完,最后执行nextTick() 里面的方法。