computed 计算属性原理
一、computed 用法
computed() 接收一个 getter 函数,自动追踪响应式依赖,返回值为一个计算属性 ref
,可以通过 .value 访问计算结果,在模板中会自动解包。
1、基本用法
<template>
<input type="text" v-model="txt" />
<div>txt length is {{ length }}</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const txt = ref('')
const length = computed(() => txt.value.length)
</script>
注意,追踪的是响应式依赖返回值才会随依赖的更新而更新。另外,computed 函数中不应该有任何副作用,而且返回结果是只读的。
2、可写的 computed
如果需要用到可写的 computed,可以用另一种写法:
<template>
<input type="number" v-model="num" />
<div>double num is {{ doubleNum }}</div>
<button @click="changeComputed">change computed to 10</button>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const num = ref(1)
const doubleNum = computed({
// getter
get() {
return num.value * 2
},
// setter
set(newNum) {
num.value = newNum / 2
}
})
const changeComputed = () => {
doubleNum.value = 10
}
</script>
上面提高 changeComputed
改变 computed 的返回值,此时 computed 是可写的。
二、computed 原理
computed 本质是一个惰性求值的观察者,内部根据参数的写法,兼容创建的 getter 和 setter,然后创建一个 ref 实例,在实例中通过 dirty 变量标记是否需要重新求值,根据依赖状态的变化完成 computed 值的计算,最后返回创建的 ref 实例。
- computed
- Watcher
computed.ts
// https://github.com/vuejs/vue/blob/main/src/v3/reactivity/computed.ts#L37
export function computed<T>(
// 可传入 getter 函数或包含 get、set 的对象
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
// 依赖收集和触发依赖的钩子函数,只在开发环境中起作用
debugOptions?: DebuggerOptions
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// 判断传入参数是否是一个函数
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
// 如果 getterOrOptions 是个函数
// 说明返回结果是不可写的,所以 setter 会设为空函数
getter = getterOrOptions
setter = __DEV__
? () => {
warn('Write operation failed: computed value is readonly')
}
: noop
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
const watcher = isServerRendering()
? null
: new Watcher(currentInstance, getter, noop, { lazy: true })
if (__DEV__ && watcher && debugOptions) {
watcher.onTrack = debugOptions.onTrack
watcher.onTrigger = debugOptions.onTrigger
}
// 创建一个 ref 实例
const ref = {
// some libs rely on the presence effect for checking computed refs
// from normal refs, but the implementation doesn't matter
effect: watcher,
get value() {
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
if (__DEV__ && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
}
watcher.depend()
}
return watcher.value
} else {
return getter()
}
},
set value(newVal) {
setter(newVal)
}
} as any
def(ref, RefFlag, true)
def(ref, ReactiveFlags.IS_READONLY, onlyGetter)
// 返回 ref 实例
return ref
}
watcher.ts
// https://github.com/vuejs/vue/blob/main/src/core/observer/watcher.ts#L41
export default class Watcher implements DepTarget {
// ...
dirty: boolean
// ...
value: any
// dev only
onTrack?: ((event: DebuggerEvent) => void) | undefined
onTrigger?: ((event: DebuggerEvent) => void) | undefined
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
// ...
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
if (__DEV__) {
this.onTrack = options.onTrack
this.onTrigger = options.onTrigger
}
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
// ...
this.dirty = this.lazy // for lazy watchers
// ...
// parse expression for getter
if (isFunction(expOrFn)) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
__DEV__ &&
warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy ? undefined : this.get()
}
// ...
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate() {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
// ...
}