探讨 Vue 2 源码中 `Watcher` 类的 `get` 方法如何触发依赖收集,以及 `update` 方法如何将变化推送到渲染队列。

早上好,各位未来的 Vue.js 大师们!今天要和大家深入挖掘 Vue 2 源码中 Watcher 类的精髓,特别是 get 方法如何巧妙地触发依赖收集,以及 update 方法如何将变化推送到渲染队列。准备好了吗?我们开始咯!

开场白:Watcher 是什么?为什么重要?

在开始之前,我们先来简单回顾一下 Watcher 在 Vue 2 中的角色。你可以把它想象成一个勤劳的工人,它的任务是监视某个表达式(比如 data 中的属性)的变化,一旦发现变化,就通知相应的组件进行更新。没有 Watcher,数据变化了,视图却纹丝不动,整个 Vue 应用就瘫痪了,所以说它非常重要!

第一幕:Watcher 的构造函数:生而不同

我们先来看看 Watcher 构造函数,了解一下 Watcher 对象在创建时都经历了什么。

/**
 * A watcher parses an expression and notifies the component when the
 * expression value changes. This is used for both the $watch() api and
 * directives.
 *
 * @param {Vue} vm      - Vue instance
 * @param {String|Function} expOrFn - Watcher expression or function
 * @param {Function} cb     - Callback when value changes
 * @param {Object} options   - Watcher options
 *                 {
 *                   deep: boolean,
 *                   user: boolean,
 *                   lazy: boolean,
 *                   sync: boolean,
 *                   before: Function
 *                 }
 * @param {Boolean} isRenderWatcher - internal marker for render watchers
 */
constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  this.vm = vm
  if (isRenderWatcher) {
    vm._watcher = this
  }
  vm._watchers.push(this)
  // options
  if (options) {
    this.deep = !!options.deep
    this.user = !!options.user
    this.lazy = !!options.lazy
    this.sync = !!options.sync
    this.before = options.before
  } else {
    this.deep = this.user = this.lazy = this.sync = false
  }
  this.cb = cb
  this.id = ++uid // uid for batching
  this.active = true
  this.dirty = this.lazy // for lazy watchers
  this.deps = []
  this.newDeps = []
  this.depIds = new Set()
  this.newDepIds = new Set()
  this.expression = process.env.NODE_ENV !== 'production'
    ? expOrFn.toString()
    : ''
  // parse expression for getter
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn
  } else {
    this.getter = parsePath(expOrFn)
    if (!this.getter) {
      this.getter = noop
      process.env.NODE_ENV !== 'production' && warn(
        `Failed watching path: "${expOrFn}" ` +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      )
    }
  }
  this.value = this.lazy
    ? undefined
    : this.get()
}

构造函数主要做了这些事情:

  • 保存关键信息: 比如 Vue 实例 vm,要监听的表达式 expOrFn,以及回调函数 cb
  • 处理选项: 解析 options,设置 deepuserlazy 等标志位。这些标志位决定了 Watcher 的行为。
  • 生成 getter 这是最关键的一步。根据 expOrFn 生成 getter 函数。这个 getter 函数的作用是获取表达式的值。如果 expOrFn 是一个函数,那么 getter 就是它本身;如果 expOrFn 是一个字符串,那么 getter 就是一个解析字符串路径的函数(parsePath)。
  • 如果是非 lazyWatcher,会立即调用 get() 方法: 这意味着 Watcher 在创建时就会立即执行依赖收集。

第二幕:get() 方法:依赖收集的启动器

接下来,我们进入今天的主题:get() 方法。这个方法是依赖收集的核心。

/**
 * Evaluate the getter, and re-collect dependencies.
 */
get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

让我们一步一步地分析 get() 方法的实现:

  1. pushTarget(this) 这是依赖收集的启动信号!pushTarget 函数会将当前的 Watcher 对象(this)推入一个全局的栈 Dep.targetStack 中。这个栈的作用是保存当前正在执行的 Watcher

  2. 执行 getter value = this.getter.call(vm, vm)。这里调用了之前生成的 getter 函数。getter 函数会访问组件实例 vm 上的数据。关键就在这里:getter 函数访问 data 中的属性时,会触发 data 属性的 getter 方法。

  3. data 属性的 getter 方法: data 属性的 getter 方法会做两件事:

    • 返回属性的值。
    • 调用 dep.depend() 方法。 这里的 depdata 属性对应的 Dep 对象。Dep 对象负责管理所有依赖于该属性的 Watcher
  4. dep.depend() 方法: dep.depend() 方法会将当前正在执行的 Watcher(也就是 Dep.targetStack 栈顶的 Watcher)添加到 depsubs 数组中。subs 数组存储了所有依赖于该属性的 Watcher

  5. deep 选项的处理: 如果 Watcher 设置了 deep 选项,那么会调用 traverse(value) 函数。traverse 函数会递归地访问 value 的所有属性,从而触发这些属性的依赖收集。

  6. popTarget()getter 函数执行完毕后,popTarget 函数会将 Dep.targetStack 栈顶的 Watcher 移除。这意味着依赖收集已经结束。

  7. cleanupDeps() 这个方法用于清理旧的依赖关系,并建立新的依赖关系。我们稍后会详细讲解。

  8. 返回 value get() 方法最终返回 getter 函数计算出的值。

