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

各位观众,晚上好!我是你们今晚的导游,将带你们深入 Vue 2 的源码腹地,探秘 Watcher 的 "get" 和 "update" 方法,看看它们是如何在依赖收集和渲染队列中翩翩起舞的。准备好迎接一场源码级别的探险了吗?Let’s go!

第一站:Watcher 的 "get" 方法:依赖收集的幕后推手

首先,我们要明确一点:依赖收集的目的是什么?简单来说,就是让 Vue 知道当哪些数据发生变化时,需要通知哪些 Watcher 进行更新。而 Watcherget 方法,就是负责启动这场 "数据与观察者" 之间浪漫邂逅的关键人物。

让我们先来看看 Watcherget 方法的代码(简化版):

// src/core/observer/watcher.js

get() {
  pushTarget(this) // 重要!将当前 watcher 推入全局的 targetStack
  let value
  try {
    value = this.getter.call(vm, vm) // 执行 getter,触发依赖收集
  } catch (e) {
    // 处理错误
  } finally {
    popTarget() // 重要!将当前 watcher 从 targetStack 中弹出
  }
  return value
}

这段代码的核心在于 pushTarget(this)this.getter.call(vm, vm)popTarget() 这三行。它们共同完成了一项伟大的任务:建立数据和 Watcher 之间的连接。

  1. pushTarget(this):吹响依赖收集的号角

    pushTarget 函数的作用是将当前 Watcher 实例推入一个全局的栈 targetStack 中。这个 targetStack 就像一个临时的 "观察者登记处",告诉我们当前哪个 Watcher 正在 "工作"。

    // src/core/observer/dep.js
    
    let targetStack = []
    
    export function pushTarget (_target: ?Watcher) {
      if (Dep.target) targetStack.push(Dep.target)
      Dep.target = _target
    }
    
    export function popTarget () {
      Dep.target = targetStack.pop()
    }

    这里 Dep.target 扮演着 "当前活跃的 Watcher" 的角色。每次只有一个 Watcher 能够成为 Dep.targetpushTarget 就像是说:“嘿,现在 this 这个 Watcher 要开始工作了,大家都注意了!”。 popTarget 则是说:“好了,this 这个 Watcher 工作结束了,可以下班了!”

  2. this.getter.call(vm, vm):触发依赖收集的导火索

    this.getter 是一个函数,它负责获取 Watcher 所观察的数据。这个函数通常是由 Vue 编译生成的,它会访问组件的 datacomputed 等属性。

    关键在于,当 this.getter 函数执行时,它会访问到响应式数据。而响应式数据的 getter 会执行依赖收集的操作。

    让我们来看看响应式数据的 getter 是如何工作的:

    // src/core/observer/index.js (observe 函数中定义的 getter)
    
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) { // 关键!判断是否存在活跃的 Watcher
        dep.depend() // 执行依赖收集
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    }

    这里的 Dep.target 就是我们刚刚 pushTarget 进去的 Watcher 实例。如果 Dep.target 存在,说明当前有 Watcher 正在 "监听"。 dep.depend() 就像是说:“嘿,Dep.target (也就是当前的 Watcher) 对我(这个数据)感兴趣,我记下来了!”

    dep.depend() 的作用是:

    • 将当前的 Watcher 实例添加到 Dep 对象的 subs 数组中。subs 数组存储了所有依赖于该数据的 Watcher
    • 同时,将当前的 Dep 对象添加到 Watcher 实例的 newDeps 数组中。

    这样,数据和 Watcher 之间就建立了双向的连接。

  3. popTarget():清理现场

    popTarget 函数的作用是将 targetStack 栈顶的 Watcher 弹出,恢复 Dep.target 的状态。 这就像是说:“好了,这个 Watcher 已经完成了它的工作,现在轮到下一个 Watcher 了!”

依赖收集的完整流程,用表格梳理一下更清晰:

步骤 操作 作用 参与者
1 pushTarget(watcher) 将当前 Watcher 推入 targetStack,设置为 Dep.target Watcher, Dep
2 执行 watcher.getter 访问响应式数据 Watcher, 组件实例
3 响应式数据的 getter 中的 dep.depend() 将当前 Watcher 添加到 Depsubs 数组中,同时将 Dep 添加到 WatchernewDeps 数组中 Dep, Watcher
4 popTarget() 将当前 WatchertargetStack 中弹出,恢复 Dep.target 的状态 Watcher, Dep

第二站:Watcher 的 "update" 方法:将变化推入渲染队列

当响应式数据发生变化时,Dep 对象会通知所有依赖于它的 Watcher 进行更新。而 Watcherupdate 方法,就是处理这些更新通知的关键人物。

让我们来看看 Watcherupdate 方法的代码(简化版):

// src/core/observer/watcher.js

update() {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this) // 重要!将 watcher 推入队列
  }
}

