Life and freedom Ge Lin ——— Draw by Razzh

Vue2 watch 实现原理

Oct 3 · 12 min

本文基于 Vue 2.16.14 版本

watch Options 用来监听一个响应式数据的变化,并触发回调函数,适合异步任务和开销较大的操作。

#核心源码分析

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 {
      createWatcher(vm, key, handler)
    }
  }
}

这里对 watch 对象做遍历,拿到每一个 handler,因为 Vue 是支持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,调用 createWatcher 方法,否则直接调用 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]
  }
  return vm.$watch(expOrFn, handler, options)
}

handler 可以有三种类型:

  • handler 为对象时,取出对象中的 handleroptions 选项,有 immeidatedeep 选项
  • handler 为字符串时,去 vm 组件实例上拿到 handler 回调函数
  • handler 为函数时,默认传入 $watch 方法中

最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watchVue 原型上的方法,它是在执行 stateMixin 的时候定义的:

  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 || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

首先 $watch,是一个 user watcher,它是能被用户直接调用的,所以在开始的时候,它会使用 isPlainObject 函数来判断用户传递的 cb 是否是对象,再利用 createWatcher 来处理对象属性。

之后 options.user = true,也正是之前提到的 user watcher 出处,之后实例化 Watcher(vm, expOrFn, cb, options) 类,也是 watch 选项的关键步骤,(Watcher 方法相对复杂,这里只提对 Watcher 类中的关键方法:

class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.get()
  }
}

这里的 expOrFn 是一个对象 key, 是一个字符串 path,所以会走 else 分支的 parsePath 方法:

const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

parsePath 方法遍历 path,比如 'car.brand',会被存进一个闭包环境下的 segments 的数组里面: ['car', 'barnd'],并返回一个匿名函数,它会在 Watcher 中的 this.value = this.get() 中调用,并保存 value 的值。

匿名函数的核心思想是通过遍历 segments 中的 path,在取值时: obj[segments[i]],触发该属性的 getter 函数收集依赖

那么,我们在 watch 选项中传入的回调函数何时触发?当视图更新时Watcher 实例上的 run 方法最终会被调用:

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

他会再次调用 const value = this.get(),再次触发 getter 方法(执行闭包匿名函数。之后返回最新的 value

之后会进行 if 判断: value !== this.value,其中 this.value 保存的是视图更新前的值,所以我们需要比较更新前后的 value 是否发生变化,由于我们是 user watcher,所以会走第一个分支,调用 invokeWithErrorHandling 方法,在这个方法中就会执行 cb 回调函数,执行我们定义函数的一些逻辑。

#immediate 选项

开启 immediate 时,watch 会在初始化的时候立即执行回调函数,在 $watch 中有这样一段代码:

Vue.prototype.$watch = function(
  expOrFn: string | Function,
  cb: any,
  options?: Object
) {
  // ...ignore
  if (options.immediate) {
    const info = `callback for immediate watcher "${watcher.expression}"`
    pushTarget()
    invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
    popTarget()
  }
  // ...ignore
}

如果 immediate 存在,会立即执行这个回调。这里面的 pushTargetpopTarget 方法是为了让我们在执行回调的时候能够收集响应式数据的依赖

#deep 选项

watch 选项在 new Watcher 的时候,会执行 parsePath 方法,用它来收集依赖,但它并不能深度收集对象中的引用类型的依赖,所以我们需要对 watch 监听的属性进行深度递归遍历

我们只需要在收集 Watcher 的过程中,深度遍历一遍当前对象,触发所有属性的 get ,然后每一个属性就会收集到当前 Watcher ,这样改变对象内部的值的时候,就会触发该 Watcher ,从而执行回调函数。

遍历对象的话,首先就需要一个 traverse 函数。

import { isObject } from "./util"
 
const seenObjects = new Set()
 
/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
export function traverse(val) {
    _traverse(val, seenObjects)
    seenObjects.clear()
}
 
function _traverse(val, seen) {
    let i, keys
    const isA = Array.isArray(val)
    if ((!isA && !isObject(val)) || Object.isFrozen(val)) {
        return
    }
    if (val.__ob__) {
        const depId = val.__ob__.dep.id
        if (seen.has(depId)) {
            return
        }
        seen.add(depId)
    }
    // 判断是数组还是对象
    if (isA) {
        i = val.length
        while (i--) _traverse(val[i], seen)
    } else {
        keys = Object.keys(val)
        i = keys.length
       // 遍历对象的每一个 key
        while (i--) _traverse(val[keys[i]], seen)
    }
}

它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher

之后在 Watcher类中新增 deep 选项和 traverse 方法

class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    /**************** 新增 ************ */
    if (options) {
      this.deep = options.deep
    }
    /**************** 新增 ************ */
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.get()
  }
 
  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 {
    /**************** 新增 ************ */
      if (this.deep) {
        traverse(value)
      }
    /**************** 新增 ************ */
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
}

在执行完 traverse 方法后,收集了 watch 目标数据的所有依赖,在下一次数据变更时,回调也会跟着触发。

#最小化实现

可以在这里的 demo 看到 watch 最小化实现

#参考

Vue.js 技术揭秘 计算属性 vs 侦听属性
Vue2 剥丝抽茧-响应式系统之 watch
Vue2 剥丝抽茧-响应式系统之 watch2

浙ICP备2024129591号-1
春秋(Live) - 张敬轩
--:-- / --:--
  1. 1春秋(Live)张敬轩
  2. 2不吐不快(live)张敬轩
  3. 3男孩最痛(live)张敬轩
  4. 4粤语残片(live)陈奕迅
  5. 5几分之几(live)卢广仲
  6. 6地球很危险古巨基
  7. 7樱花树下(live)张敬轩

春秋 (Live) - 张敬轩 (Hins Cheung)

词:林夕

曲:Edmond Tsang

那夜谁将酒喝掉

因此我讲得多了

然后你摇着我手拒绝我

动人像友情深了

我没权终止见面

只因你友善依然

仍用接近甜蜜那种字眼通电

没人应该 怨地怨天

得到这结局

难道怪罪神没有更伪善的祝福

我没有为你伤春悲秋不配有憾事

你没有共我踏过万里

不够剧情延续故事

头发未染霜 着凉亦错在我幼稚

应快活像个天使

有没有运气再扮弱者 玩失意

有没有道理为你落发

必须得到世人同意

心灰得极可耻 心伤得无新意

那一线眼泪 欠大志

爱若能堪称伟大 再难挨照样开怀

如令你发现为你而活到失败

令人不安 我品性坏

我没有为你伤春悲秋不配有憾事

你没有共我踏过万里

不够剧情延续故事

头发未染霜 着凉亦错在我幼稚

应快活像个天使

有没有运气再扮弱者玩失意

有没有道理为你落发

必须得到世人同意

心灰得极可耻 心伤得无新意

那一线眼泪 欠大志 太没意思

若自觉这叫痛苦未免过份容易

我没有被你改写一生怎配有心事

我没有被你害过恨过

写成情史 变废纸

春秋只转载要事

如果爱你欠意义

这眼泪 无从安置

我没有运气放大自私的失意

更没有道理在这日

你得到真爱制造恨意

想心酸 还可以 想心底 留根刺

至少要见面上万次