Vue之next-Tick源码解析

995 阅读1分钟

概要

官网说明:将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。

应用

首先看下开发中是如何应用的


<template>
  <div>{{ tips }}</div>
</template>

methods: {
  upateDom1() {
    this.tips = '第一种用法'
    console.log(this.tips) // => DOM未更新
    
    // 参数 -> 回调函数
    this.$nextTick(function() {
      console.log(this.tips) // => DOM已更新
    })
  },
  async updateDom2 () {
    this.tips = '换一种用法'
    console.log(this.tips) // => DOM未更新
    
    /*因为 this.$nextTick() 返回的是 Promise,所以可以使用 async/await 语法 */
    await this.$nextTick()
    console.log(this.tips) // => DOM已更新
  }
}

源码解析

Vue版本: 2.6.14
源码路径:/src/core/util/next-tick.js

初始化,先准备一个存储回调函数的队列,再准备一个执行函数


// 回调函数队列
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]()
  }

}

大概分析函数 nextTick 都做了啥

  1. 回调函数 cb 不为空推进队列 callbacks
  2. pending为false,执行函数 timerFunc下一段代码分析它
  3. 最后一段 if 就可以解释为啥可以用语法 async/await

/**

 * @param {Function} cb 回调函数

 * @param {Object} ctx 上下文

 */

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

  })

  // pending = false 执行callbacks里的函数
  if (!pending) {
    pending = true
    // 执行被包装后的函数
    timerFunc()
  }

  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {

    return new Promise(resolve => {
      _resolve = resolve
    })

  }

}

💡 Tips:提一嘴,使用的时候没传过 ctx 这玩意儿啊,其实是封装了一层,如下:


Vue.prototype.$nextTick = function (fn: Function) {
   return nextTick(fn, this)
 }

如下代码可以看出 timerFunc 的最终目的是为了执行 flushCallbacks

细节上,对于处理一些环境/兼容等问题而做的策略优先级: Promise -> MutationObserver -> setImmediate -> setTimeout


let timerFunc

/** 优先使用 promise */
if (typeof Promise !== 'undefined' && isNative(Promise)) {

  const p = Promise.resolve()

  timerFunc = () => {
    p.then(flushCallbacks)

    // 在有问题的UIWebViews中,Promise.then 会有一个奇怪的状态,回调被推到微任务队列,
    // 但队列不会被更新,直到浏览器需要做一些其他的操作,例如处理计时器,所以我们可以通过添加一个空定时器来强制触发
    if (isIOS) setTimeout(noop)
  }

  isUsingMicroTask = true

  // 这里通过 MutationObserver 的特性来触发回调函数的执行
  // MutationObserver 详情:https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
} 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)) {

  // 回退到 setImmediate.
  // 技术角度,它利用了宏任务队列,但它仍然比 setTimeout 更好
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }

} else {

  // 回退到 setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }

}

 那么this.$nextTick(cb)cb 内又是怎么知道 this.tips = '第一种用法' 这段DOM已经更新了呢,其实这段代码已经按照执行顺序先一步推进 nextTick 的队列,简单的说 data 里的数据变更也是需要排队的
如下:flushSchedulerQueue 主要是对DOM更新处理
源码路径:/src/core/observer/scheduler.js


export function queueWatcher (watcher: Watcher) {

    if (!waiting) {

      waiting = true

      // 更新DOM的处理 作为回调入参,
      nextTick(flushSchedulerQueue)

    }

  }

}

扯到 watcher 还很多东西可以讲,但这是 nextTick 的专题,俺今天能理解它就够了

官网说明:异步更新队列