💡 [本系列Vue3源码解读文章基于3.3.4版本](https://github.com/vuejs/core/tree/v3.3.4) 欢迎关注公众号:《前端Talkking》
1、前言
原始值指的是Boolean、Number、BigInt、String、undefined和null等类型的值。在JavaScript中,原始是按值传递的,而非按引用传递,这意味着,如果一个函数接受原始值作为参数,那么实参和形参之间没有引用关系,它们是两个完全独立的值,对形参的修改不会影响实参。另外,JavaScript中的Proxy无法提供对原始值的代理,因此,想要将原始值变成响应式数据,旧必须要对其做一层包装,也就是接下来我们要介绍的ref。
2、源码实现
2.1 ref函数
既然Proxy只能代理对象,无法代理原始数据类型,那么我们将原始数据类型的数据包装一层,不就可以用Proxy代理的吗?我们来看看源码实现:
typescript
export function ref(value?: unknown) {
return createRef(value, false)
}
根据上面的源码,ref接收了一个可选的参数,这个参数就是要变成响应式数据的原始数据。它底层调用了 createRef
函数来创建 ref
对象。
2.2 createRef函数实现
typescript
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
从上面的源码得知,首先通过 isRef
方法判断传入的值是否是一个 ref
对象,如果是,则返回原始值,否则使用 RefImpl
类创建一个 ref
对象。
2.3 RefImpl类实现
typescript
class RefImpl<T> {
private _value: T
private _rawValue: T
// 依赖收集
public dep?: Dep = undefined
// ref标识
public readonly __v_isRef = true
constructor(
value: T,
public readonly __v_isShallow: boolean
) {
// 保存原始值到_rawValue
this._rawValue = __v_isShallow ? value : toRaw(value)
// 如果是对象,使用reactive将对象转为响应式的,因此将一个对象传入ref,实际上也是调用了reactive
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
// 取值的时候收集依赖
trackRefValue(this)
return this._value
}
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
// 如果值改变,才会触发依赖
if (hasChanged(newVal, this._rawValue)) {
// 更新值
this._rawValue = newVal
// 判断是否是对象,进行赋值
this._value = useDirectValue ? newVal : toReactive(newVal)
// 派发通知
triggerRefValue(this, newVal)
}
}
}
上面的代码拆解如下:
- 定义两个私有变量 :
_value
和_rawValue
,其中,_value
用来存储将原始值转换为响应式数据的值,_rawValue
用来存储原始值; - 定义两个公共变量:
dep
和__v_isRef
,其中,dep
是Set类型,用来收集副作用函数,__v_isRef
是一个只读对象,用来标识这个对象是否是ref
对象; - 初始化的时候,在构造函数中初始化
value
和_rawValue
值; getter函数
中,使用trackRefValue
函数收集当前ref
对象的依赖,并将ref
对象的_value
值返回;setter函数
中,比较旧值和新值是否发生了变化,如果发生了变化,则将新值更新到私有变量_value
属性上,然后调用triggerRefValue
函数触发副作用函数的执行。
3、ref其他方法源码实现
3.1 isRef源码实现
从2.3节可知,RefImpl
内部定义了一个私有变量 __v_isRef
来判断是否是 ref
对象,因此,我们, 可以借助 __v_isRef
变量来实现 isRef
方法,如下代码所示:
typescript
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true)
}
3.2 解决响应式丢失问题源码实现
首先我们看一个例子:
typescript
// obj 是响应式数据
const obj = reactive({ foo: 1, bar: 2 })
// 将响应式数据展开到一个新的对象 newObj 中
const newObj = {
...obj
}
effect(() => {
// 在副作用函数内通过新的对象 newObj 读取 foo 属性值
console.log(newObj.foo)
})
// 很显然,此时修改 obj.foo 并不会触发响应
obj.foo = 100
以上示例的表现是,当我们修改响应式数据 obj.foo
的值时,不会触发副作用函数重新执行。请问为什么呢?
这时因为:
typescript
const newObj = {
...obj
}
其实相当于:
typescript
const newObj = {
foo: 1,
bar: 2
}
可以发现,其实就是返回了一个普通的对象,因此不具有响应式的能力。为了解决改问题,我们可以封装 toRef
函数,将响应式数据的某个属性创建成一个新的 ref
。
3.2.1 toRef源码实现
typescript
function propertyToRef(
source: Record<string, any>,
key: string,
defaultValue?: unknown
) {
const val = source[key]
return isRef(val)
? val
: (new ObjectRefImpl(source, key, defaultValue) as any)
}
该函数根据传入的键key获取对应的键值,如果该键值已经是一个 ref
对象,则直接放回,否则调用 ObjectRefImpl
类创建一个新的 ref
对象并返回。
typescript
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true
constructor(
private readonly _object: T,
private readonly _key: K,
private readonly _defaultValue?: T[K]
) {}
get value() {
const val = this._object[this._key]
return val === undefined ? this._defaultValue! : val
}
set value(newVal) {
this._object[this._key] = newVal
}
get dep(): Dep | undefined {
return getDepFromReactive(toRaw(this._object), this._key)
}
}
该函数的功能是为响应式对象的某个属性创建一个 ref
对象,和 ref
内部实现类似,它也定义了一个只读属性 __v_isRef
,用来标识对象的某个属性是否是 ref
对象。该函数同时实现了 getter
和 setter
函数。
3.2.2 toRefs源码实现
toRef
函数可以将对象的某个key转换为 ref
对象,但是如果想讲对象所有的key转换为 ref
对象该如何处理呢?答案是:借助循环。
typescript
export function toRefs<T extends object>(object: T): ToRefs<T> {
if (__DEV__ && !isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`)
}
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = propertyToRef(object, key)
}
return ret
}
该函数处理的拆解过程如下:
- 如果入参是普通的对象,则开发环境会发出警告:入参只能是reactive object;
- 初始化ret:如果对象是一个数组,则初始化为
object.length
长度的数组,否则初始化为一个空对象; - 使用for...in循环遍历响应式对象object的key值,调用
toRef
依次将该key对应的值转换成ref
,并存储到普通对象ret; - 返回ret;
3.3 自动脱ref源码实现
3.3.1 问题
我们看以下示例:
typescript
const obj = reactive({ foo: 1, bar: 2 })
obj.foo // 1
obj.bar // 2
const newObj = { ...toRefs(obj) }
// 必须使用 value 访问值
newObj.foo.value // 1
newObj.bar.value // 2
以上例子中,newObj.foo
是一个 ref
对象,如果要访问它的值,还需要通过 newObj.foo.value
访问。如果我们只想通过 newObj.foo`访问呢?
3.3.1 如何解决?
在Vue3源码中,定义了一个 proxyRefs
函数来实现自动脱 ref
能力。 proxyRefs源码实现
typescript
const shallowUnwrapHandlers: ProxyHandler<any> = {
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
set: (target, key, value, receiver) => {
const oldValue = target[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
return Reflect.set(target, key, value, receiver)
}
}
}
export function proxyRefs<T extends object>(
objectWithRefs: T
): ShallowUnwrapRef<T> {
return isReactive(objectWithRefs)
? objectWithRefs
: // 如果不是响应式对象,则创建一个代理对象,拦截get操作
// 通过__v_isRef属性判断读取的值是否是ref,从而返回它的value属性值
new Proxy(objectWithRefs, shallowUnwrapHandlers)
}
在上面的函数中,处理流程如下:
- 判断了是否是响应式对象,如果不是响应式对象,则通过
Proxy
创建了一个代理对象,拦截get和set操作, - 在get函数中,如果读取的值是
ref
,则调用unref
函数返回它的value属性值从而实现自动脱ref
能力。 - 在set函数中:如果旧值为
ref
并且新值不是ref
,那么将新值设置为旧值的value属性,从而实现自动为ref设置值的能力。
通过以上的处理,proxyRefs
函数实现了自动脱 ref
能力。
在Vue3组件中,有以下示例:
typescript
const MyComponent = {
setup() {
const count = ref(0)
// 返回的这个对象会传递给 proxyRefs
return { count }
}
}
setup
函数中返回的数据经过了 handleSetupResult
函数处理,如下所示:
typescript
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
// 省略部分代码
} else if (isObject(setupResult)) {
// 省略部分代码
// 将 setup 函数所返回的数据传递给 proxyRefs 函数进行处理
// 使组件数据自动脱 ref
instance.setupState = proxyRefs(setupResult)
} else if (__DEV__ && setupResult !== undefined) {
// 省略部分代码
}
finishComponentSetup(instance, isSSR)
}
setupResult
结果经过了 proxyRefs
函数处理,实现了组件数据自动脱 ref
能力。
4、总结
ref
本质上是一个"包裹对象",由于Proxy无法对原始值进行代理,因此我们需要使用一层对象进行包裹,间接实现原始值的响应式处理。
ref
除了能够提供原始值的响应式方案,还能用来解决响应式丢失的问题。为了解决该问题,实现了 toRef
和 toRefs
这两个方法。
最后,为了减轻用户使用的心智负担,提供了 proxyRefs
方法来实现自动脱ref能力,这样,用户在模板中使用响应式数据时,就不需要关心一个值是不是 ref
了。
5、参考资料
[1] Vue官网
[2] Vuejs设计与实现
[3] Vue3源码