Vue3源码Ⅴ-computed&ref实现

computed & ref的基本用法以及实现

computed 的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { reactive, effect, watch, watchEffect,computed} from '../../../node_modules/@vue/runtime-dom/dist/runtime-dom.esm-browser.js'

const state = reactive({firstname: 'g' , lastname: 'zy'})

// 可以传一个函数
// computed(()=>{ })

// 可以传一个对象
const fullname = computed({
get(){
console.log('getter')
return state.firstname + state.lastname
},
set(val){ // 改计算属性可以导致修改其他属性
console.log(val)
}
})

// 只打印一次getter ,缓存
console.log(fullname.value)
console.log(fullname.value)
console.log(fullname.value)

通过例子我肯可以看到,computed如下特性

  • 计算属性主要是根据其他数据进行衍生数据的
  • 计算属性默认是懒执行的,如果依赖的值不发生变化不会重新执行
  • 计算属性的值自身无法修改
  • 依赖的值变化了,后续再取值可以获取到新值

computed 的实现

computed可以传递一个函数,也可以传递一个对象

如果传递的是一个函数,这个函数就是getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

export function computed(getterOrOptions){
let getter;
let setter;

const isGetter = isFunction(getterOrOptions)
if(isGetter){
getter = getterOrOptions
setter = () => {
console.log('warn')
}
}else{
getter = getterOrOptions.get
setter = getterOrOptions.set
}
}

computed 返回的事一个普通对象,为了方便依赖收集,应该把普通值包装成一个对象

类似这样的结构

1
2
3
4
5
6
7
8
9
10
11
const abc = 123
let obj = {
_value: abc,
get value(){
// 依赖收集
return this._value
},
set value(xxx){
this._value = xxx
}
}

vue中是通过 ComputedRefImpl 类来实现的

  • 这个类接受getter setter两个函数
  • 这个类主要是用来收集依赖,离不开 ReactiveEffect
  • getter就相当于effect传递的函数,调用run方法内部就会进行依赖收集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { isFunction } from "@vue/shared";
import { ReactiveEffect } from "./effect";

class ComputedRefImpl {
public effect // 放到实例上方便取值
constructor(getter,setter){
this.effect = new ReactiveEffect(getter)
}
get value(){
this._value = this.effect.run() // this._value就是取值后的结果

return this._value
}
set value(newVal){
this.setter(newVal)
}
}

export function computed(getterOrOptions){
//...
// 把普通值包装成一个对象
return new ComputedRefImpl(getter,setter)
}

这时候控制台打印 (还没有具备缓存功能)

1
2
3
4
5
6
getter
gzy
getter
gzy
getter
gzy

增加缓存功能

  • 增加一个 _dirty 来标识是否缓存
  • 在取值前根据 _dirty 判断是否需要重新执行 run 方法
  • 在每次设置值(值发生了改变)后重置 _dirty 状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

class ComputedRefImpl {
public effect
public _value
public _dirty = true
public dep = new Set()
constructor(getter,setter){
this.effect = new ReactiveEffect(getter, ()=>{
if(!this._dirty){
this._dirty = true // 依赖的值发生变化了,会将dirty变为true
}
})
}
get value(){
if(this._dirty){
this._value = this.effect.run() // this._effect就是取值后的结果
this._dirty = false
}
return this._value
}
set value(newVal){
this.setter(newVal)
}
}

计算属性应该也进行依赖收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { reactive, effect, watch, watchEffect ,computed} from './reactivity.js'

const state = reactive({firstname: 'g' , lastname: 'zy'})

const fullname = computed({
get(){
console.log('getter')
return state.firstname + state.lastname
},
set(val){ // 改计算属性可以导致修改其他属性
console.log(val)
}
})

effect(()=>{
app.innerHTML = fullname.value
})

state.firstname = 'hello'

这时候firstname发生了变化,但是页面并没有重新渲染

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
class ComputedRefImpl {
public effect
public _value
public _dirty = true
public dep = new Set()
constructor(getter, setter) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true // 依赖的值发生变化了,会将dirty变为true

// 当依赖的值发生变化了 也应该触发更新
triggerEffect(this.dep)
}
})
}
get value() {
// 在取值时 要对计算属性也做依赖收集
// 如果计算属性是在effect中使用的要做依赖收集
// if(activeEffect){
trackEffect(this.dep)
// }

if (this._dirty) {
this._value = this.effect.run() // this._effect就是取值后的结果
this._dirty = false
}

return this._value
}
set value(newVal) {
this.setter(newVal)
}
}

可以基于以前的 track 和 trigger 进行拆分

