3 / 26 看完这篇你一定懂computed的原理

前面的话

前端日问,巩固基础,不打烊!!!

解答

如有错误欢迎指出,感谢!!!

提出问题

提出几个问题:

  • computed 是如何初始化的?
  • 为何data值的变化computed会重新计算?
  • 为什么computed值是缓存的呢?

想了解computed的原理,你需要了解Vue的响应式原理,了解computed其实就是一个惰性watcher

下面一一解答这几个问题。

Watcher 的实现

给出watcher的实现,方便下文好看。

//去重 防止重复收集
let uid = 0
class Watcher{
	constructor(vm,expOrFn,cb,options){
		//传进来的对象 例如Vue
		this.vm = vm
		if (options) {
	      this.deep = !!options.deep
	      this.user = !!options.user
	      this.lazy = !!options.lazy
	    }else{
	    	this.deep = this.user = this.lazy = false
	    }
	    this.dirty = this.lazy
		//在Vue中cb是更新视图的核心,调用diff并更新视图的过程
		this.cb = cb
		this.id = ++uid
		this.deps = []
	    this.newDeps = []
	    this.depIds = new Set()
	    this.newDepIds = new Set()
		if (typeof expOrFn === 'function') {
			//data依赖收集走此处
	      	this.getter = expOrFn
	    } else {
	    	//watch依赖走此处
	      	this.getter = this.parsePath(expOrFn)
	    }
		//设置Dep.target的值,依赖收集时的watcher对象
		this.value = this.lazy ? undefined : this.get()
	}

	get(){
		//设置Dep.target值,用以依赖收集
	    pushTarget(this)
	    const vm = this.vm
	    //此处会进行依赖收集 会调用data数据的 get
	    let value = this.getter.call(vm, vm)
	    popTarget()
	    return value
	}

	//添加依赖
  	addDep (dep) {
  		//去重
  		const id = dep.id
	    if (!this.newDepIds.has(id)) {
	      	this.newDepIds.add(id)
	      	this.newDeps.push(dep)
	      	if (!this.depIds.has(id)) {
	      		//收集watcher 每次data数据 set
	      		//时会遍历收集的watcher依赖进行相应视图更新或执行watch监听函数等操作
	        	dep.addSub(this)
	      	}
	    }
  	}

  	//更新
  	update () {
  		if (this.lazy) {
      		this.dirty = true
    	}else{
    		this.run()
    	}
	}

	//更新视图
	run(){
		console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
		const value = this.get()
		const oldValue = this.value
        this.value = value
		if (this.user) {
			//watch 监听走此处
            this.cb.call(this.vm, value, oldValue)
        }else{
        	//data 监听走此处
        	//这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
			this.cb.call(this.vm, value, oldValue)
        }
	}

    //如果计算熟悉依赖的data值发生变化时会调用
    //案例中 当data.name值发生变化时会执行此方法
	evaluate () {
	    this.value = this.get()
	    this.dirty = false
	}
	//收集依赖
	depend () {
	    let i = this.deps.length
	    while (i--) {
	      this.deps[i].depend()
	    }
	}

	// 此方法获得每个watch中key在data中对应的value值
	//使用split('.')是为了得到 像'a.b.c' 这样的监听值
	parsePath (path){
		const bailRE = /[^w.$]/
	  if (bailRE.test(path)) return
	  	const segments = path.split('.')
	  	return function (obj) {
		    for (let i = 0; i < segments.length; i++) {
		      	if (!obj) return
		      	//此处为了兼容我的代码做了一点修改	 
		        //此处使用新获得的值覆盖传入的值 因此能够处理 'a.b.c'这样的监听方式
		        if(i==0){
		        	obj = obj.data[segments[i]]
		        }else{
		        	obj = obj[segments[i]]
		        }
		    }
		    return obj
		 }
	}
}
computed 的实现

在Vue响应式原理这篇文章已经重点提了data数据的初始化(即initDate),computed的初始化是在其后面定义的:(为什么一定要在data数据的后面初始化,这也是有原因的,下面这接着说)

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函数的定义:

//空函数
const noop = ()=>{}
// computed初始化的Watcher传入lazy: true就会触发Watcher中的dirty值为true
const computedWatcherOptions = { lazy: true }
//Object.defineProperty 默认value参数
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

// 初始化computed
class initComputed {
	constructor(vm, computed){
		//新建存储watcher对象,挂载在vm对象执行
		const watchers = vm._computedWatchers = Object.create(null)
		//遍历computed
		for (const key in computed) {
		    const userDef = computed[key]
		    //getter值为computed中key的监听函数或对象的get值
		    let getter = typeof userDef === 'function' ? userDef : userDef.get
		    //新建computed的 watcher
		    watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
		    if (!(key in vm)) {
		      	/*定义计算属性*/
		      	this.defineComputed(vm, key, userDef)
		    }
		}
	}
    //把计算属性的key挂载到vm对象下,并使用Object.defineProperty进行处理
    //因此调用vm.somecomputed 就会触发get函数
	defineComputed (target, key, userDef) {
	  if (typeof userDef === 'function') {
	    sharedPropertyDefinition.get = this.createComputedGetter(key)
	    sharedPropertyDefinition.set = noop
	  } else {
	    sharedPropertyDefinition.get = userDef.get
	      ? userDef.cache !== false
	        ? this.createComputedGetter(key)
	        : userDef.get
	      : noop
	      //如果有设置set方法则直接使用,否则赋值空函数
	    	sharedPropertyDefinition.set = userDef.set
	      	? userDef.set
	      	: noop
	  }
	  Object.defineProperty(target, key, sharedPropertyDefinition)
	}

