概述
- 计算属性 computed 会基于其响应式依赖被缓存,并且在依赖的响应式数据发生变化时,重新计算
Debug Computed 源码
-
测试示例: computed.html,测试vue源码的属性
<script src='../../dist/vue.global.js'></script> <body> <div id='app'></div> </body> <script> const { ref, effect, computed } = Vue const obj = ref({ name: '张三' }) const computedObj = computed(()=>{ return '姓名: ' + obj.value.name }) effect(() => { document.querySelector('#app').innerText = computedObj.value }) const timer = setTimeout(() => { clearTimeout(timer) obj.value.name = '李四' }, 2000) </script> -
进入computed函数
export function computed<T>( getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>, debugOptions?: DebuggerOptions, isSSR = false ) { // 首先创建两个变量:getter和setter let getter: ComputedGetter<T> let setter: ComputedSetter<T> // getterOrOptions 是 我们传入的第一个参数 const onlyGetter = isFunction(getterOrOptions) if (onlyGetter) { getter = getterOrOptions // 这里没有 setter函数,初始化一个 setter = __DEV__ ? () => { console.warn('Write operation failed: computed value is readonly') } : NOOP } else { getter = getterOrOptions.get setter = getterOrOptions.set } // 创建一个 ComputedRefImpl 实例 const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR) if (__DEV__ && debugOptions && !isSSR) { cRef.effect.onTrack = debugOptions.onTrack cRef.effect.onTrigger = debugOptions.onTrigger } // 这里返回其实例 return cRef as any } -
进入 ComputedRefImpl 类查看
export class ComputedRefImpl<T> { public dep?: Dep = undefined // 这里是我们依赖收集的Set private _value!: T public readonly effect: ReactiveEffect<T> public readonly __v_isRef = true // 这里是一个true的标识,所有的计算属性的 这个属性都是真 public readonly [ReactiveFlags.IS_READONLY]: boolean = false public _dirty = true // 这里声明一个脏变量 public _cacheable: boolean constructor( getter: ComputedGetter<T>, // 我们传入的 getter private readonly _setter: ComputedSetter<T>, // setter 是只读的 isReadonly: boolean, isSSR: boolean ) { // 创建一个 effect 实例,第一个参数就是我们的fn回调参数,第二个参数是个匿名参数 this.effect = new ReactiveEffect(getter, () => { // 这里重新进行赋真值和依赖触发,这个脏变量的本质就是为了控制触发依赖,也就是在其值为假的时候 if (!this._dirty) { this._dirty = true triggerRefValue(this) } }) this.effect.computed = this this.effect.active = this._cacheable = !isSSR this[ReactiveFlags.IS_READONLY] = isReadonly } get value() { // the computed ref may get wrapped by other proxies e.g. readonly() #3376 const self = toRaw(this) trackRefValue(self) if (self._dirty || !self._cacheable) { self._dirty = false self._value = self.effect.run()! } return self._value } set value(newValue: T) { this._setter(newValue) } } -
从这里我们可知:
- 在computed中有一个getter函数和一个setter函数
- getter函数就是我们传递进来的一个函数,而setter函数默认是一个空的函数
- 在computed函数中生成了一个 ComputedRefImpl 实例,并作为返回值返回
- 而 ComputedRefImpl 内部 通过 ReactiveEffect 生成了一个 effect 实例
- 并且赋值挂载到了 this.effect 上,也就是说我们所有计算属性的实例都包括 effect
- 其中,在 new ReactiveEffect 时候,第一个参数是getter,是computed传入的回调函数,也是我们的fn函数
- 也就是实例 this.effect 执行run函数的时候,就会触发当前的回调
- 传入了第二个参数是个匿名函数,使用脏变量来触发依赖的, 在this._dirty为false的时候,执行将自身变为真,再调用triggerRefValue进行依赖触发
- 脏变量核心的本质是用于控制何时触发依赖的,只有脏变量为假的时候,才会触发,否则依赖不会触发
-
接下来,业务代码中在effect函数中触发了getter行为,当执行了 第一个 回调 函数之后,就会触发 getter行为,进入 vue源码 computed 的 get value() 函数中
- 执行了 trackRefValue 用于收集依赖的作用,这个函数就是 ref.ts中的依赖收集的函数
- 在我往下走,到
if (self._dirty || !self._cacheable) {} - 在这个if代码块中,当 self._dirty 为真 或 self._cacheable 为假的时候,才会把 self._dirty 变为假
- 之后,执行 run 函数,内部也就是我们的依赖 fn 函数
-
对于 ComputedRefImpl 而言,它也没有实现代理监听机制,它是通过 get value() 和 调度器 完成的触发
- 在每一次 get value 触发的时候,都进行依赖的收集,同时根据脏状态来判断是否需要执行run函数
- 最后computed的返回值就是run函数执行之后的结果
- 如果当前脏状态是false的时候则不会执行run函数,直接使用上次的value作为新的value进行返回,完成了数据缓存的功能
-
对于计算属性而言,当它依赖的响应式数据发生变化时,将重新计算
- 换句话说,当响应式数据触发setter行为的时候,计算属性需要重新触发依赖
- 每一次get value() 时都会主动执行依赖收集,那触发依赖在哪里?
- 实际上经上面分析,在构造器 constructor 中,new ReactiveEffect 的时候,第二个参数就是触发依赖的回调
- 它是由脏变量控制的,所以触发依赖在于脏变量状态的变更时,这第二个参数实际上是一个 scheduler
- 它本质上是一个调度器,实际上我们可以在源码 effect.ts 中可以看到 effect.scheduler 的使用
// effect.ts 源码中 function triggerEffect( effect: ReactiveEffect, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { if (effect !== activeEffect || effect.allowRecurse) { if (__DEV__ && effect.onTrigger) { effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) } if (effect.scheduler) { // 这里 effect.scheduler() } else { effect.run() } } } - 也就是说,effect 中如果包含scheduler时,将会执行调度器,否则执行run函数
-
在业务代码中,2s之后,触发 setter 行为,也就是统一走 baseHandlers 中的 createSetter方法
- 在最底部,会进入到 trigger 方法中,如下:
if (target === toRaw(receiver)) { if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { // 走这里 trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result - 进入这个 trigger 函数中 会进入 triggerEffects中
if (deps.length === 1) { if (deps[0]) { if (__DEV__) { // 走到这里 triggerEffects(deps[0], eventInfo) } else { triggerEffects(deps[0]) } } } - 之后,triggerEffects 内部触发 triggerEffect
export function triggerEffects( dep: Dep | ReactiveEffect[], debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { // spread into array for stabilization const effects = isArray(dep) ? dep : [...dep] // 下面两个for循环 for (const effect of effects) { if (effect.computed) { triggerEffect(effect, debuggerEventExtraInfo) } } for (const effect of effects) { if (!effect.computed) { triggerEffect(effect, debuggerEventExtraInfo) } } }- 在 triggerEffect
function triggerEffect( effect: ReactiveEffect, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { if (effect !== activeEffect || effect.allowRecurse) { if (__DEV__ && effect.onTrigger) { effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) } // 这里就是调度器的属性 if (effect.scheduler) { effect.scheduler() } else { effect.run() } } } - 这个 scheduler 就是计算属性的构造函数里面 传进来的 第二个参数,debugger 进入 scheduler 函数
- 就会跳转到 ComputedRefImpl 这个计算属性类的构造函数constructor中的 new ReactiveEffect 的第二个参数
- 继续debugger 查看this对象,发现当前的 脏变量 this._dirty 是 false,是因为之前执行了 get value(), 内部变更了其状态
- 脏状态是 false 就意味着将要执行:把当前脏状态 改为 true,并且进行触发依赖 triggerRefValue,进入这个函数
export function triggerRefValue(ref: RefBase<any>, newVal?: any) { ref = toRaw(ref) if (ref.dep) { if (__DEV__) { triggerEffects(ref.dep, { target: ref, type: TriggerOpTypes.SET, key: 'value', newValue: newVal }) } else { triggerEffects(ref.dep) } } } - 这个我们之前分析过,后面就不再继续了,触发依赖,执行回调,页面渲染出新的值
- 这里,就是完整的一次 setter 行为
- 在 triggerEffect
- 在最底部,会进入到 trigger 方法中,如下:
-
对业务代码中 2s 后的过程进行一次总结
obj.value.name = '李四'这行代码会触发 proxy 的 setter 行为- 在 setter 行为中,会触发 trigger 完成第一次的依赖收集, 此时依赖收集中的 effect 中包含调度器
- 因包含调度器,所以会执行调度器 effect.scheduler() 指向到 computed源码中 new ReactiveEffect 的第二个参数
- 第二个参数内部,会再次触发 triggerRefValue
- 以上是两次触发依赖,第二次的触发会导致业务代码中的 effect函数的回调执行,也就是被收集的依赖fn函数
删减后的核心实现代码
-
参考之前的rollup构建环境搭建
-
创建实例程序 computed.html 用于测试我们拆解后的 computed 源码
<script src="../dist/vue.js"></script> <body> <div id='app'></div> </body> <script> const { reactive, effect, computed } = Vue const obj = reactive({ name: '张三' }) const computedObj = computed(()=>{ return '姓名: ' + obj.name }) effect(() => { document.querySelector('#app').innerText = computedObj.value }) const timer = setTimeout(() => { clearTimeout(timer) obj.value.name = '李四' }, 2000) </script> -
创建 reactivity/computed.ts 模块
import { isFunction } from '@vue/shared' import { Dep } from './dep' import { ReactiveEffect } from './effect' import { trackRefValue, triggerRefValue } from './ref' export class ComputedRefImpl<T> { public dep?: Dep = undefined private _value!: T public readonly effect: ReactiveEffect<T> public readonly __v_isRef = true public _dirty = true constructor(getter) { this.effect = new ReactiveEffect(getter, () => { if (!this._dirty) { this._dirty = true triggerRefValue(this) } }) this.effect.computed = this } get value() { trackRefValue(this) // 缓存处理 if (this._dirty) { this._dirty = false // 更新脏状态 this._value = this.effect.run() } return this._value } } export function computed(getterOrOptions) { let getter const onlyGetter = isFunction(getterOrOptions) if (onlyGetter) { getter = getterOrOptions } const cRef = new ComputedRefImpl(getter) return cRef } -
更新 effect.ts 中的 ReactiveEffect, 用于补充私有属性 computed
import { ComputedRefImpl } from './computed' // 导入相关类型 export type EffectScheduler = (...args: any[]) => any export class ReactiveEffect<T = any> { // 添加这行,用于协同 computed 处理 computed?: ComputedRefImpl<T> // 这里添加 scheduler 属性 constructor(public fn: () => T, public scheduler: EffectScheduler | null = null) {} run() { activeEffect = this return this.fn() } } -
更新 effect.ts 中的 triggerEffect 用于调度器的参与
// 触发指定依赖 export function triggerEffect(effect: ReactiveEffect) { effect.scheduler ? effect.scheduler() : effect.run() } -
以上,我们完成了 最简洁版的 computed 函数
实现 computed 的缓存功能
-
也就是我们如果没有对数据做变更,仍旧使用之前的数据,不必每次都收集依赖和触发依赖
-
修改业务测试代码 computed.html或新建 computed-cache.html 文件
<script src="../dist/vue.js"></script> <body> <div id='app'></div> </body> <script> const { reactive, effect, computed } = Vue const obj = reactive({ name: '张三' }) const computedObj = computed(()=>{ console.log('cccc') // 这里打印computed的getter执行了多少次 return '姓名: ' + obj.name }) effect(() => { document.querySelector('#app').innerText = computedObj.value document.querySelector('#app').innerText = computedObj.value // 执行第二次 getter }) const timer = setTimeout(() => { clearTimeout(timer) obj.name = '李四' }, 2000) </script> -
以上代码,我们打开控制台,可以看到 seHandlers.ts:12 Uncaught RangeError: Maximum call stack size exceeded 循环调用,报错了
-
如果业务代码因为重复的调用而导致框架内部循环调用变成了死循环,这样的框架肯定是不鲁棒的,需要尽快修复,这里就需要用到缓存相关的优化了
-
产生死循环的原因是第二次触发 effect时,也就是 triggerEffect(effect), 拿到的 effect 是计算属性的effect, 从而导致触发effect的调度系统,导致了死循环的执行
- 调度器中会把 this._dirty 标识修改为 true
- 问题就在这个脏状态标识导致的循环调用
-
在vue源码中,effect.ts中,有两个for循环,以前以为这种写法是多余的,实际上这里有特殊的含义,通过两次for循环来构建执行的顺序,让计算属性先执行,非计算属性后执行
export function triggerEffects( dep: Dep | ReactiveEffect[], debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { // spread into array for stabilization const effects = isArray(dep) ? dep : [...dep] // 注意看这里,下面的两个 for 循环 for (const effect of effects) { if (effect.computed) { triggerEffect(effect, debuggerEventExtraInfo) } } for (const effect of effects) { if (!effect.computed) { triggerEffect(effect, debuggerEventExtraInfo) } } } -
我们来修改下自己实现的删减版代码,在 effect.ts 中
export function triggerEffects(dep: Dep) { const effects = Array.isArray(dep) ? dep : [...dep] // 依次触发循环 for (const effect of effects) { effect.computed && triggerEffect(effect) // 先执行computed属性的 } for (const effect of effects) { !effect.computed && triggerEffect(effect) // 后执行非computed属性的 } } -
这时候就没有循环调用的报错了
总结
- 计算属性的实例,是 ComputedRefImpl 的实例
- 在 ComputedRefImpl 实例中通过 脏变量来控制 run 和 triggerRefValue 函数的触发
- 计算属性的值,通过 .value 来访问,因其内部和 ref 一样是通过 get value 来实现的
- 每次 .value 访问computed数据时,都会触发依赖的收集,即调用 trackRefValue函数
- computed中的一项避免循环调用的优化:先触发 computed 的 effect, 再触发 非computed 的 effect
359



被折叠的 条评论
为什么被折叠?



