computed用法

当依赖的属性的值发生变化时,会触发它自身的更改。

依赖的值不发生变化时,使用的是缓存中的属性值。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
<div>
<div>
姓:<input v-model="firstName" type="text">
</div>
<div>
名:<input v-model="lastName" type="text">
</div>
<div>
全名:{{ name }}
</div>
<div>
<button @click="changeName">changeName</button>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'

let firstName = ref("李")
let lastName = ref("田所")

const changeName = ()=> {
// 使用函数式写法的话,name.value就是只读的
name.value = "田所-浩二"
}

// 选项式写法,支持一个对象传入get函数和set函数自定义操作
let name = computed<string>({
get() {
return firstName.value + "-" + lastName.value
},
set(newVal: string) {
// 解构赋值
[firstName.value, lastName.value] = newVal.split("-")
}
})

// 函数式写法,只能支持一个getter函数,不允许修改值
// let name = computed(()=> {
// return firstName.value + "-" + lastName.value
// })
</script>

模拟案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<template>
<div>
<div>
<input v-model="keyWord" placeholder="搜索" type="text">
</div>
<div style="margin-top: 20px;">
<table border width="600" cellpadding="0" cellspacing="0">
<thead>
<tr>
<th>名称</th>
<th>单价</th>
<th>数量</th>
<th>总价</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<!-- <tr v-for="(item, index) in data" :key="index"> -->
<tr v-for="(item, index) in searchData" :key="index">
<td align="center">{{ item.name }}</td>
<td align="center">{{ item.price }}</td>
<td align="center">
<button @click="sub(item)">-</button>
{{ item.num }}
<button @click="add(item)">+</button>
</td>
<td align="center">{{ item.num*item.price }}</td>
<td align="center"><button @click="del(index)">删除</button></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5" align="right">
<!-- 总价:{{ $total }} -->
总价:{{ total }}
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, computed, reactive } from 'vue'

interface Data {
name: string,
price: number,
num: number,
}
let keyWord = ref<string>("")

let data = reactive<Data[]>([
{
name: "KNN",
price: 114,
num: 1,
},
{
name: "SNNN",
price: 514,
num: 1,
},
{
name: "YJSP",
price: 1919,
num: 1,
},
{
name: "RU",
price: 810,
num: 1,
},
{
name: "BNKRG",
price: 364,
num: 1,
},
{
name: "YMN",
price: 364,
num: 1,
},
])

// 使用函数方式计算总价
// let $total = ref<number>(0)
// const total = ()=> {
// $total.value = data.reduce((prev: number, next: Data)=> {
// return prev+next.num*next.price
// }, 0)
// }
// 程序开始时、物品数量增删改时,都要调用这个函数,很麻烦
// total()

// 使用computed就很省心了
const total = computed(()=> {
return data.reduce((prev: number, next: Data)=> {
return prev+next.num*next.price
}, 0)
})
// 还可与用来过滤数据
const searchData = computed(()=> {
return data.filter((item:Data)=> {
return item.name.includes(keyWord.value)
})
})

const sub = (item: Data)=> {
// return item.num>1?(item.num--,total()):null
return item.num>1?item.num--:null
}
const add = (item: Data)=> {
// return item.num<99?(item.num++,total()):null
return item.num<99?item.num++:null
}
const del = (index: number)=> {
data.splice(index,1)
// total()
}
</script>

源码

在Vue源码(/package/reactivity/src/computed.ts)中可以看到computed的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// 函数重载,支持多种传参
export function computed<T>(
getter: ComputedGetter<T>,
debugOptions?: DebuggerOptions
): ComputedRef<T>
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>

// 如果传来的是函数,那么这个computed是只读的
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
// 传入的函数赋给getter
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
}


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, () => {
// 调度函数scheduler
// 只有依赖发生变化,并且脏值_dirty为false时,调度函数才会走
// 初始化时的脏值_dirty是true,所以不会进来
if (!this._dirty) {
// 把脏值_dirty变成true,说明依赖变化了,需要调用getter函数获取新值
this._dirty = true
triggerRefValue(this)
}
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}

// computed(()=> {
// return a.xx + 10
// })
// 假设a.xx是5,那它返回的值就是15,effect.run()读取的值就是15
// 如果a.xx变成6,ReactiveEffect就会走,它把脏值_dirty变成true,再调用一次effect.run()

// 劫持value
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self)
// 如果脏值_dirty是true,就重新计算
// 如果值不变就不重新计算,直接返回上次计算出来的值(缓存在_value中)
if (self._dirty || !self._cacheable) {
self._dirty = false // 脏值_dirty设为false,所以它不脏了
self._value = self.effect.run()! // effect.run()用来读取getter函数的返回值
}
// 把函数的返回值赋给_value并return
return self._value
}

set value(newValue: T) {
this._setter(newValue)
}
}

这部分位于/package/reactivity/src/effect.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ReactiveEffect类接收调度函数scheduler,此处省略

// 会被trigger调用
// trigger在依赖更新时调用
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()
}
}
}

实现Computed

computed.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { effect } from "./effect"

export const computed = (getter: Function)=> {
let _value = effect(getter, {
// 因为它会在依赖更新时被调用,所以依赖更新时脏值被赋true
scheduler:()=>{_dirty=true}
})
// 缓存值
let catchValue: any
let _dirty = true

class ComputedRefImpl {
get value() {
// 如果脏值为true,更新
if(_dirty) {
catchValue = _value()
_dirty = false
}
return catchValue
}
}

return new ComputedRefImpl()
}

修改上一节的effect.ts的部分内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 加入一个effect选项接口
interface Options {
scheduler?: Function
}

/**
* 修改effect
* @param fn 匿名函数
*/
export const effect = (fn: Function, options: Options)=> {

// 一个闭包
const _effect = function() {
activeEffect = _effect
let res = fn()
return res
}

// 把选项(这里主要是调度函数)赋给返回值_effect
_effect.options = options

// 初始化的时候调用一下
_effect()
return _effect
}

/**
* 修改trigger
* @param target
* @param key
*/
export const trigger = <T extends object> (target: T, key: string | symbol) => {
const depsMap = targetMap.get(target)
if(!depsMap) {
throw "111"
}
const deps = depsMap.get(key)
if(!deps) {
throw "222"
}

deps.forEach((effect: any)=> {
// 达到了每次依赖更新都会有则调用调度函数的效果
if(effect?.options?.scheduler) {
effect?.options?.scheduler?.()
} else {
effect()
}
})
}