effect.ts 中 track 方法拆分
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
const targetMap = new WeakMap()
export function track(target,key){
// 让这个对象上的属性 记录当前的activeEffect
if(activeEffect){
// 说明用户是在effect中使用的这个数据
let depsMap = targetMap.get(target)

// 如果没有创建一个映射表
if(!depsMap){
targetMap.set(target,(depsMap = new Map))
}

// 如果有这个映射表来查一下有没有这个属性
let dep = depsMap.get(key)

// 如果没有set集合创建集合
if(!dep){
depsMap.set(key, (dep = new Set()))
}

// 如果有则看一下set中有没有这个effect(去重)
trackEffect(dep)
}
}

export function trackEffect(dep){
let shouldTrack = !dep.has(activeEffect)
if(shouldTrack){
dep.add(activeEffect)

// name = new Set(effect)
// age = new Set(effect)

// 我可以通过当前的effect 找到这两个集合中的自己 将其移除掉就可以了
activeEffect.deps.push(dep)
}
}
effect.ts 中 trigger 方法拆分
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

export function trigger(target,key,newVal,oldVal){
// 通过对象找到对应的属性 让这个属性对应的effect重新执行
const depsMap = targetMap.get(target)
if(!depsMap) {
return
}

const dep = depsMap.get(key); // name 或者 age对应的所有 effect

triggerEffect(dep)
}

export function triggerEffect(dep){
const effects = [...dep]
// 运行的是数组 删除的是set
effects &&
effects.forEach(effect => {
// 正在执行的effect , 不要多次执行
if(effect !== activeEffect){
if(effect.scheduler){
effect.scheduler() // 用户传递了对应的更新函数则调用此函数
// 用户如果没有传递则默认就是重新运行effect函数
}else{
effect.run()
}
}
})
}

直接访问fallname

现在访问fallname需要 fallname.value ,修改代码直接fallname取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fullname = computed({
get() {
console.log('getter')
return state.firstname + state.lastname
},
set(val) { // 改计算属性可以导致修改其他属性
console.log(val)
}
})

const state = reactive({ firstname: 'g', lastname: 'zy', fullname })

effect(() => {
app.innerHTML = state.fullname
})

setTimeout(() => {
state.firstname = 'hello'
}, 2000)

增加 public __v_isRef = true

1
2
3
4
5
6
7
class ComputedRefImpl {
public effect
public _value
public _dirty = true
public dep = new Set()
public __v_isRef = true // 表示后续我们可以增加拆包的逻辑
// ...

handler.ts

如果存在 __v_isRef 直接返回 value

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

export const mutableHandlers = {
// target 指的是原对象 key 指取那个属性 receiver 指的是代理对象(这里的receiver就是proxy)
get(target,key,receiver){
// 我们在使用proxy的时候要搭配reflect来使用,用来解决this问题
// 取值的时候,让这个属性 和 effect产生关系
// return target[key]
if(key === ReactiveFlags.IS_REACTIVE){
return true
}

if(isRef(target[key])){
return target[key].value
}

// 如果在取值的时候发现取出来的值是对象,那么再次进行代理,返回代理后的结果
if(isObject(target[key])){
return reactive(target[key])
}

// 做依赖收集 记录属性和当前effect的关系
const res = Reflect.get(target,key ,receiver) // 先拿到新值
track(target,key)
return res // 取值,并且让target中的this变为代理对象
}
}

function isRef(value){
return !!(value && value.__v_isRef)
}

ref 的使用

1
2
3
4
5
6
7
8
9
10
11
12
import { reactive, effect, watch, watchEffect, computed, ref } from '../../../node_modules/@vue/runtime-dom/dist/runtime-dom.esm-browser.js'
// import { reactive, effect, watch, watchEffect, computed, ref } from './reactivity.js'

let flag = ref(true) // 装包,就是将变量在包装一层,里面还有依赖收集功能

effect(() => {
app.innerHTML = flag.value
})

setTimeout(() => {
flag.value = false
}, 1000)

ref 的实现

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
import { isObject } from "@vue/shared"
import { reactive } from "./reactivity"
import { trackEffect, triggerEffect } from "./effect"

export function toReactive(value){
return isObject(value) ? reactive(value): value
}

class RefImpl {
public _value
constructor(public rawValue){
// 如果传入的是对象会变为proxy
this._value = toReactive(rawValue)
}
get value(){
return this._value
}
set value(newVal){
if(newVal !== this.rawValue){
this.rawValue = newVal
this._value = toReactive(newVal)
}
}
}

export function ref(value){
return new RefImpl(value)
}

增加依赖收集,标识 ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class RefImpl {
public _value
public dep = new Set()
public __v_isRef = true
constructor(public rawValue){
// 如果传入的是对象会变为proxy
this._value = toReactive(rawValue)
}
get value(){
trackEffect(this.dep)
return this._value
}
set value(newVal){
if(newVal !== this.rawValue){
this.rawValue = newVal
this._value = toReactive(newVal)
triggerEffect(this.dep)
}
}
}