Vue你不得不知道的异步更新机制和nextTick原理

前言

异步更新是 Vue 焦点实现之一,在整体流程中充当着 watcher 更新的调剂者这一角色。大部分 watcher 更新都市经由它的处置,在适当时机让更新有序的执行。而 nextTick 作为异步更新的焦点,也是需要学习的重点。

本文你能学习到:

  • 异步更新的作用
  • nextTick原理
  • 异步更新流程

JS运行机制

在明白异步更新前,需要对JS运行机制有些领会,若是你已经知道这些知识,可以选择跳过这部分内容。

JS 执行是单线程的,它是基于事宜循环的。事宜循环大致分为以下几个步骤:

  1. 所有同步义务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个”义务行列”(task queue)。只要异步义务有了运行效果,就在”义务行列”之中放置一个事宜。
  3. 一旦”执行栈”中的所有同步义务执行完毕,系统就会读取”义务行列”,看看内里有哪些事宜。那些对应的异步义务,于是竣事守候状态,进入执行栈,最先执行。
  4. 主线程不停重复上面的第三步。

“义务行列”中的义务(task)被分为两类,分别是宏义务(macro task)和微义务(micro task)

宏义务:在一次新的事宜循环的过程中,遇到宏义务时,宏义务将被加入义务行列,但需要等到下一次事宜循环才会执行。常见的宏义务有 setTimeout、setImmediate、requestAnimationFrame

微义务:当前事宜循环的义务行列为空时,微义务行列中的义务就会被依次执行。在执行过程中,若是遇到微义务,微义务被加入到当前事宜循环的微义务行列中。简朴来说,只要有微义务就会继续执行,而不是放到下一个事宜循环才执行。常见的微义务有 MutationObserver、Promise.then

总的来说,在事宜循环中,微义务会先于宏义务执行。而在微义务执行完后会进入浏览器更新渲染阶段,以是在更新渲染前使用微义务会比宏义务快一些。

关于事宜循环和浏览器渲染可以看下 晨曦时梦见兮 大佬的文章 《深入剖析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示)》

为什么需要异步更新

既然异步更新是焦点之一,首先要知道它的作用是什么,解决了什么问题。

先来看一个很常见的场景:

created(){
    this.id = 10
    this.list = []
    this.info = {}
}

总所周知,Vue 基于数据驱动视图,数据更改会触发 setter 函数,通知 watcher 举行更新。若是像上面的情形,是不是代表需要更新3次,而且在现实开发中的更新可不止那么少。更新过程是需要经由繁杂的操作,例如模板编译、dom diff,频仍举行更新的性能固然很差。

Vue 作为一个优异的框架,固然不会那么“直男”,来若干就照单全收。Vue 内部现实是将 watcher 加入到一个 queue 数组中,最后再触发 queue 中所有 watcherrun 方式来更新。而且加入 queue 的过程中还会对 watcher 举行去重操作,由于在一个 vue 实例中 data 内界说的数据都是存储同一个 “渲染watcher”,以是以上场景中数据纵然更新了3次,最终也只会执行一次更新页面的逻辑。

为了到达这种效果,Vue 使用异步更新,守候所有数据同步修改完成后,再去执行更新逻辑。

nextTick 原理

异步更新内部是最主要的就是 nextTick 方式,它卖力将异步义务加入行列和执行异步义务。Vue 也将它露出出来提供给用户使用。在数据修改完成后,立刻获取相关DOM还没那么快更新,使用 nextTick 便可以解决这一问题。

熟悉 nextTick

官方文档对它的形貌:

MySQL 面试题 24 问

在下次 DOM 更新循环竣事之后执行延迟回调。在修改数据之后立纵然用这个方式,获取更新后的 DOM。

// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
  // DOM 更新了
})

// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提醒)
Vue.nextTick()
  .then(function () {
    // DOM 更新了
  })

nextTick 使用方式有回和谐Promise两种,以上是通过组织函数挪用的形式,更常见的是在实例挪用 this.$nextTick。它们都是同一个方式。

内部实现

Vue 源码 2.5+ 后,nextTick 的实现单独有一个 JS 文件来维护它,它的源码并不庞大,代码实现不外100行,稍微花点时间就能啃下来。源码位置在 src/core/util/next-tick.js,接下来我们来看一下它的实现,先从入口函数最先:

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 1
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 2
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  // 3
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
  1. cb 即传入的回调,它被 push 进一个 callbacks 数组,守候挪用。
  2. pending 的作用就是一个锁,防止后续的 nextTick 重复执行 timerFunctimerFunc 内部建立会一个微义务或宏义务,守候所有的 nextTick 同步执行完成后,再去执行 callbacks 内的回调。
  3. 若是没有传入回调,用户可能使用的是 Promise 形式,返回一个 Promise_resolve 被挪用时进入到 then

继续往下走看看 timerFunc 的实现:

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  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)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 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)) {
  // 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)
  }
}

上面的代码并不庞大,主要通过一些兼容判断来建立合适的 timerFunc,最优先肯定是微义务,其次再到宏义务。优先级为 promise.then > MutationObserver > setImmediate > setTimeout。(源码中的英文说明也很主要,它们能辅助我们明白设计的意义)

我们会发现无论哪种情形建立的 timerFunc,最终都市执行一个 flushCallbacks 的函数。

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]()
  }
}

flushCallbacks 里做的事情 so easy,它卖力执行 callbacks 里的回调。

好了,nextTick 的源码就那么多,现在已经知道它的实现,下面再连系异步更新流程,让我们对它更充实的明白吧。

异步更新流程

数据被改变时,触发 watcher.update

// 源码位置:src/core/observer/watcher.js
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this) // this 为当前的实例 watcher
  }
}

挪用 queueWatcher,将 watcher 加入行列

// 源码位置:src/core/observer/scheduler.js
const queue = []
let has = {}
let waiting = false
let flushing = false
let index = 0

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 1
  if (has[id] == null) {
    has[id] = true
    // 2
    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
    // 3
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}
  1. 每个 watcher 都有自己的 id,当 has 没有记录到对应的 watcher,即第一次进入逻辑,否则是重复的 watcher, 则不会进入。这一步就是实现 watcher 去重的点。
  2. watcher 加入到行列中,守候执行
  3. waiting 的作用是防止 nextTick 重复执行

flushSchedulerQueue 作为回调传入 nextTick 异步执行。

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
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
  }

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

flushSchedulerQueue 内将刚刚加入 queuewatcher 逐个 run 更新。resetSchedulerState 重置状态,守候下一轮的异步更新。

function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

要注意此时 flushSchedulerQueue 还未执行,它只是作为回调传入而已。由于用户可能也会挪用 nextTick 方式。这种情形下,callbacks 里的内容为 [“flushSchedulerQueue”, “用户的nextTick回调”],当所有同步义务执行完成,才最先执行 callbacks 内里的回调。

由此可见,最先执行的是页面更新的逻辑,其次再到用户的 nextTick 回调执行。这也是为什么我们能在 nextTick 中获取到更新后DOM的缘故原由。

总结

异步更新机制使用微义务或宏义务,基于事宜循环运行,在 Vue 中对性能起着至关主要的作用,它对重复冗余的 watcher 举行过滤。而 nextTick 凭据差别的环境,使用优先级最高的异步义务。这样做的利益是守候所有的状态同步更新完毕后,再一次性渲染页面。用户建立的 nextTick 运行页面更新之后,因此能够获取更新后的DOM。

往期 Vue 源码相关文章:

原创文章,作者:admin,如若转载,请注明出处:https://www.2lxm.com/archives/22402.html