浅学nextTick(一)

52 阅读4分钟

简述

Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。

我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新。

注意!!

  • vue 实现响应式并不是数据发生变化后 DOM 立即变化,而是按照一定策略异步执行 DOM 更新的
  • vue 在修改数据后,视图不会立刻进行更新,而是要等同一事件循环机制内所有数据变化完成后,再统一进行DOM更新
  • nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM。

事件循环机制

  • 宏任务: 包括整体代码 script,setTimeout,setInterval 、setImmediate、 I/O 操作、UI 渲染
  • 微任务: Promise.then、 MutationObserver(MutationObserver 可以观察整个 文档、DOM 树的一部分 或 具体 dom 元素,主要是观察元素的 属性、子节点、文本 的变化,并且可以在 DOM 被修改时异步执行回调。)

具体示例分析

示例

<template>
  <div>
    <div>{{count}}</div>
    <div @click="handleClick">click</div>
  </div>
</template>
export default {
  data () {
    return {
        number: 0
    };
  },
  methods: {
    handleClick () {
      for(let i = 0; i < 10000; i++) {
        this.count++;
      }
    }
  }
}

分析

  1. 当点击按钮时,count会被循环改变10000次。那么每次count+1,都会触发count的setter方法,然后修改真实DOM。 按此逻辑,这整个过程,DOM会被更新10000次,我们都知道DOM的操作是非常昂贵的,而且这样的操作完全没有必要。所以vue内部在派发更新时做了优化。
  2. 也就是,并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列 queueWatcher 里,然后在 nextTick 后执行 flushSchedulerQueue 处理。
  3. 当 count 增加 10000 次时,vue内部会先将对应的 Watcher 对象给 push 进一个队列 queue 中去,等下一个 tick 的时候再去执行。并不需要在下一个 tick 的时候执行 10000 个同样的 Watcher 对象去修改界面,而是只需要执行一个 Watcher 对象,使其将界面上的 0 变成 10000 即可。

理论知识

在 Vue中 数据变化 => DOM 变化这是异步的过程,一旦观察到数据变化,Vue就会开启一个任务队列,然后把在同一个事件循环 (Event loop) 中观察,得到数据变化的 Watcher(Vue源码中的Watcher类是用来更新Dep类收集到的依赖的)推送进这个队列。

如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。

nextTick的作用是为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用Vue.nextTick(callback),JS是单线程的,拥有事件循环机制,nextTick的实现就是利用了事件循环的宏任务和微任务。

队列数据结构

队列从一端存入数据,另一端调取数据,其原则称为“先进先出”原则。(first in first out,简称“FIFO”)

  • 入队列:进行插入操作的一端称为队尾。
  • 出队列:进行删除操作的一端称为队头。

队列的构示

图解:根据队列的先进先出原则,(a1,a2,a3,a4,a5)中,由于 a1 最先从队尾进入队列,所以可以最先从队头出队列,对于 a2 来说,只有 a1 出队之后,a2 才能出队。

列存储结构示意图

队列的高级使用模式有下列

  • 消息队列
  • 高性能队列
  • 优先队列
  • 延时队列

两种方式实现队列

  1. 顺序存储

    使用顺序存储结构表示队列时,首先申请足够大的内存空间建立一个数组,除此之外,为了满足队列从队尾存入数据元素,从队头删除数据元素,还需要定义两个指针分别作为头指针和尾指针

    例如图1

  2. 链式存储

    队列的链式存储结构表示为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表,不过它只能尾进头出而已。说白一点就是:链式存储是在链表的基础上,按照“先进先出”的原则操作数据元素。

    链式

队列基础性能学习

回头再看 $nextTick 源码

// vue/src/core/util/next-tick.js
const callbacks = []
let pending = false
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 将传入的回调保存到数组callbacks中
  callbacks.push(() => {
    cb.call(ctx)
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

我们可以发现我们使用 this.$nextTick 的时候传入的回调函数会保存在一个数组 callbacks 中,然后通过pending控制 timerFunc 函数在某个时机执行

timerFunc 函数做了什么?

// vue/src/core/util/next-tick.js
// 将 callbacks 中的全部回调函数拷贝一份,然后依次执行
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let timerFunc
// isNative 检查一个值是否是原生 function
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  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)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

我们可以发现代码中生成了 timerFunc 函数,然后把回调作为 microTaskmacroTask 参与到事件循环中来, 并且依次为promise -> MutationObserver -> setImmediate -> setTimeout这样的顺序进行降级,并且通过 flushCallbacks 方法将callbacks中的全部回调拷贝一份,然后依次执行。

/**
 * Perform no operation.
 * Stubbing args to make Flow happy without leaving useless transpiled code
 * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
 */
function noop(a, b, c) { }