Vue系列---理解Vue.nextTick使用及源码分析(五)


一. 什么是Vue.nextTick()?
  • 二. Vue.nextTick()方法的应用场景有哪些? 2.1 更改数据后,进行节点DOM操作。 2.2 在created生命周期中进行DOM操作。
  • 三. Vue.nextTick的调用方式如下:
  • 四:vm.$nextTick 与 setTimeout 的区别是什么?
  • 五:理解 MutationObserver
  • 六:nextTick源码分析
  • 回到顶部回到顶部回到顶部回到顶部回到顶部回到顶部回到顶部

    六:nextTick源码分析

    vue源码在 vue/src/core/util/next-tick.js 中。源码如下:
    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)) {
      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]'
    )) {
      // 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)) {
      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
        })
      }
    }

    如上代码,我们从上往下看,首先定义变量 callbacks = []; 该变量的作用是: 用来存储所有需要执行的回调函数。let pending = false; 该变量的作用是表示状态,判断是否有正在执行的回调函数。
    也可以理解为,如果代码中 timerFunc 函数被推送到任务队列中去则不需要重复推送。

    flushCallbacks() 函数,该函数的作用是用来执行callbacks里面存储的所有回调函数。如下代码:

    function flushCallbacks () {
      /*
       设置 pending 为 false, 说明该 函数已经被推入到任务队列或主线程中。需要等待当前
       栈执行完毕后再执行。
      */
      pending = false;
      // 拷贝一个callbacks函数数组的副本
      const copies = callbacks.slice(0)
      // 把函数数组清空
      callbacks.length = 0
      // 循环该函数数组,依次执行。
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }

    timerFunc: 保存需要被执行的函数。

    继续看接下来的代码,我们上面讲解过,在Vue中使用了几种情况来延迟调用该函数。

    1. promise.then 延迟调用, 基本代码如下: 

    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      timerFunc = () => {
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
      }
      isUsingMicroTask = true
    }

    如上代码的含义是: 如果我们的设备(或叫浏览器)支持Promise, 那么我们就使用 Promise.then的方式来延迟函数的调用。Promise.then会将函数延迟到调用栈的最末端,从而会做到延迟。

    2. MutationObserver 监听, 基本代码如下:

    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
    }

    如上代码,首先也是判断我们的设备是否支持 MutationObserver 对象, 如果支持的话,我们就会创建一个MutationObserver构造函数, 并且把flushCallbacks函数当做callback的回调, 然后我们会创建一个文本节点, 之后会使用MutationObserver对象的observe来监听该文本节点, 如果文本节点的内容有任何变动的话,它就会触发 flushCallbacks 回调函数。那么要怎么样触发呢? 在该代码内有一个 timerFunc 函数, 如果我们触发该函数, 会导致文本节点的数据发生改变,进而触发MutationObserver构造函数。

    3. setImmediate 监听, 基本代码如下:

    else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      // Fallback to setImmediate.
      // Techinically it leverages the (macro) task queue,
      // but it is still a better choice than setTimeout.
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    }

    如果上面的 Promise 和 MutationObserver 都不支持的话, 我们继续会判断设备是否支持 setImmediate, 我们上面分析过, 他属于 macrotasks(宏任务)的。该任务会在一个宏任务里执行回调队列。

    4. 使用setTimeout 做降级处理

    如果我们上面三种情况, 设备都不支持的话, 我们会使用 setTimeout 来做降级处理, 实现延迟效果。如下基本代码:

    else {
      // Fallback to setTimeout.
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }

    现在我们的源码继续往下看, 会看到我们的nextTick函数被export了,如下基本代码:

    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 函数接收2个参数,cb 是一个回调函数, ctx 是一个上下文。 首先会把它存入callbacks函数数组里面去, 在函数内部会判断cb是否是一个函数,如果是一个函数,就调用执行该函数,当然它会在callbacks函数数组遍历的时候才会被执行。其次 如果cb不是一个函数的话, 那么会判断是否有_resolve值, 有该值就使用Promise.then() 这样的方式来调用。比如: this.$nextTick().then(cb) 这样的使用方式。因此在下面的if语句内会判断赋值给_resolve:

    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(resolve => {
        _resolve = resolve
      })
    }

    使用Promise返回了一个 fulfilled 的Promise。赋值给 _resolve; 然后在callbacks.push 中会执行如下:

    _resolve(ctx);

    全局方法Vue.nextTick在 /src/core/global-api/index.js 中声明,是对函数nextTick的引用,所以使用时可以显式指定执行上下文。代码初始化如下:

    Vue.nextTick = nextTick;

    我们可以使用如下的一个简单的demo来简化上面的代码。如下demo:

    如上我们已经知道了 nextTick 是Vue中的一个全局函数, 在Vue里面会有一个Watcher, 它用于观察数据的变化, 然后更新DOM, 但是在Vue中并不是每次数据改变都会触发更新DOM的, 而是将这些操作都缓存到一个队列中, 在一个事件循环结束后, 会刷新队列, 会统一执行DOM的更新操作。

    在Vue中使用的是Object.defineProperty来监听每个对象属性数据变化的, 当监听到数据发生变化的时候, 我们需要把该消息通知到所有的订阅者, 也就是Dep, 那么Dep则会调用它管理的所有的Watch对象,因此会调用Watch对象中的update方法, 我们可以看下源码中的update的实现。源码在 vue/src/core/observer/watcher.js 中如下代码:

    update () {
      /* istanbul ignore else */
      if (this.lazy) {
        this.dirty = true
      } else if (this.sync) {
        // 同步执行渲染视图
        this.run()
      } else {
        // 异步推送到观察者队列中
        queueWatcher(this)
      }
    }

    如上代码我们可以看到, 在Vue中它默认是使用异步执行DOM更新的。当异步执行update的时候,它默认会调用 queueWatcher 函数。

    我们下面再来看下该 queueWatcher 函数代码如下: (源码在: vue/src/core/observer/scheduler.js) 中。

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

    如上源码, 我们从第一句代码执行过来, 首先获取该 id = watcher.id; 然后判断该id是否存在 if (has[id] == null) {} , 如果已经存在则直接跳过,不存在则执行if
    语句内部代码, 并且标记哈希表has[id] = true; 用于下次检验。如果 flushing 为false的话, 则把该watcher对象push到队列中, 考虑到一些情况, 比如正在更新队列中
    的watcher时, 又有事件塞入进来怎么处理? 因此这边加了一个flushing来表示队列的更新状态。

    如果加入队列到更新状态时,又分为两种情况:

    1. 这个watcher还没有处理, 就找到这个watcher在队列中的位置, 并且把新的放在后面, 比如如下代码:

    if (!flushing) {
      queue.push(watcher)
    }

    2. 如果watcher已经更新过了, 就把这个watcher再放到当前执行的下一位, 当前的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)
    }

    接着如下代码:

    if (!waiting) {
      waiting = true
    
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }

    waiting 为false, 等待下一个tick时, 会执行刷新队列。 如果不是正式环境的话, 会直接 调用该函数 flushSchedulerQueue; (源码在: vue/src/core/observer/scheduler.js) 中。否则的话,  把该函数放入 nextTick 函数延迟处理。

    相关