	//计算属性的getter 获取计算属性的值时会调用
	createComputedGetter (key) {
	  return function computedGetter () {
	  	//获取到相应的watcher
	    const watcher = this._computedWatchers && this._computedWatchers[key]
	    if (watcher) {
	    	//watcher.dirty 参数决定了计算属性值是否需要重新计算,默认值为true,即第一次时会调用一次
	      	if (watcher.dirty) {
	      		/*每次执行之后watcher.dirty会设置为false,只要依赖的data值改变时才会触发
	      		watcher.dirty为true,从而获取值时从新计算*/
	        	watcher.evaluate()
	      	}
	      	//获取依赖
	      	if (Dep.target) {
	        	watcher.depend()
	      	}
	      	//返回计算属性的值
	      	return watcher.value
	    }
	  }
	}
}

开头的参数很重要。

初始化大致流程:

  • 新建存储watcher对象的数组,并挂载在vm_computedWatchers属性上。
  • 遍历整个computed上的属性key
    • 获取key上的监听函数(赋给getter)

    • 创建watcher (即每一个computed属性都是一个watcher)

    • 调用defineComputed函数 :将每一个计算属性key挂载到vm上,并使用Object.defineProperty进行处理。

      具体看一看defineComputed函数做了什么:

      既然要是用Object.defineProperty函数将每个计算属性key挂载到vm上,需要三个值:挂载目标 vm挂载对象 key设置key的value对象。前面两个已经具备,只差value对象了。还记得开头的参数么?这个value对象就是开头设置的sharedPropertyDefinition对象

      sharedPropertyDefinition对象的get属性刚开始是空函数,在这里我们调用createComputedGetter函数来设置get属性。

      具体看一看createComputedGetter 函数做了什么:

      它返回了一个函数给sharedPropertyDefinition.get. 其内部:根据计算属性key找到对应的watcher。(因为计算属性watcher创建时,会传入computedWatcherOptions 对象,这个对象里面定义了lazy: true,导致了计算属性watcher的lazy为true,dirty值初始时为lazy的值。)

      找到对应的watcher后,会执行这段代码:

      	if (watcher.dirty) {
      	      		/*每次执行之后watcher.dirty会设置为false,
      	      		只要依赖的data值改变时才会触发
      	      		watcher.dirty为true,从而获取值时从新计算*/
      	        	watcher.evaluate()
      	    }
      

      watcher.evaluate()就是本文的核心: 初始时,就会执行这个函数

       //如果计算熟悉依赖的data值发生变化时会调用
          //案例中 当data.name值发生变化时会执行此方法
      	evaluate () {
      	    this.value = this.get()
      	    this.dirty = false
      	}
      

      了解响应式原理的应该知道wacther.get()函数的用途:就是绑定Dep.target 为当前的watcher。并且进行data数据的依赖收集。

      看代码: 调用watcher.get会触发data数据的getter,进行依赖收集,收集这个watcher。前面讲过data里面的数据会比computed里面的数据先初始化,就是这个原因。

      get(){
      		//设置Dep.target值,用以依赖收集
      	    pushTarget(this)
      	    const vm = this.vm
      	    //此处会进行依赖收集 会调用data数据的 get
      	    let value = this.getter.call(vm, vm)
      	    popTarget()
      	    return value
      	}
      

到此为止,上面的问题应该有答案了吧! 计算属性的值,是依赖于其他data属性的,而计算属性本质是计算watcher, 它所依赖的data属性的dep会将这个watcher加入到subs数组中,当其变化时,就会通知这个watcher改变,所以说计算属性时缓存的。

总结
  • 初始化一个computed,会为每个属性key,创建相应的watcher(将key的监听函数传入,作为watcher的getter)。

  • 挂载属性key到vm上,并用Object.defineProperty为其添加getter。

  • 第一次调用属性key时,触发watcher.evaluate --> 触发watcher.get --> watcher.getter(就是key的监听函数) --> 触发属性key所依赖的data属性的getter,进而收集这个watcher。

  • 当data属性变化时,触发其setter,通知watcher更新,执行watcher.run,进而触发diff,更新视图。

    在这里插入图片描述

通过一个computed实例来走一下上面的流程
computed:{
  sayHello() {
    return this.hello + ' ' + this.world;
  }
},

sayHello计算属性依赖两个属性:this.hellothis.world

  • 在初始化时会创建一个sayHello对应的计算属性watcher,watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)

  • 将sayHello计算属性挂载到vm上,用Object.defineProperty定义其get属性。

    初次获取sayHello属性,触发监听函数 :

    • 获取this.hello -->触发hello的getter --> hello的dep.depend收集收集sayHello的watcher.

    • 获取this.world时,触发world的getter,dep.depend收集sayHello的watcher.

  • 所以sayhello的watcher最后,会有两个dep收集它。hello与world其中任意一个更新,都会触发更新sayhello watcher的run函数

  • 最后触发diff算法,更新虚拟dom,进而更新界面

computed 与watch的区别
  • computed属性不是data中的属性值,是一个新值,初始化时使用Object.defineProperty方法挂载到vm上;而watch是监听已经存在于data中的属性
  • computed本质是一个惰性的观察者,具有缓存性,之后依赖的data值变化时,才会变化;watch没有缓存性,数据变化就更新
  • computed适合与一个数据被多个数据影响;而watch适用于一个数据影响多个数据。

参考

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值