这段代码根据 Watcher 的不同类型,采取不同的更新策略。

  • this.lazy:懒执行的 Watcher (例如 computed property)

    如果 Watcher 是一个懒执行的 Watcher (例如 computed property 对应的 Watcher),那么只需要将 this.dirty 设置为 true,表示该 computed property 的值已经过期,下次访问时需要重新计算。

  • this.sync:同步更新的 Watcher

    如果 Watcher 是一个同步更新的 Watcher,那么直接调用 this.run() 方法进行更新。 这种方式通常用于一些需要立即更新的场景,例如在 watch 选项中使用 sync: true

  • 默认情况:异步更新,推入队列

    在大多数情况下,Watcher 都会采用异步更新的方式。 queueWatcher(this) 的作用是将当前的 Watcher 实例推入一个队列中,等待 Vue 在下一个 tick 时统一进行更新。

    // src/core/observer/scheduler.js
    
    const queue: Array<Watcher> = []
    let has: { [key: number]: ?true } = {}
    let waiting = false
    let flushing = false
    let index = 0
    
    function queueWatcher (watcher: Watcher) {
      const id = watcher.id
      if (has[id] == null) {
        has[id] = true
        if (!flushing) {
          queue.push(watcher)
        } else {
          // 如果正在 flushing,则根据 watcher.id 插入到队列的合适位置
          // 保证:
          // 1. 组件的 update 由父到子执行
          // 2. 用户定义的 watcher 比 render watcher 先执行
          let i = queue.length - 1
          while (i > index && queue[i].id > watcher.id) {
            i--
          }
          queue.splice(i + 1, 0, watcher)
        }
        if (!waiting) {
          waiting = true
          nextTick(flushSchedulerQueue)
        }
      }
    }

    queueWatcher 函数的作用是:

    • Watcher 添加到 queue 队列中。
    • 使用 has 对象来避免重复添加同一个 Watcher
    • 如果当前没有正在刷新队列 ( flushingfalse ),则直接将 Watcher 添加到队列的末尾。
    • 如果当前正在刷新队列 ( flushingtrue ),则根据 Watcherid 将其插入到队列的合适位置,以保证更新的顺序。
    • 如果当前没有正在等待刷新队列 ( waitingfalse ),则调用 nextTick(flushSchedulerQueue) 函数,触发 Vue 在下一个 tick 时刷新队列。

    nextTick 函数的作用是将 flushSchedulerQueue 函数推入一个微任务队列中,等待浏览器空闲时执行。

    // src/core/util/next-tick.js
    
    let 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)) {
      // 使用 Promise
      const p = Promise.resolve()
      timerFunc = () => {
        p.then(flushCallbacks)
      }
    } else if (typeof MutationObserver !== 'undefined' && (
      isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
      // 使用 MutationObserver
      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)
      }
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      // 使用 setImmediate
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else {
      // 使用 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()
      }
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }

    nextTick 函数会优先使用 PromiseMutationObserversetImmediate,如果都不支持,则使用 setTimeout

    当浏览器空闲时,flushSchedulerQueue 函数会被执行,它会遍历 queue 队列,依次执行每个 Watcherrun 方法。

    // src/core/observer/scheduler.js
    
    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 component's update.
      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]
        id = watcher.id
        has[id] = null
        watcher.run()
        // in dev build, check and invoke watcher's callback
        if (has[id] != null) {
          cleanupDeps(watcher)
        }
      }
    
      resetSchedulerState()
    }
    
    function resetSchedulerState () {
      index = queue.length = 0
      has = {}
      waiting = flushing = false
    }

    flushSchedulerQueue 函数的作用是:

    • flushing 设置为 true,表示当前正在刷新队列。
    • queue 队列进行排序,以保证更新的顺序。
      • 父组件的更新先于子组件。
      • 用户定义的 Watcher 先于渲染 Watcher
    • 遍历 queue 队列,依次执行每个 Watcherrun 方法。
    • 在开发环境下,会检查并调用 Watcher 的回调函数。
    • 调用 resetSchedulerState 函数,重置调度器的状态。

    Watcherrun 方法会真正执行更新操作:

    // src/core/observer/watcher.js
    
    run() {
      if (this.active) {
        const value = this.get() // 重新求值,触发依赖收集
        if (
          value !== this.value ||
          // 对象的引用发生了变化
          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 方法的作用是:

    • 调用 this.get() 方法,重新求值,触发依赖收集。
    • 比较新值和旧值,如果发生了变化,则调用 this.cb 方法,执行更新操作。

总结:Watcher 的 "get" 和 "update" 方法的协作流程

  1. 依赖收集 (get):当组件首次渲染或数据变化时,Watcherget 方法被调用。get 方法会将当前 Watcher 推入 targetStack,然后执行 getter 函数,访问响应式数据,触发依赖收集。
  2. 数据变化:当响应式数据发生变化时,Dep 对象会通知所有依赖于它的 Watcher
  3. 推入队列 (update)Watcherupdate 方法被调用,将 Watcher 推入更新队列 queue 中。
  4. 异步更新:在下一个 tick 时,Vue 会遍历更新队列 queue,依次执行每个 Watcherrun 方法。
  5. 执行更新 (run)Watcherrun 方法会重新求值,并执行更新操作,例如更新 DOM。

用表格总结 Watcherupdate 方法的流程:

步骤 操作 作用 参与者
1 数据变化,Dep 通知 Watcher 触发 Watcher 的更新 Dep, Watcher
2 Watcherupdate 方法被调用 根据 Watcher 的类型决定更新策略 Watcher
3 如果是异步更新,则将 Watcher 推入 queue 等待下一个 tick 时统一更新 Watcher, 调度器
4 nextTick 触发 flushSchedulerQueue 遍历 queue,执行每个 Watcherrun 方法 调度器
5 Watcherrun 方法被调用 重新求值,比较新旧值,执行更新操作 Watcher, 组件实例

一些需要注意的点:

  • Vue 的依赖收集是细粒度的,只有当组件实际使用了某个数据时,才会建立依赖关系。
  • Vue 的更新是异步的,采用队列的方式,可以有效地减少不必要的 DOM 操作,提高性能。
  • Vue 的更新顺序是有保证的,父组件的更新先于子组件,用户定义的 Watcher 先于渲染 Watcher

好了,今天的源码探险就到这里。希望通过这次旅程,大家对 Vue 2 中 Watcher 的 "get" 和 "update" 方法有了更深入的理解。下次再见!

发表回复

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