vue2源码系列-深入Watcher

940 阅读7分钟

前面我们在 vue2源码系列-响应式原理 中介绍了 vue 中的整个响应式实现及流程,其中跳过了某些细节性的代码,现在我们再去好好学习研究一番。

学习目标

我们先来梳理下本篇文章的学习目标

  1. 整明白 Watcher 的每一行代码(PS:有点夸张了)

  2. 明白 renderWatcheruserWatcher

  3. 清楚 watchcomputed 选项实现原理,两者的参数实现及差异

  4. 清楚 asyncWatcher 的排队执行机制

Watcher实现

前面学习 vue响应式原理 的时候其实是有对 Watcher 的实现进行一个比较完整的分析的,只是部分细节没有深入。这次争取弄明白全部实现。

let uid = 0
export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    // renderWatcher 一个实例对应唯一一个renderWatcher
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep // deepWatcher
      this.user = !!options.user // userWatcher
      this.lazy = !!options.lazy // computed实现
      this.sync = !!options.sync // syncWatcher
      this.before = options.before // 前置函数 比如在渲染前会调用 hook:mounted
    } 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

    // deps newDeps 用于dep收集
    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)
    }

    // 非computed执行get()
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 执行getter添加订阅
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // watch deep 参数实现原理
      if (this.deep) {
        // traverse就是递归遍历value触发各个属性的getter
        traverse(value)
      }
      popTarget()

      // 清空本轮deps
      this.cleanupDeps()
    }
    return value
  }

  // 添加订阅
  addDep (dep: Dep) {
    const id = dep.id
    // 防止重复收集
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  // 清空本轮deps
  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
  }

  // 接收订阅中心的更新通知
  update () {
    // computed使用
    if (this.lazy) {
      this.dirty = true
    // 同步watcher
    } else if (this.sync) {
      this.run()
    // 异步watcher队列
    } else {
      queueWatcher(this)
    }
  }

  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) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  // computed的更新通知
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  // 用于computed依赖其它属性deps
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  // 移除deps
  teardown () {
    if (this.active) {
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

有了上次的基础,Watcher 的代码理解起来其实不难。上面我们对每个函数及重点代码都有注释分析,下面我们再看看一些容易看不明白的点。

newDepIds/depIds

值得一说的是收集 dep 的作用。很简单,就是防止重复收集。那为什么需要两个数组来实现了,单单防止重复收集的话,一个 Set 数组无疑是最简单的。

我们知道每个更新的时候都会执行 this.get() -> this.getter.call(vm, vm), 其实使用两个数组的原因就在于在 getter 函数中,是会触发不同属性的 getter 的,他们会将属性的对应的 dep 添加当前 watcher 订阅。

所以每次更新的时候 getter 函数有可能触发不同属性的 getter,这时候应该添加新一轮的订阅,而老一轮的订阅可能已经不存在了,所以需要及时移除,这就是使用两个数组而不是一个数组实现的原因。具体复现可以通过指令 v-if 来试试,加深理解。

 cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    // 移除上一轮不必要的订阅
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }

  // 这里的实现有点绕 将newDepIds赋值为depIds,而后再通过clear方法清空
  // 让人难免觉得多此一举,为什么不直接 newDepIds = [] 呢
  // 按照我个人的理解是 newDepIds = [] 势必要新建数组
  // 而下面的方式不需要开辟新内存,也不用回收旧内存,一直是原来两个内存地址,属于优化内存的写法
  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
}

computed的实现

上面 Watcher 的实现其实很多是和实现 computed 相关的,所以我们来看看 computed 的实现原理

initComputed

我们之前分析 vue 初始化的时候,知道在 initState 中会调用 initComputed 初始计算属性

