Vue源码之nextTick(个人向)

786 阅读7分钟

写在前面

来了来了,我来填坑了之前响应式文章中,关于最后Dep的notify后通知Watcher的run去更新数据的中间环节省略内容,以及nextTick的内容,这篇文章讲会大致说一下。大致的会分为一下两个部分

  • 怎么走到了nextTick
  • 来看看nextTick

注:这次源码内容基于"vue@^2.6.10"版本,因为会和大佬PDF的版本(Vue.js 2.5.17-beta.0,)有部分不同,虽然我再关键处会贴出两个部分进行对比, 但是我还是建议看这个文章的时候也能打开自己Vue项目里的Vue文件大致看一下。

ok 让我们开始。

1、怎么走到了nextTick

这里我们不再已响应式那个流程走,而是以nextTick为源头看看,来搜源码。

  • 定义nextTick 在 src/core/util/next-tick.js 中,
  • 那里用了?全局搜,除了一些runtime和全局混入中的Vue.prototype.$nextTick(),我们找到了src/core/observer/scheduler.js 中queueWatcher函数调用了nextTick(flushSchedulerQueue)。
  • 来 我们继续,flushSchedulerQueue干啥了 ,看里面 它主要各种操作走了watcher.run()的方法。继续
  • watcher.run()我们来到src/core/observer/watcher.js中看一下run的方法。好了 完事了,这里调用了cb然后就去更新数据了。
  • 好了好了,完事了,逆推完美撒花,结束,,,,,,哈哈,当然不可能。大致走完了,我们接下来将源码贴上,再从run开始推回去,我们再屡一下。

1.1 Dep.notify => Watcher.updata => queueWatcher

  • src/core/observer/dep.js
 notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

看到了,发布订阅的通知就是从这里开始,继续走update,当然这里还对是否有全局异步进行了判断,这个地方我们后面大致说一下吧,因为这个地方2.5版本中并没有。。。而且后面有不少地方都能见到这个process.env.NODE_ENV !== 'production' && !config.async判断。

  • src/core/observer/watcher.js
 /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  

我们在watcher的update中可以看到,还是判断,正常没有乱七八糟的配置的话,就会走queueWatcher。

  • src/core/observer/scheduler.js
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
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)
    }
  }
}

这⾥引⼊了⼀个队列的概念,这也是 Vue在做派发更新的时候的⼀个优化的点,它并不会每次数据改 变都触发 watcher 的回调,⽽是把这些 watcher 先添加到⼀个队列⾥,然后在 nextTick 后执 ⾏ flushSchedulerQueue 。 这⾥有⼏个细节要注意⼀下,⾸先⽤ has 对象保证同⼀个 Watcher 只添加⼀次;接着对 flushing 的判断;最后通过 wating 保证对 nextTick(flushSchedulerQueue) 的调⽤逻辑只有⼀次,另外 nextTick 的实现我之后会抽⼀⼩节 专门去讲,⽬前就可以理解它是在下⼀个 tick,也就是异步的去执⾏ flushSchedulerQueue 。

  • src/core/observer/scheduler.js
/**
 * Flush both queues and run the watchers.
 */
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()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

...do other thing....
}
  1. 队列排序 来看注释。queue.sort((a, b) => a.id - b.id) 对队列做了从⼩到⼤的排序,这么做主要有以下要确保以下 ⼏点:

1.1. 组件的更新由⽗到⼦;因为⽗组件的创建过程是先于⼦的,所以 watcher 的创建也是先⽗后⼦,执⾏顺序也应该保持先⽗后⼦。

1.2. ⽤户的⾃定义 watcher 要优先于渲染 watcher 执⾏;因为⽤户⾃定义 watcher 是在渲染 watcher 之前创建的。

1.3. 如果⼀个组件在⽗组件的 watcher 执⾏期间被销毁,那么它对应的 watcher 执⾏都可以被跳过,所以⽗组件的 watcher 应该先执⾏。

  1. 队列遍历(不细究了,代码都有注释,注意 queueWatcher 中else中的注释
else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.

) 3. 状态恢复(resetSchedulerState)

  • src/core/observer/watcher.js
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

来到了run()这就很明显了。就是执行回调,这里你如果是看2.5版本的话会看到

run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}

然后getAndInvoke这个方法和2.6的版本执行的一样,这里也可以看到,Vue也是在会不断地优化自己的代码,当然也不排除之前有想再run中加别的东西的可能。

- 小结:

  • 再正式开始说nextTick前,我们算是把之前响应式派发的债补了,但是这也是我看nextTick后才回过劲迷糊过来的,之前不敢写也是因为自己也是糊里糊涂。
  • 大致梳理一下,就是我们派发更新会调用notify然后走watcher的update然后呢走queueWatcher这个又是一个调用nextTick传递flushSchedulerQueue函数的方法,flushSchedulerQueue中又是走了watcher的run,。我们会再nextTick中依次调用flushSchedulerQueue也就是已经拍好对的run的方法,也就是依次执行数据更新。,语言文字够清晰了吧 ,奥利给。。。。。后面给个图吧,

  • 下面我们来说一下nextTick吧

来看看nextTick

代码不多直接上源码吧(2.6.10)

  • src/core/util/next-tick.js
/* @flow */
/* globals MutationObserver */

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

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

