Vue2.x 源码 - computed 和 watch 的依赖收集和更新

上一篇:Vue2.x 源码 - 响应式原理

Vue 的组件对象支持计算属性 computed 和侦听属性 watch,上一篇只是统一的分析了整个响应式的过程,这一篇针对这个两个属性来详细说明一下;

computed

依赖收集
var vm = new Vue({
	data:{
		name:'xx',
		value:'11'
	},
	computed:{
		str: function(){
			return `${this.name}:${this.value}`;
		}
	}
})

1、在 Vue 实例初始化阶段的 initState 函数中,这里执行了 initComputed 方法来对 computed 进行初始化:

//空函数,什么都不做,用于初始化一些值为函数的变量
export function noop () {}
const computedWatcherOptions = { lazy: true } //缓存
//初始化computed
function initComputed (vm: Component, computed: Object) {
  // 创建一个空对象 watchers  没有原型链方法
  const watchers = vm._computedWatchers = Object.create(null)
  //是否是服务端渲染
  const isSSR = isServerRendering()
 // 循环computed 
  for (const key in computed) {
    const userDef = computed[key]
    //计算属性可能是一个function,也有可能设置了get以及set的对象。
    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) {
      // 为computed属性创建内部监视器 Watcher,保存在vm实例的_computedWatchers中
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    //当前key没有重复
    if (!(key in vm)) {
    //将computed绑定到 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 其实都是一个 Watcher ,这里有这样一句 const computedWatcherOptions = { lazy: true } 在创建 Watcher 的时候当作参数传入,这是 computed 可以缓存的重要原因;

Watcher 类的构造函数里面有这样一段代码:

this.value = this.lazy ? undefined : this.get()

lazy 存在说明这个时候创建的是 computed watcher,并且在初始化的时候,不会立刻调用 this.get() 去求值;

2、在组件挂载时,会执行 render 函数来创建一个 render watcher ,当访问到 this.str 的时候,会触发计算属性的 getter

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      //重新取值
      if (watcher.dirty) {
        watcher.evaluate()
      }
      //依赖收集
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

dirty 初始值为 true ,调用 watcher.evaluate():

evaluate () {
  this.value = this.get()
  this.dirty = false
}
get () {
  pushTarget(this)
  let value
  const vm = this.vm
  value = this.getter.call(vm, vm)
  if (this.deep) {
  traverse(value)
  }
  popTarget()
  this.cleanupDeps()
  }
  return value
}

重点:
2.1、将 dirty 设置为 false ,调用 Watcher 构造函数内部的 get;
2.2、在 get 里面会执行 value = this.getter.call(vm, vm),这实际上就是执行了计算属性定义的 getter 函数,在我这里就是 ${this.name}:${this.value}
2.3、通过pushTarget(this)处理,这个时候的 Dep.target 就是个 computed watcher ,由于 name 和 value 是响应式的,又会触发他们的 getter 方法,在 getter 中会把这个computed watcher 作为依赖收集起来,保存到 name 和 value 的 dep 中;

数据之间的依赖收集已经结束了,下面手动调用 watcher.depend() 再次收集一次 Dep.target,于是 data 又收集到 恢复了的页面watcher,这样就完成了计算属性的依赖收集。

更新

一旦计算属性依赖的数据发生变化,就会触发对应的 setter,通知所有的订阅者更新;调用 Watcher 的 update 方法:

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

这个时候 computed watcher 在初始化的时候 lazy 参数就是 true,因此在更新时会设置 dirty 为 true,告诉计算属性:需要重新计算了;在读取计算属性的时候就会重新计算了。

总结

1、初始化时 computed 不会立刻求值, value 为 undefined,这个时候 Dep.target 为页面 watcher;
2、挂载的时候会调取 computed 的属性,这个时候触发计算属性的 getter 方法 createComputedGetter,第一次的时候会调用 watcher.evaluate() 来进行求值,这个时候会触发 Watcher 类的 get 方法,Dep.target 会被设置成对应的 computed watcher,旧的值会被缓存;
3、computed 计算会调用计算属性定义的 getter 函数,这里会读取 computed 所有依赖的 data,读取 data 的时候又会触发 data 的getter 方法,这个时候 computed watcher 会被 data 的依赖收集器 dep 收集起来,在计算完成之后释放之前的 Dep.target ;
4、求值完成之后紧接着就会手动执行 watcher.depend 让 data 再次收集一次 Dep.target,这个时候收集到的是页面 watcher ,用于数据变化之后通知页面更新;

注意:computed 中的数据不经过 Observer 监听,所以不存在 dep。只有 data 和 props 会通过 Observer 监听。

watch

侦听属性的初始化过程,与计算属性类似,都发生在 Vue 实例初始化阶段的 initState() 函数中,其中有一个 initWatch 函数:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    //watch的每一项是不是数组
    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
) {
  //handler存在,只有handler是纯正的对象才会返回true
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  //字符串,从组件实例上获取handler
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  //创建监听器
  return vm.$watch(expOrFn, handler, options)
}

1、获取 watch 循环遍历每一项,然后调取 createWatcher 方法;
2、 createWatcher 方法中 key 区分对象和字符串分别处理,然后为每一个属性创建监听器;

$watch 是 Vue 原型上的⽅法,它是在执⾏ stateMixin 的时候定义的:

//设置 $watch
 Vue.prototype.$watch = function (
   expOrFn: string | Function, //用户手动监听
   cb: any, //监听变化之后的回调函数
   options?: Object //参数
 ): Function {
   const vm: Component = this
   //如果cb是对象执行createWatcher,获取cb 对象里面的handler属性继续递归执行 $watch,直到cb不是对象
   if (isPlainObject(cb)) {
     return createWatcher(vm, expOrFn, cb, options)
   }
   options = options || {}
   //标记为用户 watcher,用于给data从新赋值时走watch的监听函数
   options.user = true
   //为expOrFn添加watcher
   const watcher = new Watcher(vm, expOrFn, cb, options)
   //immediate==true 立即执行cb回调
   if (options.immediate) {
     try {
       cb.call(vm, watcher.value)
     } catch (error) {
       handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
     }
   }
   //取消观察函数
   return function unwatchFn () {
     //从所有依赖项的订阅者列表中删除当前watcher
     watcher.teardown()
   }
 }

判断 cb 是对象执行 createWatcher 方法,然后为 expOrFn 创建 watcher 实例,一但 watch 的数据变化了就会执行 watcher 的 run 方法,执行 cb 回调函数,最后会移除 watcher;

所以本质上侦听属性也是基于 Watcher 实现的,它是⼀个 user watcher ,具体分析看下面;

Watcher

从 Watcher 的参数来区分,Watcher 可以分为4种类型: deep、user、computed、sync;

deep watcher

通常,如果我们想对⼀下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:

var vm = new Vue({ 
	data() { 
		a: {
			b: 1 
		}
	 },
	 watch: {
	 	a: {
	 		handler(newVal) {
	 			 console.log(newVal)
	 		 } 
	 	}
	 } 
})
vm.a.b = 2

这个时候是不会 log 任何数据的,因为我们是 watch 了 a 对象,只触发了 a 的 getter,并没有触发 a.b 的 getter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了;

⽽我们只需要对代码做稍稍修改,就可以观测到这个变化了

watch: { 
	a: {
		deep: true, 
			handler(newVal) { 
				console.log(newVal) 
			}
	}
 }

这样就创建了⼀个 deep watcher 了,在 watcher 执⾏ get 求值的过程中有⼀段逻辑:

if (this.deep) {
        traverse(value)
      }

在对 watch 的表达式或者函数求值后,会调⽤ traverse 函数,它的定义在 src/core/observer/traverse.js 中:

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 ((!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
    while (i--) _traverse(val[keys[i]], seen)
  }
}

traverse 的逻辑也很简单,它实际上就是对⼀个对象做深层递归遍历,因为遍历过程中就是对⼀个 ⼦对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher ,这个函数实现还有⼀个⼩的优化,遍历过程中会把⼦响应式对象通过它们的 dep id 记 录到 seenObjects ,避免以后重复访问。

在使用的时候需要注意一下,按照实际情况开启 deep 模式,因为 traverse 函数,会有⼀定的性能开销;

user watcher

通过 vm.$watch 创建的 watcher 是⼀个 user watcher ,其实它的功能很简 单,在对 watcher 求值以及在执⾏回调函数的时候,会处理⼀下错误,如下:

//watcher get方法里面
if (this.user) {
 handleError(e, vm, `getter for watcher "${this.expression}"`)
  } else {
    throw e
  }
 //watcher run
if (this.user) {
   try {
      this.cb.call(this.vm, value, oldValue)
    } catch (e) {
      handleError(e, this.vm, `callback for watcher "${this.expression}"`)
    }
  } else {
    this.cb.call(this.vm, value, oldValue)
  }

除了对错误的处理,就是直接执行 cb 函数,这个 cb 函数就是自定义的回调函数;

computed watcher

computed watcher 就是为计算属性量⾝定制的,可以参考上面 computed 依赖收集和更新;

sync watcher

在 Watcher 的 update 方法中:

if (this.sync) {
      this.run()
    }

当响应式数据发送变化后,触发了 watcher.update() , 只是把这个 watcher 推送到⼀个队列中,在 nextTick 后才会真正执⾏ watcher 的回调函数。 ⽽⼀旦我们设置了 sync ,就可以在当前 Tick 中同步执⾏ watcher 的回调函数。

只有当我们需要 watch 的值的变化到执⾏ watcher 的回调函数是⼀个同步过程的时候才会去设置该 属性为 true。

区别

1、computed

computed 是响应式的
computed 可以缓存
依赖的 data 变了,computed 才会更新
属性名不能和 data 、props重名

2、watch

可以设置 immediate 立即执行,deep 深度监听、sync 同步执行
watch值变了然后会触发对应的 data 更新
监听的属性必须是 data 、props 里面已经定义的

下一篇:Vue2.x 源码 - 生命周期

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值