总结一下,get() 方法通过 pushTargetpopTarget 两个函数,将 Watcher 对象推入和弹出 Dep.targetStack 栈,从而标记当前正在执行的 Watcher。当 getter 函数访问 data 属性时,会触发 data 属性的 getter 方法,进而调用 dep.depend() 方法,将当前的 Watcher 添加到 depsubs 数组中。这就是依赖收集的过程!

第三幕:update() 方法:变化的传递者

现在我们来看看 update() 方法。这个方法负责将变化推送到渲染队列,最终触发视图的更新。

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

update() 方法的实现非常简单,但是根据 Watcher 的不同类型,它的行为也会有所不同:

  • lazyWatcher 如果 Watcherlazy 的,那么会将 this.dirty 设置为 truelazyWatcher 不会立即更新,而是等到需要时才更新。
  • syncWatcher 如果 Watchersync 的,那么会立即调用 this.run() 方法。syncWatcher 会同步更新,这意味着视图会立即更新。
  • 默认情况: 如果 Watcher 既不是 lazy 的,也不是 sync 的,那么会将 this 传递给 queueWatcher(this) 函数。queueWatcher 函数会将 Watcher 添加到渲染队列中。

第四幕:queueWatcher() 函数:渲染队列的管理者

queueWatcher() 函数是 Vue 2 中一个非常重要的函数。它负责管理渲染队列,确保视图的更新以高效的方式进行。

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

queueWatcher() 函数的实现逻辑如下:

  1. 检查 Watcher 是否已经存在: 使用 has 对象来记录已经添加到队列中的 Watcherid。如果 Watcher 已经存在,那么直接返回,避免重复添加。
  2. 添加到队列: 如果 Watcher 不存在,那么将其添加到 queue 数组中。
    • 如果当前没有在刷新队列(!flushing),那么直接将 Watcher 添加到队列的末尾。
    • 如果当前正在刷新队列(flushing),那么需要根据 Watcherid 将其插入到队列的合适位置,以确保更新的顺序正确。
  3. 触发队列刷新: 如果当前没有在等待刷新(!waiting),那么会将 waiting 设置为 true,并调用 nextTick(flushSchedulerQueue) 函数。nextTick 函数会将 flushSchedulerQueue 函数添加到下一个事件循环中执行。

第五幕:flushSchedulerQueue() 函数:渲染队列的执行者

flushSchedulerQueue() 函数负责执行渲染队列中的所有 Watcher

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  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 update,
  //    we can skip the child's update.
  queue.sort(sort)

  // 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]
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and warn for recursive updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      recursiveUpdateCount++
      if (recursiveUpdateCount > RECURSION_LIMIT) {
        warn(
          'You may have an infinite update loop in a component. ' +
          'Make sure you do not have an infinite recursive updates.'
        )
        break
      }
    }
  }

  // keep copies of the queue so that lifecycle hooks can be used to selectively
  // invoke wathcers.
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
}

flushSchedulerQueue() 函数的实现逻辑如下:

  1. 设置 flushingtrue 表示当前正在刷新队列。
  2. 对队列进行排序: 调用 queue.sort(sort) 函数对队列进行排序。排序的目的是确保组件的更新顺序是从父组件到子组件,并且用户 Watcher 在渲染 Watcher 之前执行。
  3. 遍历队列: 遍历队列中的所有 Watcher,并依次执行它们的 run() 方法。
  4. 重置状态: 调用 resetSchedulerState() 函数重置调度器的状态。

第六幕:Watcher.run() 方法:触发回调函数

最后,我们来看看 Watcherrun() 方法。这个方法负责执行 Watcher 的回调函数。