(
我卑微的添加
let microTimerFunc
let macroTimerFunc
2.5版本没有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)
  ...进行 MutationObserver的处理,用不到为了短点,就删了
} 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)
  }
} 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
    })
  }
}

这里其实和大佬PDF中有点不一样大佬PDF 分析的是2.5版,不同代码我就不贴出去了,。我再我这个地方的加注释吧,凑活看,不行的话,有兴趣的自己下载2.5去看、2.5的时候Vue在这里会声明 microTimerFunc 和 macroTimerFunc 2 个变量,它们分别对应的是 microtask 的函数和 macro task 的函数,但是后面也是会加判断,最后⼀次性地根据 useMacroTask 条件执⾏ macroTimerFunc 或者是 microTimerFunc 。

我们既然自己已经看了2.6+版,那么我们就不在去学习2.5的了,已上面2.6+为主, 来开始

  • 读注释: 上面有两大段注释,我彩笔翻译(加机翻)大致如下
  1. 这里我们有异步延迟使用microtasks包装。在2.5我们使用(宏观)任务(结合microtasks)。然而,当状态发生改变且重绘之前会发生一些微妙的问题,同时,在事件处理程序中使用(宏观)任务会导致一些奇怪的行为,因此现在我们选择再任何地方都使用microtasks。这个权衡的一个主要缺点是,有一些场景比如,microtasks过高的优先级。。。。我去他XXX 遍不下去了,,,求大佬指点,反正人尤大肯定有考虑选择了这个方案。,
  2. nextTick行为利用了microtask队列,可以直接使用原生的promise或者是 MutationObserver.MutationObserver更广泛的支持,然而它是严重困扰着/ 在iOS UIWebView > = 9.3.3事件处理程序触发后联系。。。。。大致意思就是告诉你怎么在这判断吧MutationObserver这玩意,不太了解,这里还是判断了setImmediate这是⼀个⾼版本 IE 和 Edge 才⽀持的特性,不⽀持的话再去检测是否⽀持原⽣的 MessageChannel , 如果也不⽀持的话就会降级为 setTimeout 0 ;

哎反正乱七八糟翻译了一下,大致意思就是在判断选用什么样的异步方式,当然我们只看我们最关键的Web的。

走流程吧

先来看 nextTick ,这就是我们在上⼀节执⾏ nextTick(flushSchedulerQueue) 所⽤到的函数。它的逻辑也很简单,把传⼊的回调函数 cb 压⼊ callbacks 数组,它们都会在下⼀个 tick 执⾏ flushCallbacks , flushCallbacks 的逻辑⾮常简单,对callbacks遍历,然后执⾏相应的回调函数。这⾥使⽤ callbacks⽽不是直接在nextTick中执⾏回调函数的原因是保证在同⼀个 tick内多次执⾏nextTick,不会开启多个异步任务,⽽把这些异步任务都压成⼀个同步任务,在下⼀个 tick 执⾏完毕。

然后就很清晰了啊, 回去调用watcher的run去更新数据了,哈哈 完事。。

但是这里还有两个细节我想再说一下

  • slice(0)
const copies = callbacks.slice(0)

我们可以看到上面有个这样的一段代码。我专门搜了搜,说是有两个功能。 我们使用slice(0)对原始数组进行一个深拷贝和将类数组对象转化为真正的数组对象,当时我看到这的时候确实不太了解,看了看,然后才发现Vue中有很多类似的写法,哈哈,有兴趣的可以再stackoverflow看看 注:我的第一个评论,开心,先声明一下啊。关于slice(0)的使用。我最开始是百度搜索,唯一搜到的是这个文章"blog.csdn.net/i042416/art…",里面给了两种说法和stackoverflow的链接,这个深浅拷贝很好验证,控制台稍微打印一下,可以验证,如下图,但是关于这个知识点需要好好梳理一下,这里算是在给自己加个坑把。

具体的分析还是建议看stackoverflow。over

再次注:我先道歉,上面是错的。晚上再看一个课程视频中正好讲了slice的深浅拷贝问题具体表现如下

我们可以看到,对于数组中的引用类型修改,这种拷贝并没有完成深拷贝,我不再乱言。推荐"www.cnblogs.com/echolun/p/7…"这篇文章,里面讲到了关于slice拷贝的部分。 最后还是谢谢我第一个文章评论者left_泽鑫的指正,也让我进一步学习了。

  • 对于config.sync的判断
 if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }

我们上面留的问题,这里我们需要去找一下config.async

  • 源码地址src/core/config.js 对于全局配置可以看我的API学习文档
/**
   * Perform updates asynchronously. Intended to be used by Vue Test Utils
   * This will significantly reduce performance if set to false.
   */
  async: true,

这就是async的注释,翻译如下 执行异步更新。为了Vue的单元测试使用,,如果设置为false。这将显著降低性能。默认是true 拿回去看,就很明白了。上面的判断那就是如果有async的专门配置的话,就会让所有的数据更新进行单个流程的跑,

- 总结:

nextTick算是完事了,。但是我觉得还缺点什么应该是实际的分析和操作吧,后续有空补上,毕竟Vue的开发一大利器,不能仅限于源码看。。。

后记

  • 没啥说的,希望多交流,也希望看到的你能有收获,谢谢。