vue源码解析—computed原理

本文详细介绍了Vue中computed的初始化过程,包括创建watcher和computedGetter,以及在组件挂载时如何触发计算和依赖绑定。当data变化时,如何通知computed进行更新,并详细阐述了这一过程中的依赖管理和异步更新机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


阅读本文前题是,已经对vue数据绑定机制中的的observer和watcher原理有个大致的了解,只要知道一个watcher去get一个data时,就会建立一个互相绑定的关系,data的会把watcher存人dep.subs数组,知道都有谁在watch它,以便发生变化时通知,而watcher也会把data的dep放入自己的deps数据,知道自己依赖哪些数据。

在此基础上,本文进一步讲解一下computed的原理,整体流程图如下,可对照此图理解后面的内容:

在这里插入图片描述

1. computed初始化

在初始化组件创建组件实例时,会调用到initComputed函数,这里会初始化组件里定义的computed属性,代码如下:

// vue/src/core/instance/state.js
const computedWatcherOptions = { lazy: 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) {
    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)
      }
    }
  }
}

在for循环中,针对每一个computed属性,会做两件事:创建watcher,以及compuetdGetter(即vm.sum的getter)

1.1. 创建watcher

...
watchers[key] = new Watcher(
	vm,
	getter || noop,
    noop,
    computedWatcherOptions
)
...

getter值即为用户定义的computed属性,如本示例中的sum:

// myproject/src/views/myview.js
computed: {
	sum() {
		return this.a + this.b
	}
}

options的lazy值为true,这个值会同时传给watcher的dirty,这两个值初始化为true,决定了computed watcher和render watcher (负责监听数据变化,并更新组件视图的watcher) 在数据发生变化时,响应执行过程的不同,这个后面会讲到。

1.2. 创建computedGetter

在vm实例上为sum创建一个computedGetter,即vm.sum的getter,在initComputed中通过如下调用步骤进入到computedGetter的定义:

defineComputed -> createComputedGetter

// vue/src/core/instance/state.js
...
function initComputed (vm: Component, computed: Object) {
	...
	if (!(key in vm)) {
		defineComputed(vm, key, userDef)
	}
	...
}

export function defineComputed (target: any, key: string, userDef: Object | Function) {
	...
	createComputedGetter(key)
	...
}

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

以上computed初始化工作就完成了,此时这个function computedGetter () {…}里的内容还并未执行,这时watcher.value还并没有被计算得到值,而没有去计算值,也就没有去调用data的getter去取data,所以此时computed watcher和data也还没有绑定依赖,这个值的变化要关注下面两个过程:

  1. 当data发生变化时,watcher.value依然不会马上计算出值,只会去设置watcher.dirty为true,标识watcher.value的是个脏值,已经过时了,别人再来取我时,我需要重新计算。前面讲到computed watcher初始化时dirty为true,所以初始化完成后,watcher.value就已经是一个脏值,因为还没有执行过计算得到值。
  2. 组件mount时,创建render watcher去初次get vm.sum这个computed属性时,执行computedGetter的代码,才会通过watcher.evaluate()去计算得到watcher.value的值,并把dirty设置为false,而evaluate的过程会去取data,就会让computed watcher和所需的data绑定依赖。

这里还要关注一个问题,就是data的变化会通知computed watcher去设置dirty,就是上面第1步,但是render watcher是怎么知道数据变化,并去取vm.sum,即上面第2步。
这是借助上面computedGetter函数里的watcher.depend(),以computed watcher为桥梁,让render watcher和data之间做了一个依赖绑定。

接下来就详细讲解这几个过程在代码里是怎么做的。

2. computed初次计算及依赖绑定

2.1 组件mount创建render watcher触发computedGetter

组件mount时,会创建一个render watcher,传入的expOrFn参数是一个更新组件的函数updateComponent:

// vue/src/core/instance/lifecycle.js
export function mountComponent (
	...
 	updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    ...
    new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  ...
}

render watcher的构造函数中,this.getter值得到的就是updateComponent这个函数,然后会主动去调用一自己的get函数,最终进入到updateComponent函数。

