vue3源码学习-9-ref的实现

摘要

在页面编写中,之前都是使用 reactive 来包裹对象,这样对象属性的值改变,其对应的 effect 包裹渲染动作就会被触发。并且通常有函数解构的操作,例如let people = reactive({name:"张三",age:24}); let {name,age} = people;。如果这么结构操作的话,name 和 age 就会变成普通的变量,那么如果在 effect 中使用的话,就算改变了值也不会触发回调函数。那么为了解决例如此类的需求,vue3 提供了ref、toRef、toRefs

官方中的写法探究

引入官方的包,尝试了几种简单的写法。

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="" id="app"></div>
<script src="./dist/reactivity.global.js"></script>
<script>
const { reactive, effect,toRef,ref,toRefs,proxyRefs } = VueReactivity
// let people = reactive({name:"张三",age:99})
let name = ref("张三")
let age = ref(23)
let people = proxyRefs({name,age})
// let {name,age} = toRefs(people)
effect(() => {
app.innerHTML = `${people.name}今年${people.age}岁`
})

setTimeout(() => {
people.name = "利斯"
},1000)
</script>
</body>
</html>

可以用 Ref 包裹基础数据类型,也可以包裹引用型数据。让数据变成响应式数据,可以被 effect 收集,然后更新数据触发回调函数。

编写 Ref

首先开始编写 ref.ts,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 { isArray, isObject } from "@vue/shared";
import { trackEffect, triggerEffect } from "./effect";
import { reactive } from "./reactive";
function toReactive(value) {
return isObject(value) ? reactive(value) : value;
}

class RefImpl {
public _value;
constructor(public rawValue) {
this._value = toReactive(rawValue);
}

get value() {
return this._value;
}

set value(newValue) {
if (newValue !== this.rawValue) {
this._value = toReactive(newValue);
this.rawValue = newValue;
}
}
}

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

上面是一个初步的对象包裹,但是还没加入依赖收集。现在加入****源码学习-7****中分离的依赖收集函数和更新触发的函数。

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
......
import { isArray, isObject } from '@vue/shared'
import { trackEffect, triggerEffect } from './effect'
import { reactive } from './reactive'
function toReactive(value){
return isObject(value) ? reactive(value) : value
}

class RefImpl{
public _value
public dep = new Set()
public __v_isRef = true
constructor(public rawValue){
this._value = toReactive(rawValue)
}

get value(){
trackEffect(this.dep)
return this._value;
}

set value(newValue){
if(newValue !== this.rawValue){
this._value = toReactive(newValue);
this.rawValue = newValue;
triggetEffect(this.dep)
}
}
}

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

这样就完成了 Ref 的基本功能。现在有了包裹对象,那么就有第二中需求,结构这种需求。toRefs 和 toRef 就是 vue3 提供的函数。

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
......
class ObjectRefImpl {
constructor(public object, public key) {}

get value() {
return this.object[this.key]
}

set value(newValue) {
this.object[this.key] = newValue
}
}

export function toRef(object, key) {
return new ObjectRefImpl(object, key)
}

export function toRefs(object) {
const result = isArray(object) ? new Array(object.length) : {}

for (let key in object) {
result[key] = toRef(object, key)
}

return result
}

toRefs 就比较简单就是单个 toRef 的单个版本实现,所以基于 toRefs 就基于 toRef 实现就行。
另外可以观察到 vue3 中 Ref 包裹的对象在 template 中使用发现不需要xx.value,这是 vue3 官方做的一个优化,实际就像是 ref 包裹的反向过程。

编写 proxyRefs

上面知道了 vue3 提供了一个 proxyRefs 这样的函数,来使得 ref 对象使用更加方便了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
......
export function proxyRefs(object) {
return new Proxy(object, {
get(target, key, recevier) {
let r = Reflect.get(target, key, recevier)
return r.__v_isRef ? r.value : r
},

set(target, key, value, receiver) {
let oldValue = target[key]
if (oldValue.__v_isRef) {
oldValue.value = value
return true
} else {
return Reflect.set(target, key, value, receiver)
}
},
})
}

template 模板上通常都是 get 数据那么就是代理对象反射这个.value这样,为啥要用 Reflect 来反射,是因为 this 的指向问题。第三篇文章分析了这个问题。

1
git:[@github/MicroMatrixOrg/vue3-plan/tree/ref]