Vue之watch、Computed源码解读

Vue之watch、Computed源码解读

1.Watch

watch 用法

watch是Vue中一个监听数据变化的一个方法

监听基本数据类型

  

{{ msg }}

 data () {
    return {
      msg: '1231'
    }
  },
 watch: {
    msg (val, oldVal) {
      console.log(val, oldVal) // 不好 1231
    }
  },
  methods: {
    changeMsg () {
      this.msg = this.msg === '哈哈哈哈' ? '好' : '不好'
    }
  }

watch 可以接收两个参数,一个是变化后的数据,一个是变化之前的数据。

监听对象

 

{{objectData.msg}}

data () {
    return {
      objectData: {
        msg: '对象'
      }
    }
  },
  watch: {
    objectData: {
      handler (val, oldVal) {
        console.log(val.msg, oldVal.msg) // 对象改变了 对象改变了
      },
      deep: true,
      immediate: true
    }
  },
  methods: {
    changObject () {
      this.objectData.msg = '对象改变了'
    }
  }

在监听对象变化的时候,加上deep这个属性即可深度监听对象数据;如果你想在页面进来时就执行watch方法,加上immediate即可。值得注意的是,设置了immediate属性的watch的执行顺序实在created 生命周期之前。

  • deep 设置为 true 用于监听对象内部值的变化
  • immediate 设置为 true 将立即以表达式的当前值触发回调

watch接收参数为数组

watch监听的属性不去设置一个方法而是接收一个数组的话,可以向当前监听的属性传递多个方法。

    

{{name}}

data () {
    return {
      name: '修改前'
    }
  },
  watch: {
    name: [
      {
        handler: function (val, oldVal) {
          console.log(val, oldVal) // 修改前 修改后
        },
        immediate: true
      },
      function (val, oldVal) {
        console.log(val, oldVal, 2) // 修改前 修改后
      }
    ]
  },
  methods: {
    changeName () {
      this.name = '修改后'
    }
  }

数组中可以接收不同形式的参数,可以是方法,也可以是一个对象。具体的书写方式和普通的watch没什么不同。

初始化watch

initState
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) // 如果有 props ,初始化 props
  if (opts.methods) initMethods(vm, opts.methods) // 如果有 methods ,初始化 methods 里面的方法
  if (opts.data) { // 如果有 data 的话,初始化,data;否则响应一个空对象
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed) // 如果有 computed ,初始化 computed
  if (opts.watch && opts.watch !== nativeWatch) { // 如果有 watch ,初始化 watch
    initWatch(vm, opts.watch)
  }
}

首先在initState初始化watch 如果有watch这个属性,就将watch传入initWatch方法中处理

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,我们可以看到initWatch会遍历watch,然后判断每个值是否是数组。如果是数组就遍历这个数组,创建多个回调函数,这块也就解释了上边watch监听的数据可以接收数组为参数原因;如果不是数组的话,就直接创建回调函数。

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

createWatcher 中会判断handler是否是对象 ,如果是对象将handler挂载到options这个属性。再将对象的handler属性提取出来;如果handler是一个字符串的话,会才Vue实例找到这个方法赋值给handler。从这里我们可以看出来。watch还可以支持字符串的写法。执行vue实例上的$watch方法

