前言
最近一直重新学习Vue3,看到composition API了,尝试结合源码看看,理解深刻一些。本文先来看看 reactive 和 ref 两个API
一、reactive
官方定义
我们先来看官方对于reactive的解释,官方的解释也非常简单
返回对象的响应式副本
但从这句话我们可以得到以下信息
reactive接受一个对象作为参数- 其返回值是经
reactive函数包装过后的数据对象,这个对象具有响应式
产生一些疑问
但同样会有一些疑问
比如,reactive的参数只能传递一个对象吗,如果传递其他值会怎么样?
比如,返回的响应式数据的本质是什么,为啥就能让数据变成响应式?
比如,"副本"是不是意味着响应式数据与原始数据没有关联?
比如,返回的响应式副本里头的数据是深度响应式吗,即是否递归监听对象的所有属性?等等
通过测试解决疑问
带着这些疑问我们一起来看
首先,通过reactive创建一个响应数据
import { reactive } from "vue";
export default {
setup() {
const state = reactive({
count: 0,
});
},
};
如上代码就可以创建一个响应式数据state,我具体来看一下这个
console.log(state)

可以看见,返回的响应副本state其实就是Proxy对象。所以reactive实现响应式就是基于ES2015 Proxy的实现的。那我们知道Proxy有几个特点:
- 代理的对象是不等于原始数据对象
- 原始对象里头的数据和被
Proxy包装的对象之间是有关联的。即当原始对象里头数据发生改变时,会影响代理对象;代理对象里头的数据发生变化对应的原始数据也会发生变化。 需要记住:是对象里头的数据变化,并不能将原始变量的重新赋值,那是大换血了
因此,既然reactive实现响应式是基于Proxy的实现的,那我们大胆猜测,原始数据与相应数据也是有关联的。那我们来测试一下
<template>
<button @click="change">
{{ state.count }}
</button>
</template>
<script>
import { reactive } from "vue";
export default {
setup() {
const obj = {
count: 0,
};
const state = reactive(obj);
function change(){
++state.count
console.log(obj);
console.log(state);
}
return { state,change};
},
};
</script>
以上代码测试结果如下

验证,确实当响应式对象里头数据变化的时候原始对象的数据也会变化
如果反过来,结果也是一样
// ++state.count
++obj.count;
当响应式对象里头数据变化的时候原始对象的数据也会变化
那问题来了,我们操作数据的时候通过谁来操作呢?
官方的建议是
建议只使用响应式代理,避免依赖原始对象
再来解决另外一个问题看看reactive是否会深度监听每一层呢?
const state = reactive({
a:{
b:{
c:{name:'c'}
}
}
});
console.log(state);
console.log(state.a);
console.log(state.a.b);
console.log(state.a.b.c);