/**
 * This is where the scheduler starts.
 * It is called recursively to run all queued watchers,
 * until there are no more watchers left to run.
 */
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() 方法的实现逻辑如下:

  1. 检查 Watcher 是否处于激活状态: 只有处于激活状态的 Watcher 才能执行。
  2. 获取新值: 调用 this.get() 方法获取表达式的新值。这个过程会再次触发依赖收集,但是这次的依赖收集是为了更新 Watcher 的依赖关系。
  3. 比较新值和旧值: 如果新值和旧值不相等,或者 Watcher 是深度监听的,或者监听的是对象或数组,那么需要执行回调函数。
  4. 执行回调函数: 调用 this.cb.call(this.vm, value, oldValue) 执行回调函数,并将新值和旧值作为参数传递给回调函数。

第七幕:cleanupDeps() 方法:依赖的清理工

cleanupDeps() 方法是用来清理依赖的,防止内存泄漏,并保证依赖关系的正确性。

/**
 * Clean up for dependency collection.
 */
cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  let tmp = this.depIds
  this.depIds = this.newDepIds
  this.newDepIds = tmp
  this.newDepIds.clear()
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  this.newDeps.length = 0
}

这个方法主要做了一下几件事:

  1. 遍历旧的依赖: 遍历 this.deps 数组,这个数组包含了上一次依赖收集中收集到的所有 Dep 对象。
  2. 检查依赖是否仍然存在: 对于每个 Dep 对象,检查它是否仍然存在于 this.newDepIds 中。this.newDepIds 包含了本次依赖收集中收集到的所有 Dep 对象的 id
  3. 移除不再存在的依赖: 如果 Dep 对象不再存在于 this.newDepIds 中,说明该依赖已经失效,需要从 Dep 对象的 subs 数组中移除当前的 Watcher
  4. 更新依赖关系: 交换 this.depIdsthis.newDepIds,交换 this.depsthis.newDeps,并清空 this.newDepIdsthis.newDeps

总结:依赖收集和更新流程

我们来总结一下整个依赖收集和更新的流程:

  1. 创建 Watcher 在创建 Watcher 时,会生成 getter 函数,并立即调用 get() 方法(除非是 lazyWatcher)。
  2. get() 方法触发依赖收集: get() 方法会将当前的 Watcher 推入 Dep.targetStack 栈,并执行 getter 函数。
  3. data 属性的 getter 方法收集依赖:getter 函数访问 data 属性时,会触发 data 属性的 getter 方法,进而调用 dep.depend() 方法,将当前的 Watcher 添加到 depsubs 数组中。
  4. 数据变化:data 属性的值发生变化时,会触发 dep.notify() 方法。
  5. dep.notify() 方法通知 Watcher dep.notify() 方法会遍历 depsubs 数组,并依次调用每个 Watcherupdate() 方法。
  6. update() 方法将 Watcher 添加到渲染队列: update() 方法会将 Watcher 添加到渲染队列中(除非是 lazysyncWatcher)。
  7. flushSchedulerQueue() 函数执行渲染队列: flushSchedulerQueue() 函数会将渲染队列中的所有 Watcher 按照一定的顺序执行。
  8. Watcher.run() 方法触发回调函数: Watcher.run() 方法会获取表达式的新值,并比较新值和旧值,如果需要更新,则执行回调函数。

表格总结

方法/函数 作用
Watcher 构造函数 创建 Watcher 对象,保存关键信息,生成 getter 函数。
Watcher.get() 触发依赖收集,将 Watcher 推入 Dep.targetStack 栈,执行 getter 函数,获取表达式的值。
data 属性的 getter 当访问 data 属性时触发,返回属性的值,并调用 dep.depend() 方法,将当前的 Watcher 添加到 depsubs 数组中。
dep.depend() 将当前的 Watcher 添加到 depsubs 数组中。
dep.notify() data 属性的值发生变化时触发,遍历 depsubs 数组,并依次调用每个 Watcherupdate() 方法。
Watcher.update() Watcher 添加到渲染队列中(除非是 lazysyncWatcher)。
queueWatcher() 管理渲染队列,将 Watcher 添加到队列中,并触发队列刷新。
flushSchedulerQueue() 执行渲染队列中的所有 Watcher,按照一定的顺序执行,确保组件的更新顺序是从父组件到子组件,并且用户 Watcher 在渲染 Watcher 之前执行。
Watcher.run() 获取表达式的新值,并比较新值和旧值,如果需要更新,则执行回调函数。
cleanupDeps() 清理旧的依赖关系,并建立新的依赖关系。

结语:掌握 Watcher,玩转 Vue

掌握 Watcher 的工作原理,你就能更深入地理解 Vue 的响应式系统,也能更好地调试和优化 Vue 应用。希望今天的讲座对大家有所帮助!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注