深入理解Vue的watch与computed原理

2,252 阅读10分钟

watch与computed原理

侦听器watch与计算属性computed对于我这样的Vue初学者,很容易就弄混淆了,感觉他俩差不多呀,近期扣了这部分原理,总结了这篇文章。

第一次写这么长的文章,有点乱,有问题希望大家帮我指出来~~

watch

分三步进行:

  • initWatch
  • mount
  • update

initWatch

Vue在initState时,会对watch也进行初始化

// /src/core/instance/state.js
export function initState (vm: Component) {
  // .....
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)   // <== 调用initWatch
  }
}

watch属性可有多种类型 string | object | array | function,在initWatch函数中主要处理了数组的情况,

// /src/core/instance/state.js
function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]  // 拿到对应侦听的键值,类型为 string | object | array | function
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {  // 是数组就遍历每一项
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler) // 调用createWatcher来处理watch[key]的其余情况,并创建用户watcher
    }
  }
}

createWatcher主要处理handler类型为object、string的情况,最后调用vm.$watch创建watcher

vm.$watch的参数:

  • expOrFn:watch的键key eg: 'a' | 'a.c'
  • handler:key对应的函数 eg:function(){....}
  • options:一个对象,存储handler为对象的配置信息 eg:{handler: function(){}, deep: true, immediate: true}
// /src/core/instance/state.js
function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {  // handler为对象,
    options = handler // handler赋给options
    handler = handler.handler // handler.handler取出对应的函数
  }
  // handler为字符串,在当前vm实例的methods查找名为handler的方法,赋给handler
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // expOrFn:watch的健key  eg: 'a' | 'a.c'
  // handler:key对应的函数 eg:function(){....}
  // options:一个对象,存储handler为对象的配置信息 eg:{handler: function(){}, deep: true, immediate: true}
  return vm.$watch(expOrFn, handler, options)
}

vm.$watch主要就是创建一个用户watcher

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {  // 查看 cb是否为对象,是就调用createWatcher处理
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true // 标识是用户自己写的watcher
    // vm.$watch('msg',()=>{})  vm  'msg'  ()=>{} {deep,immediate, user}
    const watcher = new Watcher(vm, expOrFn, cb, options) // 创建一个watcher,默认执行watcher.get()对msg取值
    if (options.immediate) {  // 需要立即执行?
      try {
        cb.call(vm, watcher.value)  // watcher.value:msg初始值
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
  }

看看Watcher构造函数做了什么

  • 判断expOrFn是否为函数,侦听器的key一般为字符串表达式,所以执行parsePath(expOrFn)将表达式转为函数function(){return vm.xxx},主要作用就是取值,然后将函数赋给watcher.getter

  • 执行watcher.get(),将当前watcher添加到全局,接着执行watcher.gettervm.xxx进行取值,并让vm.xxx的dep收集当前的用户watcher,目前vm.xxx就一个用户watcher。接着有watcher.deep判断vm.xxx是否需要深度代理watcher,这种情况一般vm.xxx是一个内嵌有多层的对象(vm.xxx:{a:{c: 1}}),或数组的情况,执行watcher.get没有对内嵌的对象进行依赖搜集,所以用户可以配置deep来对内嵌的对象(或数组)也进行依赖搜集。

  • 那看看deep原理

    • export function traverse (val: any) {
        _traverse(val, seenObjects)
        seenObjects.clear()
      }
      
      function _traverse (val: any, seen: SimpleSet) {
        let i, keys
        const isA = Array.isArray(val)
        if (val.__ob__) {  // 初始化观察data属性时,data每个属性都被递归观察添加了一个__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
          while (i--) _traverse(val[keys[i]], seen)
        }
      }
      
    • 当前Dep.target为用户watcher,并且vm.data在initData过程中,每一个属性都被递归响应式观察了,就是说对象或数组都被递归观测了,即每个属性都会有个dep实例。重点来了,deep原理就是对当前对象或数组的每一项递归取值,在调用getter取值过程中,每一项都会进行dep的依赖搜集,所以改变vm.xxx的内嵌对象属性时也会触发搜集的用户watcher

  • popTarget()执行,在全局下清除当前watcher,最后返回value值给watcher.value

// //src/core/observer/watcher.js
export default class Watcher {
  // watch     vm   'xxx'   ()=>{....}  {immidate, deep, user}
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    this.deep = !!options.deep
    this.user = !!options.user
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn) // 会将表达式转成函数  msg function(){return vm.msg}
    }
    this.value = this.get() // 用户watcher默认会执行get
  }

  get () {
    pushTarget(this) // 将watcher 放到全局上  Dep.target = watcher
    let value
    const vm = this.vm
    try { // 对msg 进行取值  {a:{a:1}}
      value = this.getter.call(vm, vm) // user watcher:取值 会进行依赖收集
    } catch (e) {
		// ......
    } finally {
      if (this.deep) { // deep:true
        traverse(value)
      }
      popTarget()
    }
    return value
  }
}