// vue/src/core/observer/watcher.js
export default class Watcher {
	constructor(vm, expOrFn, cb, options, isRenderWatcher) {
	...
	if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    }
    ...
	// render watcher lazy 为 false, computed watcher 为 true
	this.value = this.lazy ? undefined : this.get()
	...
    }
    ...

	get() {
		value = this.getter.call(vm, vm)
	}
}

进入到updateComponent后,会调用vm._render函数,然后进入组件编译生成的render函数:

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c("div", [_vm._v(_vm._s(_vm.sum) + " ")])
}

因为组件模板中用到了{{ sum }},所以render函数中会引用到vm.sum这个值:
便会调用到vm.sum的computedGetter函数。

// vue/src/core/instance/state.js
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,就会去调evaluate

2.2. computed初次计算及computed watcher的依赖绑定

// vue/src/core/observer/watcher.js
export default class Watcher {
	...
	evaluate () {
    	this.value = this.get()
		this.dirty = false
    }
    ...
    
	get() {
		...
		value = this.getter.call(vm, vm)
		...
		return value
	}
}

evaluate函数里,会调用computed watcher的getter,前面讲到,getter就是那this.a + this.b函数,进而就会去调到a和b的getter函数,这里就把computed watcher和a,b做了依赖绑定。

接下来把计算的值给了this.value,这便是computed的缓存值,并把dirty设置为false,如果下次数据没有更新,dirty为false,就会不调用evaluate,而是直接取这个值。

2.3. render watcher的依赖绑定

evaluate结束之后,接下来会去调用vm.sum的computed watcher的depend()

// vue/src/core/observer/watcher.js
export default class Watcher {
	...
	depend () {
		let i = this.deps.length
		while (i--) {
		this.deps[i].depend()
	}
	...
}

此时的Dep.target是render watcher,这里的this.deps,就是vm.sum的computed watcher刚绑定的依赖data.a和data.b的dep。

computed watcher的depend函数就实现了以自已为桥梁,让render watcher和data.a,data.b建立了绑定。

前面那个问题为什么render watcher没有直接引用data,却能知道data的变化,答案就在这里。

至此vm.sum的computed watcher已计算出了初始值,computed watcher和所依赖的data之间,render watcher和所依赖的data之间,也都进行了绑定,并且computed watcher的dirty值设为了false,watcher.value缓存了计算结果,下次取vm.sum的值不再evaluate计算,直接返回watcher.value值,除非data变化了。

3. data 变化通知 watcher 更新

通过上一节我们知道了,computed watcher和render watcher都绑定了对应的data,当data变化时,便会通知两个watcher去更新。

// vue/src/core/observer/watcher.js
export default class Watcher {
  ...
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  ...
}

3.1. computed watcher 更新

由于computed watcher由于lazy值为true,所以update过程很简单,就是将dirty设置为true,vm.sum再被get时,就会执行第2节中的一系列过程,重新走一遍计算新值和绑定依赖。

3.2. render watcher 更新

render watcher的更新则是一个异步操作,先通过queueWatcher,将自己添加到队列中:

// vue/src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {
	...
    queue.push(watcher)
    ...
    if (!waiting) {
      	waiting = true
	  	...
      	nextTick(flushSchedulerQueue)
    }
}
...

然后异步的在下一个时钟nextTick里执行flushSchedulerQueue,这就保证了,computed watcher的this.dirty = true的本时钟内同步赋值,会先于render watcher异步更新,而如果render watcher先更新,computed watcher的dirty还没有改为true,那么render watcher访问vm.sum时的computedGetter时,拿到的就是缓存的旧值。

nextTick会将flushSchedulerQueue放到一个callbacks数据组里,通过一个异步方法timerFunc,依次执行这些callbacks,这个异步过程的实现有4种,按照如下顺序依次检测哪种可用就用哪种:

  1. Promise
  2. MutationObserver
  3. setImmediate
  4. setTimeout

在下一个时钟,执行flushSchekulerQueue过程如下:

// vue/src/core/observer/scheduler.js
function flushSchedulerQueue () {
	...
	for (index = 0; index < queue.length; index++) {
		...
		watcher.run()
		...
	}
}
// vue/src/core/observer/watcher.js
export default class Watcher {
  	...
  	run () {
  		...
      	const value = this.get()
      	...
  	}
  	...
}

这里便又会去执行get过程,把第2节中的内容再次走一遍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值