「Vue3学习篇」-computed()
『引言』
看到标题,想必对computed
并不会感到陌生。
在vue2中,需要进行数值计算
,并且依赖于其它数据时,使用computed
。
因为可以利用computed
的缓存特性
,避免每次获取值时,都要重新计算。
在对大数据进行操作时,可以提高性能
和减少计算次数
。
🤔🤔那vue3中的computed
是怎么使用的❓会有什么不一样❓一起探索一下🤓🤓。
『回顾』
通过一个简单示例回忆一下,vue2中computed的使用。
『写法一』
<template>
<div>Hello,大家好,我的全名叫::{{ fullName }}</div>
</template>
<script>
export default {
data() {
return {
firstName: 'wn',
lastName: 'xx'
}
},
computed: {
fullName() {
return this.firstName + this.lastName
}
}
}
</script>
『写法二』
<template>
<div>
<div>Hello,大家好,我的全名叫:{{ fullName }}</div>
<button @click="changeName">点击按钮修改全名</button>
</div>
</template>
<script>
export default {
data() {
return {
firstName: 'wn',
lastName: 'xx'
}
},
computed: {
fullName: {
get() {
return this.firstName + this.lastName
},
set(newValue) {
this.firstName = `${newValue.firstName}`
this.lastName = `${newValue.lastName}`
}
}
},
methods: {
changeName() {
this.fullName = {
firstName: 'pu',
lastName: 'pu'
}
}
}
}
</script>
接下来进入正题,看一看今日的【主角】
:vue3中的computed
。
『computed()』-0
『定义』
【官方解释】
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value
暴露 getter 函数的返回值。它也可以接受一个带有 get
和 set
函数的对象来创建一个可写的 ref 对象。
『用法』
computed
本身有两种使用方式:
const xxx = computed(() => xxx)
const xxx1 = computed({get: (newValue) => {}, set: (newValue) => {}})
『官网示例🌰』
创建一个只读的计算属性 ref:
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
创建一个可写的计算属性 ref:
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
『注意⚠️』
- vue.3.0 中用于从vue 按需导入 computed 计算属性。
- 如果传入的是一个getter 函数,会返回一个不允许修改的计算属性。
- 如果传入一个对象,包含get 和 set 函数, 就可以创建一个可以修改的计算属性。
『示例🌰』
<template>
<div>
<h2>init count:
{{ state.count }}
</h2>
<h2>init number:
{{ state.number }}
</h2>
<h2>computedCount:
{{ computedCount }}
</h2>
<h2>computedNumber:
{{ computedNumber }}
</h2>
<button @click="changeCount">
changeCount
</button>
</div>
</template>
<script setup>
import {reactive, computed } from 'vue'
// reactive
const state = reactive({
count: 1,
number: 10
})
// computed getter
const computedCount = computed(() => {
return state.count + 10
})
// computed set get
const computedNumber = computed({
get: () => {
return state.number + 100
},
set: (value) => {
state.number = value - 50
}
})
const changeCount = () =>{
state.count++;
computedNumber.value = 100
}
</script>
『效果展示』
『computed源码』
computed源码如下⬇️:
这个是computed源码的入口函数。
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__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
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
}
『源码分析』
getterOrOptions
是computed
接收函数方法,computed
的参数,可以是【getter函数】
也可以是【包含get、set的对象】
。
之后会判断getterOrOptions
是否是function,是
的话,说明computed是可读的
。不是
的话,说明computed是可读可写的
。
最后就是确定好之后,创建一个ComputedRefImpl
实例,并将其返回。ComputedRefImpl
是computed
源码中的核心。
接着一起看一下ComputedRefImpl
的源码。
ComputedRefImpl
源码如下⬇️:
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
public _dirty = true
public _cacheable: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
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() {}
set value(newValue: T) {}
}
『源码分析』
ComputedRefImpl
包含以下属性
- _value: 用来缓存我们计算的结果。
- effect: 在构造器中创建的ReactiveEffect实例。
- __v_isRef: 标记为一个ref类型。
- ReactiveFlags.IS_READONLY: 只读标识。
- _dirty: 用来控制是否需要重新计算。
- _cacheable: 是否可缓存,取决于SSR。
还包括constructor和get、set等函数。
先探索一下ComputedRefImpl
的构造器。ComputedRefImpl
构造器接收四个参数:
getter
setter
isReadonly
(是否只读)isSSR
(是否为SSR
)
在constructor
初始化的过程中,调用了ReactiveEffect
,第一次调用ReactiveEffect
时,只会传入一个参数getter
,第二次之后会就接受两个参数。
『ReactiveEffect源码』
ReactiveEffect
源码如下⬇️:
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
/**
* Can be attached after creation
* @internal
*/
computed?: ComputedRefImpl<T>
/**
* @internal
*/
allowRecurse?: boolean
/**
* @internal
*/
private deferStop?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
return this.fn()
}
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
try {
this.parent = activeEffect
activeEffect = this
shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}
stop() {
// stopped while running itself - defer the cleanup
if (activeEffect === this) {
this.deferStop = true
} else if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
『ReactiveEffect源码分析』
ReactiveEffect
中调用recordEffectScope
(this, scope)方法,记录当前 ReactiveEffect
对象的作用域。
recordEffectScope
会向 effects
中添加 effect
,如果没有传入scope参数,那么在执行recordEffectScope
时就会有一个默认的参数为activeEffectScope
。
export function recordEffectScope(
effect: ReactiveEffect
// scope默认值为activeEffectScope
scope: EffectScope | undefined = activeEffectScope
) {
if (scope && scope.active) {
scope.effects.push(effect)
}
}
ReactiveEffect
中的run方法
就是执行副作用函数,并且在执行副作用函数的过程中,会收集依赖。
run方法
中会先判断当前 ReactiveEffect
对象是否处于活跃状态,如果当前 ReactiveEffect
对象不处于活动状态,直接返回 fn
的执行结果。然后,会找当前 ReactiveEffect
对象的最顶层的父级作用域,记录父级作用域为当前活动的 ReactiveEffect
对象。
在 run 方法
中会判断 deferStop
的值,如果为 true,就会执行 stop 方法
。
ReactiveEffect
中的stop方法
就是停止当前的ReactiveEffect
对象,停止之后,就不会再收集依赖了。
接着创建effect对象
,在这里会判断_dirty
,_dirty
为false
,会将_dirty
设置为true
。之后会触发更改,调用triggerRefValue
函数。
并且此时的this.effect.computed
指向this,this.effect.active
与this._cacheable
在SSR中为false
,把isReadonly
赋值给ReactiveFlags.IS_READONLY
属性。
『triggerRefValue源码』
triggerRefValue
源码如下⬇️:
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
const dep = ref.dep
if (dep) {
if (__DEV__) {
triggerEffects(dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(dep)
}
}
}
『triggerRefValue源码分析』
triggerRefValue
会接收两个参数【ref】
和【newVal】
,先会通过toRaw
获取ref的原始对象
,然后对ref的原始对象
中是否有dep属性
进行判断,如果有,就触发dep中的依赖
。会调用
triggerEffects
方法。
『triggerEffects源码』
triggerEffects
源码如下⬇️:
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
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()
}
}
}
『triggerEffect源码分析』
首先是获取effects
,dep是一个依赖收集容器,dep实例
是一个set结构 [...dep] = [ effect ],真正的依赖是effect
实例。
然后会循环依赖列表,如果dep中的effect实例
,是计算属性的effect
,那么就会触发计算属性的effect
,执行effect
。如果dep中的effect实例
,不是计算属性的effect
,触发非计算属性的effect
,执行effect
。
最后将 dep
解构并生成 effects
数组,会判断是否具有这『activeEffect』『effect.allowRecurse』
两个方法, 如果 effect.onTrigger
存在,就会执行,只有开发模式下才会执行,然后判断如果 effect
是一个调度器,就会调用scheduler()
,其它情况调用run()
方法。
『ComputedRefImpl中get、set源码』
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)
}
『ComputedRefImpl中get、set源码分析』
当读取ComputedRefImpl
实例的value
属性时,会先使用toRaw
获取其原始对象。然后调用trackRefValue
进行依赖的收集。
export function trackRefValue(ref: RefBase<any>) {
// 如果允许收集并且存在activeEffect进行依赖收集
if (shouldTrack && activeEffect) {
ref = toRaw(ref)
if (__DEV__) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
} else {
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
}
通过_dirty
与_cacheable
属性判断决定是否需要修改self._value
,如果是_dirty或不可以被缓存,那么会将_dirty
设置为false
,并调用self.effect.run()
,修改self._value
,最后返回self._value
。
当修改ComputedRefImpl
实例的value
属性时,会调用实例的_setter
函数。
『vue2与vue3中computed的异同』
『相同点』
- computed都有缓存的功能。
- 都可以基于响应式数据进行计算,当响应式数据发生变化时,计算结果也会自动更新。
『不同点』
-
计算属性的定义方式不同
1)vue2中,使用
getters
来定义计算属性。2)Vue3中,在
setup
函数中进行定义。 -
计算逻辑可以直接访问
props
和data
1)vue2中,
getters
可以访问props
和data
,但是需要使用函数参数传入2)Vue3中,计算逻辑可以直接访问
props
和data
,不需要使用函数参数。 -
计算逻辑不支持
watch
,但是可以使用watchEffect
1)在
Vue2
中,可以使用watch
来监测计算属性的变化。2)在
Vue3
中,计算逻辑不再支持watch
,但是可以使用watchEffect
来监测变化。 -
计算逻辑可以定义为响应式的数据
1)在
Vue3
中,可以将计算逻辑声明为响应式的数据,这样可以在setup
函数中对其进行访问和更改。
转载自:https://juejin.cn/post/7283710975345213503