此时用户watcher创建完成,就是对侦听的data属性进行取值和依赖(user watcher)搜集

再回到vm.$watch里接着向下执行,查看用户是否配置了immediateimmediate为true时,执行侦听器的回调函数

if (options.immediate) {  // 需要立即执行?
  try {
    cb.call(vm, watcher.value)  // watcher.value:msg初始值
  } catch (error) {
    handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
  }
}

watch初始化完成,接着页面会挂载

mount

页面挂载过程中,每个组件都会被创建一个渲染watcher,然后模板在编译过程中对侦听的data属性进行取值,就会触发被侦听属性的getter,然后该data属性就会收集当前的渲染watcher,即当前该data属性至少搜集了两个watcher,用户watcher和渲染watcher

举个例子

........
<div>{{ msg }}</div>
........

var vm = new Vue({
  data:{
    msg: '1111'
  },
  watch:{
    msg:function(val, oldval) {
      console.log(val, oldval);
    }
  }
})

vm.msg在initWatch阶段搜集了一个用户watcher。在挂载阶段,又对msg取值,触发了msg的getter,然后msg收集了第二个watcher——当前组件的渲染watcher

msg ==> dep.subs['user watcher', 'render watcher']

update

再修改vm.msg的值

......
mounted() {
  this.msg = '2222'
}
......

触发msg的setter,修改msg值并调用dep.notify循环通知每个watcher更新,

  • 首先user watcher更新,触发user watcher的run方法

    •   run () {
          if (this.active) {
            const value = this.get()
            if ( value !== this.value ) {
              const oldValue = this.value // initWatch 取得值
              this.value = value
              if (this.user) {
                this.cb.call(this.vm, value, oldValue)
              } else {
                this.cb.call(this.vm, value, oldValue)
              }
            }
          }
        }
      
    • 调用watcher.get取新值,接着比较新值与老值,不等就调用msg对应的侦听方法

      msg: function(val, oldval) {
        console.log(val, oldval);
      }
      
  • 接着render watcher更新,就是更新组件了

watch基本上就这个原理了

computed

也分为三部分:

  • initComputed
  • mount
  • update

initComputed

initState

// /src/core/instance/state.js
export function initState (vm: Component) {
// .....
  if (opts.computed) initComputed(vm, opts.computed)  // <== initComputed
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initComputed主要做了两件事,其一就是创建计算属性watcher,其二调用defineComputed将计算属性映射到vm实例上。vm._computedWatchers存储着当前vm实例所有的computed watcher

function initComputed (vm: Component, computed: Object) {
  // watchers存储计算属性watcher
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
  // 两种写法 只读 xxx:() => {}    可读可操作:xxx:{get(){...}, set(){...}}
  for (const key in computed) {
    const userDef = computed[key] // 获取用户定义的 方法 | 对象
    const getter = typeof userDef === 'function' ? userDef : userDef.get //获取读的方法

    if (!isSSR) {
      watchers[key] = new Watcher(  // 给每个计算属性创建一个watcher
        vm,
        getter || noop, // 将用户定义的方法传入 xxx:() => {....}    get(){...}
        noop,       // ()=>{}
        computedWatcherOptions // {lazy:true}
      )
    }

    if (!(key in vm)) {   // 计算属性名不在vm.data里
      defineComputed(vm, key, userDef) // 将计算属性定义到实例上
    } else if (process.env.NODE_ENV !== 'production') {
      // ....
    }
  }
}

创建计算属性watcher

计算属性watcher的标识:{lazy: true}

export default class Watcher {
  // computed  vm|()=>{....}|()=>{}|{lazy: true}
  constructor ( vm, expOrFn, cb, options, isRenderWatcher ) {
    // .......
    this.dirty = this.lazy // 如果是计算属性默认dirty就是true
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // .....
    }
    this.value = this.lazy // 计算属性默认不执行
      ? undefined
      : this.get()
  }
}