可以看到结果reactive是递归会将每一层包装成Proxy对象的,深度监听每一层的property
最后测试一下如果reactive传递是非对象而是原始值会怎么样
const state = reactive(0);
console.log(state)
结果是,原始值并不会被包装,所以也没有响应式特点
源码解析
下面,我们看看reactive的源码吧
源码目录位置:vue-next\packages\reactivity\src\reactive.ts
直接找到reactive的类型声明:
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
可以看到reactive接受一个参数target,target的类型是泛型T,而T类型是extends object,简单来说接受的参数target的类型是object类型或者时继承自object类的子类类型
返回值的类型的UnwrapNestedRefs<T>
看看UnwrapNestedRefs<T>类型
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
使用type关键字声明类型UnwrapNestedRefs<T>,这里有个三目运算符,用于进一步判断T;如果传入的T属于Refs类或者其子类,那么返回传入的T,否者就是UnwrapRef<T>
下面具体看看reactive方法的定义
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
接受一个类型为object参数,当传入对象是只读,返回本身。这里的as关键字是断言,表示传入的值一定是Target类型,里头有个ReactiveFlags.IS_READONLY,用于判断是否是只读的属性
export interface Target {
[ReactiveFlags.SKIP]?: boolean
[ReactiveFlags.IS_REACTIVE]?: boolean
[ReactiveFlags.IS_READONLY]?: boolean
[ReactiveFlags.RAW]?: any
}
如果传递的对象是普通对象(不是readonly),则执行创建响应式对象函数createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers)
该方法比较长,是reactive的核心方法,所以还是得读一下源码
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
可以看到除了几种特殊情况返回target本身之外,就返回proxy,proxy就是通过new Proxy构造函数构建出来的。这里也进一步证明了reactive的响应式功能确实是通过Proxy实现的
可以看一样Proxy的定义
interface ProxyHandler<T extends object> {
getPrototypeOf? (target: T): object | null;
setPrototypeOf? (target: T, v: any): boolean;
isExtensible? (target: T): boolean;
preventExtensions? (target: T): boolean;
getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined;
has? (target: T, p: PropertyKey): boolean;
get? (target: T, p: PropertyKey, receiver: any): any;
set? (target: T, p: PropertyKey, value: any, receiver: any): boolean;
deleteProperty? (target: T, p: PropertyKey): boolean;
defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean;
enumerate? (target: T): PropertyKey[];
ownKeys? (target: T): PropertyKey[];
apply? (target: T, thisArg: any, argArray?: any): any;
construct? (target: T, argArray: any, newTarget?: any): object;
}
interface ProxyConstructor {
revocable<T extends object>(target: T, handler: ProxyHandler<T>): { proxy: T; revoke: () => void; };
new <T extends object>(target: T, handler: ProxyHandler<T>): T;
}
declare var Proxy: ProxyConstructor;
里面的具体实现方法,在createReactiveObject传参的时候就传入进来了
mutableHandlers和mutableCollectionHandlers,具体可以去`vue-next\packages\reactivity\src\baseHandlers.ts文件中看
小结
经过上面的了解,我们可以总结和回答一下最开始几个疑问了
-
reactive的参数可以传递对象也可以传递原始值。但是原始值并不会包装成响应式数据
-
返回的响应式数据的本质Proxy对象
-
返回的响应式"副本"与原始数据有关联,当原始对象里头的数据或者响应式对象里头的数据发生,会彼此相互影响。两种都可以触发界面更新,操作时建议只使用响应式代理对象
-
返回的响应式对象里头时深度递归监听每一层的,每一层都会被包装成Proxy对象
二、ref
官方定义
关于ref,官方的解释是:
接受一个内部值并返回一个响应式且可变的
ref对象
为了方便理解,下文中将内部值都称为原始数据(orgin)
简单来说ref就是:原始数据=>响应式数据 的过程
产生疑问
但有几个问题得搞明白
ref接受的原始数据是什么类型?是原始值还是引用值,还是都行?- 返回的响应式数据本质具体是什么?根据传递的数据类型不同,返回的响应式对象是否不同?
- 响应式数据改变会触发界面更新,那原始数据改变会触发界面更新吗?即原始数据和返回的响应式数据是否有关联
测试解决疑问
示例代码1:
let origin = 0; //原始数据为原始值
let count = ref(origin);
function add() {
count.value++;
}
示例代码2:
let origin = { val: 0 };//原始数据为对象
let count = ref(origin);
function add() {
count.value.val++;
}
经测试,我们发现,传递的原始数据orgin可以是原始值也可以是引用值,但是需要注意,如果传递的是原始值,指向原始数据的那个值保存在返回的响应式数据的.value中,如上count.value;如果传递的一个对象,返回的响应式数据的.value中对应有指向原始数据的属性,如上count.value.val
为了测试第二个问题,我们将上述示例中的count打出来,看返回的具体是什么
console.log(count)
console.log(count.constructor)
对比发现,不管传递数据类型的数据给ref,无论是原始值还是引用值,返回的响应式数据对象本质都是由RefImpl类构造出来的对象。但不同的是里头的value,一个是原始值,一个是Proxy对象
源码分析
到这里,不妨来读一下RefImpl类的源码
目录:vue-next\packages\reactivity\src\ref.ts
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, private readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
可以看见RefImpl class传递了一个泛型类型T,里头具体包含:
- 有个私有属性
_value,类型为T,有个公开只读属性__v_isRef值为true - 有两个方法,
get value(){}和set value(){},分别对应私有属性的读写操作,用于供外界操作value - 有一个构造函数
constructor,用于构造对象。构造函数接受两个参数:- 第一个参数
_rawValue,要求是T类型 - 第二个参数
_shallow,默认值为true
- 第一个参数
当通过它构建对象时,会给对象的_value属性赋值为 _rawValue或者convert(_rawValue)
再看convert源码如下:
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val
通过源码我们发现,最终Vue会根据传入的数据是不是对象isObject(val),如果是对象本质调用的是reactive,否则返回原始数据
下面再来验证最后一个问题就是:通过ref包装的结果,当原始数据改变时会触发界面更新吗?即原始数据和返回的响应式数据是否有关联?
示例代码3
let origin = 0; //原始值
let count = ref(origin);
function add() {
origin++
console.log(count.value)
}
示例代码4
let origin = { val: 0 }; //引用值
let count = ref(origin);
function add() {
origin++
console.log(count.value.val)
}
发现,无论传入给ref的原始数据是原始值还是引用值,当原始数据发生修改时,并不会影响响应式数据,更不会触发界面UI的更新
实例代码5
let origin = 0;
let count = ref(origin);
function add() {
count.value++
console.log(origin)
}
上述代码,无论count修改多少次,origin一直是0
即如果响应式数据发生改变,对应界面UI是会自动更新的,注意不影响原始数据
小结
简单小结一下:
-
ref本质是将一个数据变成一个对象,这个对象具有响应式特点
-
ref接受的原始数据可以是原始值也可以是引用值,返回的对象本质都是RefImpl类的实例`
-
无论传入的原始数据时什么类型,当原始数据发生改变时,并不会影响响应数据,更不会触发UI的更新。但当响应式数据发生改变,对应界面UI是会自动更新的,注意不影响原始数据。所以ref中,原始数据和经过ref包装后的响应式数据是无关联的
END
以上就是关于和reactive和ref所有内容~
源码看得比较少,如有问题欢迎留言告知~