Computed: Vue 3 计算属性深入解析
- 1.计算属性的基本用法
- 2. ComputedRefImpl 类深入解析
- JavaScript 中的 getter 函数
- 3. 计算属性的创建:computed 方法解析
- 3.1 源码解析
- 3.2 使用示例
- 4. 计算属性的工作原理
- 5. 手动实现简化的计算属性
- 6. 结语
在 Vue 3 的响应式系统中,计算属性(computed)扮演着重要的角色。它们基于响应式依赖进行缓存,并仅在依赖项变化时重新计算。这意味着,计算属性能够提供高效的数据处理方式,因为只有当实际需要时计算属性的值才会更新。下面,我们将详细探讨 Vue 3 中计算属性的实现细节及其用法。
1.计算属性的基本用法
计算属性依赖于其他响应式数据,并且仅在这些依赖数据发生变化时才重新计算其值。这种机制保证了性能的优化,避免了不必要的计算。
import { reactive, computed } from "vue";
const state = reactive({
count: 1,
});
const plusOne = computed(() => state.count + 1);
console.log(plusOne.value); // 2
state.count++;
console.log(plusOne.value); // 3
2. ComputedRefImpl 类深入解析
ComputedRefImpl 类是计算属性在 Vue 3 中的实现基础。它负责把用户定义的 getter 函数封装成响应式引用,并且管理计算结果的缓存。该类的关键实现如下:
export class ComputedRefImpl<T> {
//用于存储与此计算属性相关的依赖(副作用函数)。
public dep?: Dep = undefined
//用于存储计算属性的当前值
private _value!: T
//一个 ReactiveEffect 实例,用于封装计算属性的 getter 函数。这个副作用函数会在依赖的响应式数据变化时重新执行,以更新计算属性的值
public readonly effect: ReactiveEffect<T>
//内部标志,用于标记这个对象是一个 Ref 类型,并且指示其只读状态。
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
public _cacheable: boolean
/**
* Dev only
*/
_warnRecursive?: boolean
/**
* 构造函数接收四个参数:
* getter: 用户定义的计算属性的获取函数。
* _setter: 用户定义的计算属性的设置函数,用于允许计算属性被赋新值。
* isReadonly: 表明这个计算属性是否是只读的。
* isSSR: 标记是否在服务器端渲染环境中使用,影响是否缓存计算结果。
*/
constructor(
private getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean,
) {
//ReactiveEffect 被用于封装 getter 函数,确保每当依赖的数据变化时,都能够自动重新计算值,并缓存结果以提高性能。
this.effect = new ReactiveEffect(
() => getter(this._value),
() =>
triggerRefValue(
this,
this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
? DirtyLevels.MaybeDirty_ComputedSideEffect
: DirtyLevels.MaybeDirty,
),
)
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
/**
* 当访问计算属性的 value 时,会执行这个 getter 函数。
* 这个函数首先检查是否需要重新计算计算属性的值(基于缓存逻辑和依赖数据的变化)。
* 如果需要,它会运行封装的 getter 函数来更新 _value。
* 然后,它会注册当前活动的副作用函数为这个计算属性的依赖,以便将来数据变化时能触发更新
*/
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
//“获取当前计算属性实例(this)背后的原始对象,并将其赋值给 self 变量”
const self = toRaw(this)
if (
(!self._cacheable || self.effect.dirty) &&
hasChanged(self._value, (self._value = self.effect.run()!))
) {
triggerRefValue(self, DirtyLevels.Dirty)
}
trackRefValue(self)
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
if (__DEV__ && (__TEST__ || this._warnRecursive)) {
warn(COMPUTED_SIDE_EFFECT_WARN, `\n\ngetter: `, this.getter)
}
triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
}
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
// #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
get _dirty() {
return this.effect.dirty
}
set _dirty(v) {
this.effect.dirty = v
}
// #endregion
}
这个类通过 ReactiveEffect 封装 getter 函数,使计算属性能够响应依赖数据的变化。同时,通过缓存机制保证了性能的优化。
JavaScript 中的 getter 函数
get value() {} 是 JavaScript 中的一个 getter 函数的写法,它是对象属性访问器的语法之一。Getter 函数允许你定义一个对象属性,该属性在被访问时会自动执行一个函数来返回值,而不是直接返回一个值。这使得在对象属性被访问时可以执行更复杂的操作或计算,而对于使用者来说,这种访问看起来就像访问一个普通属性一样。
const person = {
firstName: "John",
lastName: "Doe",
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
};
console.log(person.fullName); // 输出: John Doe
3. 计算属性的创建:computed 方法解析
Vue 3 提供了 computed 函数,用于创建计算属性。这个函数既可以接受一个简单的 getter 函数,也可以接受一个包含 get 和 set 方法的对象,允许创建可读写的计算属性。
3.1 源码解析
/**
* 接受一个 getter 函数作为参数。这个 getter 函数定义了计算属性的计算逻辑,
* 当依赖的响应式数据变化时,这个函数会被重新执行来更新计算属性的值。
* 在这种情况下,计算属性是只读的,尝试写入会导致警告(在开发模式下)。
*/
export function computed<T>(
getter: ComputedGetter<T>,
debugOptions?: DebuggerOptions,
): ComputedRef<T>
/**
* 接受一个包含 get 和 set 方法的对象 options 作为参数
* 这允许你创建一个可写的计算属性。get 方法定义了计算逻辑,和只读计算属性一样。
* set 方法允许你自定义当尝试修改计算属性的值时的行为,这在需要基于计算属性的值反向更新其依赖的响应式数据时非常有用。
*/
export function computed<T>(
options: WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
): WritableComputedRef<T>
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false,
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
setter = __DEV__
? () => {
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
}
3.2 使用示例
import { computed } from "vue";
// 创建只读计算属性
const readOnlyComputed = computed(() => someReactiveData.value + 1);
// 创建可写计算属性
const writableComputed = computed({
get: () => someReactiveData.value + 1,
set: (newValue) => { someReactiveData.value = newValue - 1; }
});
4. 计算属性的工作原理
计算属性背后的核心是其延迟计算和缓存机制。ComputedRefImpl 类中的 effect 通过跟踪响应式依赖自动管理这些逻辑,保证了数据的实时性和性能的优化。当依赖数据变化时,计算属性会重新计算;否则,将直接使用缓存的结果。
5. 手动实现简化的计算属性
理解计算属性的实现机制后,我们可以尝试手动实现一个简化版本,以加深对其原理的理解。
function computedManual(getter){
const result = ref(); // 用于存储计算属性的结果
const runner = effect(getter,{
lazy:true, // 让 effect 不会立即执行
scheduler:()=>{
// 当依赖变化时,重新计算并更新 result 的值
result.value = runner();
}
})
// 立即执行一次 effect,初始化 result 的值
result.value = runner();
return {
// 返回一个具有 value 属性的对象,模拟 ComputedRef 接口
get value(){
return result.value;
},
// 提供一个停止响应式依赖更新的方法
stop:()=>stop(runner)
}
}
// 使用示例
const count = ref(1);
const doubled = computedManual(() => count.value * 2);
console.log(doubled.value); // 输出: 2
count.value = 2;
console.log(doubled.value); // 输出: 4
这个简化的实现利用了 Vue 的 effect 和 ref,通过设定 lazy 选项来控制副作用函数的执行,同时使用调度器更新计算结果。
6. 结语
计算属性是 Vue 3 响应式系统中不可或缺的一部分,它通过缓存和自动更新机制,有效地优化了数据处理的性能。通过深入理解其背后的实现原理,我们能更好地利用 Vue 提供的响应式功能构建高效的应用。