【vue2.x原理剖析三】侦听属性原理

144 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

前言

源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大

侦听属性的书写方式

  • 函数形式
watch:{
  sum(newVal, oldVal){
    ...
  }
}
  • 对象形式
watch:{
  sum: {
    handle(newVal, oldVal){
      ...
    }
  }
}
  • 字符串形式
methods:{
  add(){...}
},
watch:{
  sum: 'add'
}
  • 数组形式
watch:{
  sum:[{
    handle(newVal, oldVal){
      ...
    }
  }]
}

侦听属性的初始化

在初始化时,vue针对不同写法做了不同的处理,最后都调用vm.$watch去创建,在官方文档中侦听属性的写法只有函数形式一种,其余写发及参数时vm.$watch的写法,由于侦听属性也是通过vm.$watch初始化的,所以写法可以移植过来

// /src/core/instance/state.js
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)
    }
  }
}
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)
}

$watch就是watcher的实例化,传入不同的属性配置

// /src/core/instance/state.js
Vue.prototype.$watch = function ( //$watch
    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 // 是一个用户watcher
    // watcher参数分别为vue实例,侦听属性的key,侦听属性对应的函数,自定义配置
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // 立即执行 这里的pushTarget()是为保证将计算的值存在当前侦听属性的value中
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      // 立即执行handler函数,初始化顺序为props -> methods -> data -> computed -> watch,所以此时watch立即执行handler可以访问到data里的属性值
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

watcher中侦听属性相关

// /src/core/observer/state.js
export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // options
    if (options) {
      this.user = !!options.user// 是否为侦听属性
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 对类似于'a.b.c'的形式的侦听属性做处理,obj['a']['b']['c']
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
      }
    }
    // 非计算属性实例化会默认调用get方法进行取值,计算属性的实例化时候不会去调用get
    this.value = this.lazy
      ? undefined
      : 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 {
      // deep深度监听执行
      if (this.deep) {
        traverse(value)
      }
      popTarget()
    }
    return value
  }
  ...
  run () {
    if (this.active) {
      //计算新值
      const value = this.get()
      // 判断旧值与新值不相同时调用handler函数
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // 取旧值
        const oldValue = this.value
        // 赋新值,目的是下次的旧值是这次的新值
        this.value = value
        // 如果是用户watcher,则执行用户定义的侦听属性handler函数
        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)
        }
      }
    }
  }

}

深度监听

如果参数有deep: true会开启深度监听,让该对象或数组下的所有有__ob__属性(有__ob__属性说明已经被vue拦截过)的值记住当前的侦听属性watcher

// /src/core/observer/traverse.js
const seenObjects = new Set()
export function traverse (val: any) {
  _traverse(val, seenObjects)
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    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
    // 递归获取子值,触发getter,收集依赖,此时的watcher为侦听属性watcher
    while (i--) _traverse(val[keys[i]], seen)
  }
}

注意点

  • 侦听属性有三种书写方式,对象形式、函数形式、字符串形式
  • 由于初始化顺序为prop、methods、data、computed、watch,所以watch的immediate属性,立即执行可以访问到data
  • 侦听属性依赖于对数据做过劫持,所以一般数据是监听不到的

总结

对于侦听属性,有三种书写方式,在初始化过程中会针对不同的形式处理成统一的形式,如果侦听属性为例如a.b.c的形式,就会对其进行拆分(split+循环访问)组合成可访问形式,初始化时为watcher传入一个user值表示是侦听属性,同时,在初始化过程中会将当前的侦听属性watcher被收集在在侦听对象的dep中,在初次渲染访问时,目标对象dep中就会存在侦听属性watcher渲染watcher。由于在初始化data或者prop时对其做了劫持。所以当侦听的属性发生变化时,会再次进行计算值,当上次保存的值和本次值不一样时,会触发用户定义的handler函数。此外,侦听属性有两个属性值deepimmediate,表示深度监听初始化时立即执行,immediate属性在初始化侦听属性时就会立即去执行用户定义的handler函数。deep属性是在初始化侦听属性时,就会递归获取它的子值,触发子值getter,收集当前的侦听属性watcher依赖。

系列链接

【Vue2.x原理剖析一】响应式原理
【Vue2.x原理剖析二】计算属性原理
【Vue2.x原理剖析三】侦听属性原理
【Vue2.x原理剖析四】模板编译原理
【Vue2.x原理剖析五】初始渲染及更新原理
【Vue2.x原理剖析六】diff算法原理
【Vue2.x原理剖析七】组件原理