创建watcher主要做了两件事,赋予watcher.getter函数,此函数就是用户定义的,初始化watcher.dirty为true

defineComputed

主要处理Object.defineProperty的第三个参数,确认当前计算属性的getter和setter。然后调用Object.defineProperty将计算属性映射到vm实例上。

export function defineComputed ( // vm
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()  // server端不需要缓存
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key) // 创建计算属性的getter
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {   // userDef 为对象 有getter或setter 取出
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // 将计算属性映射到vm实例上
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

createComputedGetter创建计算属性getter,当取计算属性的值时就会触发computedGetter

computedGetter主要做了什么?mount挂载阶段会详说

function createComputedGetter (key) {
  return function computedGetter () { // 取值的时候回调用此方法
    // ......
  }
}

computed初始化主要就是创建计算属性watcher、将计算属性映射到vm实例、并创建getter

mount

看个例子

<div>{{ fullName }}</div>


new Vue({
	data(){
    return {
      firstName: '111',
      lastName: '222'
    }
  },
  computed:{
    fullName() {
      return this.firstName + this.lastName;
    }
  }
})

组件的渲染watcher创建

组件挂载,首先为组件创建一个渲染watcher,添加到全局targetStack数组中,Dep.target指向当前的渲染watcher。

// /src/core/observer/dep.js
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

接着执行当前组件的更新方法初始化页面,会对fullName进行取值,也就触发计算属性fullName的getter。就是createComputedGetter创建的computedGetter

function createComputedGetter (key) {
  return function computedGetter () { // 取值的时候回调用此方法
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) { // 做了一个dirty 实现了缓存的机制
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

vm._computedWatchers获取fullName计算属性watcher,在创建watcher时,watcher.dirty被初始化为true,所以这里需要调用watcher.evaluatefullName取值,看看watcher.evaluate方法

class Watcher {
  //....
  get () {
    pushTarget(this) // 将watcher 放到全局上  Dep.target = watcher
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
    } finally {
      popTarget()
    }
    return value
  }
    
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
}

调用this.get()

  • 将当前的computed watcher push到targetStackDep.target指向computed watcher,此时targetStack存着两个watcher

    • targetStack = ['render watcher', 'computed watcher']
      
  • 执行this.getter.call(vm, vm),就是执行计算属性的方法

    • computed:{
        fullName() {
          return this.firstName + this.lastName;
        }
      }
      
    • 就是执行fullName回调,执行过程中,又会对firstNamelastName取值,触发了它俩的getter,所以firstNamelastName的dep会搜集当前Dep.target所指向的computed watcher。同样firstNamelastName的dep也会被添加到computed watcher中,此时fullName计算属性watcher保留着两个dep。

    • 返回fullName计算的值

  • this.getter.call(vm, vm)将结果返回给value。调用popTarget,删除targetStack末尾的computed watcherDep.target指向'render watcher',此时,targetStack只有一个渲染watcher

  • 返回value

到这this.get()执行完毕,watcher.value拿到返回的value;接着将watcher.dirty值改为false,表示当前已为计算属性取过值了,只要firstNamelastName值不变,以后就不需要重新计算取值,达到一个缓存的效果。

计算属性求值完成,接着执行其getter后面的代码

function createComputedGetter (key) {
  return function computedGetter () { // 取值的时候回调用此方法
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) { // 做了一个dirty 实现了缓存的机制
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

Dep.target指向渲染watcher,执行watcher.depend(),目的:给firstNamelastName的dep添加当前的渲染watcher,这样firstNamelastName值改变就会更新组件了,此时firstNamelastName的dep有两个watcher

depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

fullName的getter执行完了,也就是组件渲染获取fullName完毕,在清楚点就是渲染watcherwatcher.getter到这执行完毕。组件挂载完毕,接着执行popTarget(),清除当前渲染watcher,此时targetStack为空数组,Dep.target指向空

渲染watcher创建完毕

我在说什么鬼~~,自己有点晕了

updata

mounted() {
  this.firstName = '33333';
}

改变firstName的值,触发firstName的setter,dep.notify通知watcher更新

firstName ==> dep.subs = ['computed watcher', 'render watcher'];
  • computed watcher

    • update () {
        /* istanbul ignore else */
        if (this.lazy) { // 计算属性  依赖的数据发生变化了 会让计算属性的watcher的dirty变成true
          this.dirty = true
        } else if (this.sync) { // 同步watcher
          this.run()
        } else {
          queueWatcher(this) // 将watcher放入队列
        }
      }
      
    • 判断当前watcher是否为计算属性watcher,是就执行this.dirty = true语句,表示下次计算属性要重新取值

  • render watcher执行watcher.run()

    • get () {
        pushTarget(this) // 将watcher 放到全局上  Dep.target = watcher
        let value
        const vm = this.vm
        try {
          value = this.getter.call(vm, vm)
        } catch (e) {
        } finally {
          popTarget()
        }
        return value
      }
      run () {
          const value = this.get()   
      }
      
    • get() --> 对fullName取值 getter --> fullName的计算属性watcher.dirty为true,重新计算取值 -->获取fullName新值 --> 组件更新

computed的东西基本上就这些

总结

watch(user:true)

侦听的属性,必须是vm.data的属性。在侦听的属性创建用户watcher时,默认会执行watcher.get()对侦听属性取值,并进行依赖搜集。当侦听属性被修改后就会触发依赖更新,用户watcher主要触发侦听属性的回调;若页面引用了该属性,还会触发渲染watcher更新组件视图。

watch deep的原理:在创建用户watcher时,watcher.get()会根据用户的配置信息,判断是否需要让当前watcher.value内的属性也进行依赖搜集,dep == true那就递归循环对每一项取值,取值触发getter,所以遍历的每一项都会进行依赖搜集。

computed(lazy:true)

计算的属性不允许能在data中定义过,否则会报错,但初始化会将计算属性代理到vm实例上。

创建计算属性watcher时,默认不会执行什么操作,但在组件渲染对计算属性取值时,会触发Vue自定义的计算属性getter,对计算属性取值,取值时也会对监听的vm.data属性取值和依赖搜集,vm.data属性的计算属性watcher是此时搜集来的,同时计算属性watcher也会对vm.data属性的dep进行搜集(详细可以看源码dep.depend方法和watcher.addDep方法,主要实现依赖watcher与dep实例一多一、多对多的关系)。

vm.data属性只搜集计算属性watcher可不行呀,还得搜集组件的渲染watcher,如何呢?计算属性取完值后,计算属性watcher会被targetStack移除。接着调用计算属性的watcher.depend对搜集了当前计算属性watcher的dep,循环添加Dep.target指向的watcher,这时的watcher就是渲染watcher了。

当计算属性所依赖的vm.data的属性变化时,该属性的dep就会循环更新搜集的watcher,计算属性watcher主要就是将watcher.dirty修改为true,表示需要重新取值;接着渲染watcher更新,当对计算属性取值时,发现watcher.dirty为true,然后重新取值,将watcher.dirty为false,更新组件。

区别

1、watch监听是vm.data的一个属性,它的getter不需另做处理,而computed监听是一个新的属性,它需要被代理到vm实例上,并且getter需要重新创建

2、底层都是watcher:watch创建watcher内部默认会对属性取值,它要有老值嘛;computed watcher的创建就不会取值,只有组件渲染需要计算属性值时,才会取值

3、computed对计算属性的值有缓存处理,只要依赖的vm.data属性值不变,重复取值,不会重复计算

4、watch趋向于监听某个vm.data属性值变化后进行一些操作,而computed趋向于"我"的改变依赖其他属性值的变化

5、watch的用户watcher的更新主要是执行回调,computed的计算属性watcher更新就是将watcher.dirty修改为true,等待重新取值时计算