$watch
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  // 获取 Vue 实例 this
  const vm: Component = this
  if (isPlainObject(cb)) {
    // 判断如果 cb 是对象执行 createWatcher
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  // 标记为用户 watcher
  options.user = true
  // 创建用户 watcher 对象
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 判断 immediate 如果为 true
  if (options.immediate) {
    // 立即执行一次 cb 回调,并且把当前值传入
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  // 返回取消监听的方法
  return function unwatchFn () {
    watcher.teardown()
  }
}

watch去调用。在这里不再过多赘述。

$watch 会创建一个Watcher对象,这块也是涉及响应式原理,在watch中改变的数据可以进行数据的响应式变化。同时也会判断是否有immediate这个属性,如果有的话,就直接调用回调

Watcher
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array;
  newDeps: Array;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  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 {
      // expOrFn 是字符串的时候,例如 watch: { 'person.name': function... }
      // parsePath('person.name') 返回一个函数获取 person.name 的值
      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()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  /*获得getter的值并且重新进行依赖收集*/
  get () {
     /*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      /*
      执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。
      在将Dep.target设置为自身观察者实例以后,执行getter操作。
      譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c,
      那么在执行getter的时候就会触发a跟c两个数据的getter函数,
      在getter函数中即可判断Dep.target是否存在然后完成依赖收集,
      将该观察者对象放入闭包中的Dep的subs中去。
    */
      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
      /*如果存在deep,则触发每个深层对象的依赖,追踪其变化*/
      if (this.deep) {
        /*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  ... 其他方法
}

2.Computed

computed基本用法

  

{{msg}}

// 25
  data () {
    return {
      conut1: 12,
      conut2: 13
    }
  },
  computed: {
    msg () {
      return this.conut1 + this.conut2
    }
  }

Vue中我们不需要在template里面直接计算{{this.conut1 + this.conut2}},因为在模板中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量的复杂的逻辑表达式处理数据时,回对页面的可维护性造成很大的影响/ 而computed的涉及初衷也正是用于解决此类问题

computed和watch的差异

1.computed是计算一个新的属性,并将该属性挂载到vm(Vue实例)上,而watch是监听已经存在且已挂载到vm上的数据,所以用watch同样可以监听computed计算属性的变化(其它还有data、props)

2.computed的本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问computed属性,才会计算新的值,而watch则是当数据发生变化便会调用执行函数

3.从使用场景上说,computed适用一个数据被多个数据影响,而watch适用一个数据影响多个数据

原理分析

当你把一个普通的JavaScript对象传给Vue实例的data选项时,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setter,这些getter/setter对用户来说是不可见的,但是在内部它们让Vue追踪依赖,在属性被访问和修改时通知变化,每个组件都有相应的watcher实例对象,他会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。

Vue 响应系统,其核心有三点:observewatcherdep

  1. observe:遍历 data 中的属性,使用 Object.defineProperty 的 get/set 方法对其进行数据劫持;
  2. dep:每个属性拥有自己的消息订阅器 dep,用于存放所有订阅了该属性的观察者对象;
  3. watcher:观察者(对象),通过 dep 实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应。

初始化computed

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // computed初始化
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

调用initComputed函数(其前后也分别初始化了initData和initWatch)并传入两个参数vm实例和opt.compuetd开发者定义的computed选项,转到initComputed函数:

const computedWatcherOptions = { computed: true }
 
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
 
  for (const key in computed) {
  // 获取计算属性的定义userDef和getter求值函数
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        'Getter is missing for computed property "${key}".',
        vm
      )
    }
 
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
 
    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn('The computed property "${key}" is already defined in data.', vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn('The computed property "${key}" is already defined as a prop.', vm)
      }
    }
  }
}
  1. 获取计算属性的定义 userDefgetter 求值函数

    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    
    

    定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加set和get方法的对象形式,所以这里首先获取计算属性的定义userDef,再根据userDef的类型获取相应的getter求值函数。

2.计算属性的观察者 `watcher` 和消息订阅器 `dep`
watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    computedWatcherOptions
)

这里的watcher也就是vm._computedWatcher对象的引用,存放了每个计算属性的观察者watcher实例,,Watcher 构造函数在实例化时传入了 4 个参数:vm 实例、getter求值函数、noop 空函数、computedWatcherOptions 常量对象(在这里提供给 Watcher 一个标识 {computed:true} 项,表明这是一个计算属性而不是非计算属性的观察者。

watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

3.defineComputed 定义计算属性

if (!(key in vm)) {
  defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
  if (key in vm.$data) {
    warn('The computed property "${key}" is already defined in data.', vm)
  } else if (vm.$options.props && key in vm.$options.props) {
    warn('The computed property "${key}" is already defined as a prop.', vm)
  }
}

因为 computed 属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性,defineComputed 传入了三个参数:vm实例、计算属性的 key 以及 userDef 计算属性的定义(对象或函数)。 然后继续找到 defineComputed 定义处:

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        'Computed property "${key}" was assigned to but it has no setter.',
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

在这段代码的最后调用了原生 Object.defineProperty 方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition,初始化为:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

随后根据 Object.defineProperty 前面的代码可以看到 sharedPropertyDefinitionget/set 方法在经过 userDefshouldCache 等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinitionget 函数也就是 createComputedGetter(key) 的结果,我们找到 createComputedGetter 函数调用结果并最终改写 sharedPropertyDefinition 大致呈现如下:

sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
            watcher.depend()
            return watcher.evaluate()
        }
    },
    set: userDef.set || noop
}

当计算属性被调用时便会执行 get 访问函数,从而关联上观察者对象 watcher 然后执行 wather.depend() 收集依赖和 watcher.evaluate() 计算求值。

computed总结

1.在组件初始化的时候,computed和data会分别建立各自的响应系统,observer遍历data中每个属性设置get/set数据拦截

2.初始化computed会调用initComputed函数

1.注册一个watcher实例,并在内实例化一个Dep消息订阅器用作后续手机依赖(比如渲染函数的watcher或者其他观察该计算属性变化的watcher)

2.调用计算属性时会触发其objecet.defineProperty的get访问器函数

3.调用watcher.depend()方法向自身的消息订阅器dep的subs中添加其他属性的watcher

4.调用watcher的evaluate方法(进而调用watcher)的get方法让自身成为其他watcher的消息订阅器的订阅者,首先将watcher赋给Dep.target,然后执行getter求值函数,当访问求值函数里面的属性(比如来自 dataprops 或其他 computed)时,会同样触发它们的 get 访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并返回求值函数结果。

5.当某个属性发生变化,触发 set 拦截函数,然后调用自身消息订阅器 depnotify 方法,遍历当前 dep 中保存着所有订阅者 wathcersubs 数组,并逐个调用 watcherupdate 方法,完成响应更新。

搬运+实践

你可能感兴趣的:(Vue之watch、Computed源码解读)