vue 中 nextTick 源码和原理

109 阅读3分钟

vue 中 nextTick 源码和原理

1.示例

<div class="app">
  <div ref="Div">{{msg}}</div>
  <div v-if="look1">Message got outside $nextTick: {{msg1}}</div>
  <div v-if="look1">Message got inside $nextTick: {{msg2}}</div>
  <div v-if="look1">Message got outside $nextTick: {{msg3}}</div>
  <button @click="changeMsg">
    Change the Message
  </button>
</div>

vue实例

new Vue({
  el: '.app',
  data: {
    msg: 'Hello Vue.',
    msg1: '',
    msg2: '',
    msg3: ''
  },
  methods: {
    changeMsg() {
      this.msg = "Hello world."
      this.msg1 = this.$refs.Div.innerHTML
      this.$nextTick(() => {
        this.msg2 = this.$refs.Div.innerHTML
      })
      this.msg3 = this.$refs.Div.innerHTML
    }
  }
})

2.应用场景

1.在vue的生命周期里created()钩子函数进行DOM操作的时候一定要放在Vue.nextTick()的回调函数中

3.原理

1.简单理解,Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

2.Vue 在修改数据后,视图不会立刻更新,(因为视图的更新是一个异步的过程),而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新

3.在下次DOM更新循环之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM

4.一般我是用在数据渲染完毕之后执行某些操作

this.list =xx,xx,xx
this.$nextTick(()=>{
    this.isLoading=false
})

5.nextTick是宏任务 6.数据 name 被 页面引用,name 会收集到 页面的 watcher

name 被修改时,会通知所有收集到的 watcher 进行更新(watcher.update)

this.name = 2

this.name = 3

this.name = 4

如果

name 一时间被修改三次时,按道理应该会通知三次 watcher 更新,那么页面会更新三次

但是最后只会更新一次

就是因为他们的合作

设置 nextTick 回调 + 过滤 watcher

当数据变化后,把 watcher.update 函数存放进 nextTick 的 回调数组中,并且会做过滤。

通过 watcher.id 来判断 回调数组 中是否已经存在这个 watcher 的更新函数

不存在,才 push

之后 nextTick 遍历回调数组,便会执行了更新

所以

当三次修改数据的时候,会准备 push进 回调数组 三个 watcher.update,但是只有第一次是 push 成功的,其他的会被过滤掉

所以,不管你修改多少次数据,nextTick 的回调数组中只存在唯一一个 watcher.update,从而页面只会更新一次

4.源码

1.源码的三个参数

callback:我们要执行的操作,可以放在这个函数当中,我们没执行一次$nextTick就会把回调函数放到一个异步队列当中;

pending:标识,用以判断在某个事件循环中是否为第一次加入,第一次加入的时候才触发异步执行的队列挂载

timerFunc:用来触发执行回调函数,也就是Promise.thenMutationObserversetImmediatesetTimeout的过程

var callbacks = [];   // 缓存函数的数组
var pending = false;  // 是否正在执行
var timerFunc;  // 保存着要执行的函数

2.nextTick的实现单独有一个JS文件来维护它,在src/core/util/next-tick.js nextick 方法

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
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)
  }
}

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

nextTick 源码主要分为两块:能力检测能力检测以不同方式执行回调队列

5.nextTick 实现

1.首先 nextTick 把传入的 cb 回调函数用 try-catch 包裹后放在一个匿名函数中推入callbacks数组中。 这么做是因为防止单个 cb 如果执行错误不至于让整个JS 线程挂掉。 每个 cb 都包裹是防止这些回调函数如果执行错误不会相互影响,比如前一个抛错了后一个仍然可以执行。

然后检查 pending 状态,这个跟之前介绍的 queueWatcher 中的 waiting 是一个意思。 它是一个标记位,一开始是 false 在进入macroTimerFunc、microTimerFunc方法前被置为 true。因此下次调用 nextTick 就不会进入macroTimerFunc、microTimerFunc方法。 这两个方法中会在下一个 macro/micro tick 时候 flushCallbacks 异步的去执行callbacks队列中收集的任务,而 flushCallbacks 方法在执行一开始会把 pending 置 false。 因此下一次调用 nextTick 时候又能开启新一轮的 macroTimerFunc、microTimerFunc,这样就形成了 vue 中的 event loop。

最后检查是否传入了 cb。因为 nextTick 还支持 Promise 化的调用:nextTick().then(() => {})。所以如果没有传入 cb 就直接return了一个Promise实例,并且把resolve传递给_resolve。这样后者执行的时候就跳到我们调用的时候传递进 then 的方法中。

2.同步方式: 当把data中的name修改之后,此时会触发name的 setter 中的 dep.notify 通知依赖本data的render watcher去 update,update 会把 flushSchedulerQueue 函数传递给 nextTick,render watcher在 flushSchedulerQueue 函数运行时 watcher.run 再走 diff -> patch 那一套重渲染 re-render 视图,这个过程中会重新依赖收集,这个过程是异步的;所以当我们直接修改了name之后打印,这时异步的改动还没有被 patch 到视图上,所以获取视图上的 DOM 元素还是原来的内容。