const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  // 收集计算属性
  const watchers = vm._computedWatchers = Object.create(null)
  // 判断服务端渲染
  const isSSR = isServerRendering()

  // 遍历computed
  for (const key in computed) {
    const userDef = computed[key]
    // 兼容函数及对象写法
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if (!isSSR) {
      // 实例化watcher
      // 特别
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // 定义getter
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

可以看到 initComputed 主要是对 computed 选项进行遍历初始化 watcher 实例,和其它 watcher 不同之处就在于选项中传了参数 lazy:true。我们再看看 defineComputed

defineComputed

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 这段代码比较多但是逻辑不复杂 主要就是判断计算属性的set get设置
  // 我们重点关注 get 就行
  // 其实主要逻辑就是通过createComputedGetter创建getter函数再定义到vm对应属性中
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    // 这边有个cache参数可以用于关闭缓存
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }

  // 定义到vm对应属性中
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

createComputedGetter

createComputedGetter 函数应该是精华之处了,这边我们能看到其和 watcher 的联系,以及方法 evaluatedepend 的使用,而这也是实现计算属性缓存的原理。

function createComputedGetter (key) {
  return function computedGetter () {
    // 获取我们在函数initComputed中设置的watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]

    if (watcher) {
      // 我们之前说过computed watcher设置了lazy=true
      // 这样同时dirty也会设置为true
      if (watcher.dirty) {
        // 正常的watcher在实例化的时候就会调用get()进行依赖收集及获取值
        // 而计算属性需要在属性的get函数中主动计算值
        watcher.evaluate()
      }

      // 这里是实现computed数据驱动的原理
      // 我们举个例子 name() {return this.firtName + this.lastName}
      // 在渲染watcher中因为会调用到 this.name 就会走到当前函数
      // 在当前函数中 Dep.target 则会指向渲染 watcher
      // 而在计算属性new Watcher的时候
      // 我们说过其会收集对应的dep数组也就是firtName和lastName对应的dep
      // 此时调用watcher.depend()则会将收集的dep分别订阅当前的渲染watcher
      // 所以当触发firtName或者lastName的时候,就会触发dep.notify进而通知渲染watcher更新
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

computed缓存原理

上面我们解析了计算属性依赖于其它属性更新的原理,我们再来分析分析其缓存实现

缓存原理的实现其实在于 dirty 属性的运用。

if (watcher.dirty) {
  // 正常的watcher在实例化的时候就会调用get()进行依赖收集及获取值
  // 而计算属性需要在属性的get函数中主动计算值
  watcher.evaluate()
}

如果访问计算属性的 get 函数,会进行 watcher.dirty 的判断,如果为 true 才会调用 evaluate 获取新值,否则则返回旧值,那么他是如何判断值更新呢?

update () {
  // 和普通watcher不同的是
  // 依赖的值更新时不会调用run函数
  // 而是仅仅将dirty设置true来表明值已更新
  // 这样就不会调用get()进行更新
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

在实际使用到计算属性的访问函数 get 中,才会通过 dirty 判断进入 evaluate

evaluate () {
  // 在此才会调用get更新value 同时将dirty设置为fasle表明值已经更新
  this.value = this.get()
  this.dirty = false
}

watch实现

我们再来看看 watch 选项的实现,会比计算属性简单许多

initWatch

同样从入口开始 initState -> initWatch

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    // 数组处理
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      // 创建watcher
      createWatcher(vm, key, handler)
    }
  }
}

createWatcher

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 处理兼容不同的写法
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // 最终调用vm.$watch
  return vm.$watch(expOrFn, handler, options)
}

$watch

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    // 将user参数设置为true 用于标志开发者watcher
    options.user = true
    // 正常的实例化watcher
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // 一个值得注意的参数
    // 如果配置了immediate则在此时(vue初始化)进行调用回调cb
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      // 为啥这边要进行pushTarget呢
      // 而且target为undefined
      // 因为当前可能处于其它watcher实例化当中,例如渲染watcher
      // 如果这边不推入其它watcher得话会导致cb中得某些属性dep添加了渲染watcher
      // 则可能引起没必要得渲染watcher get执行
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }

    // 移除deps解除订阅
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}

异步watcher

Watcherupdate 中有这么个判断

update () {
  // lazy用于computed
  if (this.lazy) {
  } else if (this.sync) {
  // 同步watcher
    this.run()
  // 异步watcher
  } else {
    queueWatcher(this)
  }
}

看来只有同步 watcher 才会直接调用 run 进行更新。而异步 watcher 则会调用 queueWatcher,实际上大部分 watcher 都是异步 watcher,因为 sync 默认值就是 false

queueWatcher

那么我们主要来研究 queueWatcher 是怎么个实现,watcher 更新函数是以怎么样一个排队机制来进行更新的。

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 通过watcherID判断防止重复执行
  // 这里也是设计为异步的一个重要原因吧
  // 开发中肯定会存在某些修改触发同一个渲染watcher的情况
  // 通过异步队列则可以很好的防止重复,大大优化效率
  if (has[id] == null) {
    has[id] = true
    // flushing表明当前queue正在执行更新
    if (!flushing) {
      // 如果不是正在更新,则推入队列即可
      queue.push(watcher)
    } else {
      // 如果正在更新,则要对比watcherID的大小 将watcher推入正确位置
      // 这种情况是由于watcher的回调cb又通知了新的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

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

      // 使用nextTick来实现异步执行 
      // 其中执行函数flushSchedulerQueue通过回调方式传递、
      // nextTick的原理以后分析 知道它是将函数添加到异步队列中就行了
      nextTick(flushSchedulerQueue)
    }
  }
}

flushSchedulerQueue

我们再来看看实际更新函数 flushSchedulerQueue

function flushSchedulerQueue () {
  // 这边会设置flushing=true
  // 表明当前正在执行更新
  flushing = true
  let watcher, id

  // 注意这边会将watcher进行排序
  // 和上面的排序其实是对应的,正因为上面的queueWatcher在排序之后
  // 所以才需要对比watcherID插入特定位置  
  // 这边的排序主要为了处理
  // 1. 父组件的更新总是应该先于子组件
  // 2. userWatcher总是应该先于渲染watcher
  // 3. 如果父组件已经销毁,其实不再需要执行更新
  queue.sort((a, b) => a.id - b.id)

  // 这边要注意queue.length不会通过len=queue.length的方式缓存
  // 因为queue的长度其实是实时变化的,和前面说的原因一样
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // before执行
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    // 重置has[id] 不然下一轮不会添加到队列了
    has[id] = null
    // 执行更新函数
    watcher.run()
  }

  // keepAlive组件
  const activatedQueue = activatedChildren.slice()
  // 用于获取普通组件 通过watcher.vm
  const updatedQueue = queue.slice()

  // 重置参数 见下文
  resetSchedulerState() 

  // 触发组件update及activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
}

resetSchedulerState

正常的重置队列参数

function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  waiting = flushing = false
}

总结

本文主要分析了 Watcher 的实现原理及与其相关的 computedwatch 选项的实现。同时分析了 watcher 的更新函数是如何添加到异步队列中及其执行机制。内容实际上是比较多的,可能有些地方没说明白,希望多多理解。good good staduy day day up