vue3使用响应式对象的同时,会有比较多疑问,类似:
- ref可以生成基本类型的响应式副本,也可以定义引用类型的响应式副本吗
- reactive只能接收对象吗,接收其他类型的值会有什么问题
- 具体什么时候使用ref或reactive
- 被ref包装的值为什么要用.value操作,reactive包装的值使用.value会有什么现象
- 使用ref解构会不会断开响应式的追踪,使用reactvie时结构又会是什么情况
reactive
实现响应式就是基于ES2015 Proxy
的实现的。那我们知道Proxy
有几个特点:
- 代理的对象是不等于原始数据对象
- 原始对象里头的数据和被
Proxy
包装的对象之间是有关联的。即当原始对象里头数据发生改变时,会影响代理对象;代理对象里头的数据发生变化对应的原始数据也会发生变化。 需要记住:是对象里头的数据变化,并不能将原始变量的重新赋值,那是大换血了
demo测试
测试一
let tstring = reactive("xin"); //如果是在ts环境中,会有类型异常提示
const tObj = reactive({
a: 1,
});
setTimeout(() => {
tstring = "xin2";
tObj.a = 5;
console.log("======>tstring", tstring);
console.log("======>tObj", tObj);
}, 1000);
结论:
reactive绑定不是对象的数据类型时,会直接返回原始值,同时提示waring;绑定对象时,会返回响应式Proxy代理对象。
测试二
const tstring = ref("xin");
const tObj = ref({
a: 1,
});
setTimeout(() => {
tstring.value = "xin2";
tObj.value.a = 5;
console.log("======>tstring", tstring);
console.log("======>tObj", tObj);
}, 1000);
结论:
使用ref的话
tstring
、tObj
都变成响应式的了,页面引用会更新,需要使用.value对其进行修改,打印两个变量返回了RefImpl
对象
测试三
// Composition API逻辑抽离 useGrowUp.js
import { ref, onMounted, onUnmounted } from "vue";
export default function useGrowUp() {
const timer = ref(null);
const happy = ref(100);
const money = ref(0);
function growUp() {
money.value++;
happy.value--;
};
onMounted(() => {
timer.value = setInterval(growUp, 5000);
})
onUnmounted(() => {
clearInterval(timer.value);
})
return { happy,money };
}
在vue文件中使用
<template>
<h2>模拟人生</h2>
<div>快乐:{{ happy }},财富:{{ money }}</div>
</template>
<script>
import useGrowUp from './useGrowUp.js'
setup() {
const { money, happy } = useGrowUp();
console.log('money', money)
console.log('happy', happy)
return {
money,
happy
}
}
</script>
可以看到
money
,happy
是RefImpl对象,现象是响应式的;但如果我们不想使用很多ref()
,像如下这种用一个reactive()
定义一个people返回,看看会有什么效果
const people = reactive({
timer: null,
happy: 100,
money: 0
})
...
return people;
这次只是打印出了
money
,happy
的具体值,页面已经失去了响应式,useGrowUp返回的是Proxy,是因为使用了解构const { money, happy } = useGrowUp()导致失去响应式;
测试四
<template>
<button @click="changeObj">changeObj</button>
<div>obj:{{ obj.count }}</div>
<button @click="changeReactiveObj">changeReactiveObj</button>
<div>reactiveObj:{{ reactiveObj.count }}</div>
</template>
<script lang="ts">
import { reactive } from "vue";
export default {
setup() {
const obj = {
count: 0,
};
const reactiveObj = reactive(obj);
function changeObj() {
obj.count++;
console.log(obj);
console.log(reactiveObj);
}
function changeReactiveObj() {
reactiveObj.count++;
console.log(obj);
console.log(reactiveObj);
}
return { changeObj, changeReactiveObj, reactiveObj, obj };
},
};
</script>
当点击changObj按钮时,可以看到obj
与reactiveObj
对应值都进行了改变,但视图未进行更新。
当点击changeReactiveObj按钮时,可以看到obj
与reactiveObj
对应值都进行了改变,视图也进行更新,但是会从changObj最后一次记录数据开始更新视图。
所以当响应式对象里头数据变化的时候原始对象的数据也会变化,同样,原始对象的数据变化也会引起响应式数据变化
测试五
测试reactive
是否会深度监听每一层
const t2 = reactive({
a: {
b: {
c: { name: "c" },
},
},
});
console.log(t2);
console.log(t2.a);
console.log(t2.a.b);
console.log(t2.a.b.c);
结果
reactive
是递归会将每一层包装成Proxy
对象的,深度监听每一层的property
源码解析
1、reactive
// 源码位置 packages/reactivity/src/reactive.ts
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,
reactiveMap
)
}
// ReactiveFlags枚举 该枚举类型在后续源码中戏份很大
export const enum ReactiveFlags {
SKIP = '__v_skip', // 标记是否跳过响应式
IS_REACTIVE = '__v_isReactive', // 标记是否是reactive对象
IS_READONLY = '__v_isReadonly', // 标记是否是只读对象
RAW = '__v_raw' // 标记是否原始值
}
首先入口判断是否是只读,是则直接返回
target
,后执行createReactiveObject
操作,下面看下createReactiveObject
的代码
// reactive核心创建逻辑
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, 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 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
}
- 如果不是object类型,Dev环境下会报警告,并且返回target,也就是
reactive
不支持基本类型的响应式转换; - 如果
target
有原始值标记,说明是已经响应化了,直接返回target
,并且有个例外:可以对proxy对象再进行readonly操作;
既如下这种情况是会被允许的
let obj = {
a: 1
}
let testReactive = reactive(obj)
let testReadonly = readonly(testReactive)
console.log('======>reactive', testReactive)
console.log('======>readonly', testReadonly)
- 检查
target
是不是已经有了proxy
对象的映射,有的话直接取出返回,后边代码会看到,会对生成的proxy
对象在WeakMap结构中添加一条target,proxy的映射; - 通过
getTargetType
获取target
的类型,只有白名单里的类型才能白响应式,看代码知道也就是支持Object
、Array
、Map
、Set
、WeakMap
、WeakSet
;
const enum TargetType {
INVALID = 0,
COMMON = 1,
COLLECTION = 2
}
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
function getTargetType(value: Target) {
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
? TargetType.INVALID
: targetTypeMap(toRawType(value))
}
- 执行响应化,Proxy构造函数的第二个参数handler,针对不同的类型使用不同的handler,集合类型
Map
、Set
、WeakMap
、WeakSet
使用collectionHandlers
,Object
、Array
使用baseHandlers
,我们日常开发大部分情况会用到baseHandlers
,整个的响应式系统还有收集依赖(track),触发依赖(trigger),这些操作会在handler中执行,关于handler可以单开一篇,不是本文重点 - 最后会在WeakMap结构中添加一条target,proxy的映射;
以上仅仅展示了reactive的过程,还有其他一些API,像
shallowReactive
、readonly
等等过程类似,都是调用createReactiveObject
;
2、ref
// 仅展示源码中需要的代码
export function ref<T extends object>(value: T): ToRef<T>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value, false)
}
// 创建ref
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
// 根据__v_isRef判断是否是ref
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
return Boolean(r && r.__v_isRef === true)
}
ref定义这里的写法是TS的重载写法,重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
这里很简单,看看传进来的值是不是有__v_isRef
标记,有就代表已经响应化了,直接返回值,没有就new一个RefImpl
类的实例,然后看下RefImpl
类
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly _shallow: boolean) {
this._rawValue = _shallow ? value : toRaw(value)
this._value = _shallow ? value : convert(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
triggerRefValue(this, newVal)
}
}
}
通过RefImpl
类可以看出,ref传入的值,会通过一个value进行获取,修改,这样做是因为js本身没有对基本类型监听的能力,所以需要将值放到一个载体上,对其进行get,set操作时才能进行收集依赖(track),触发依赖(trigger)。
_shallow
决定当前值的响应式是否由reactive()
创建
内部值_rawValue
存放而如果_shallow
为false直接等于value,否则对 value进行toRaw处理成proxy
对象的原始值或基本类型的值
内部值_value
存放而如果_shallow
为false直接等于value,否则通过convert
方法判断是否是Object
,是则调用reactive()
创建
get
,set
方法同理,加入了对比新旧值及收集依赖(track),触发依赖(trigger)过程
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val
值得提一句的是,ref还支持自定义get
,set
方法,既CustomRefImpl
,看了源码后,官方用例也就好理解了
class CustomRefImpl<T> {
public dep?: Dep = undefined
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
public readonly __v_isRef = true
constructor(factory: CustomRefFactory<T>) {
const { get, set } = factory(
() => trackRefValue(this),
() => triggerRefValue(this)
)
this._get = get
this._set = set
}
get value() {
return this._get()
}
set value(newVal) {
this._set(newVal)
}
}
总结
创建响应式对象api有两个,ref
和reactive
。从各种影响总的概括来说,本质是因为两个api返回的响应式对象是不同的。
const refVal = ref(1);
const reactiveVal = reactive({ name: "xin" });
console.log("refVal", refVal);
console.log("reactiveVal", reactiveVal);
// refVal RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 1, _value: 1}
// reactiveVal Proxy {name: 'xin'}
如上图可看到,使用ref
返回实例对象为RefImpl
,而reactive
返回的是一个Proxy
对象,例如影响结构的原因,其实就是因为实例RefImpl
对象结构value后,value指对应是个响应式数据,而Proxy
对象解构后自然会失去响应式。vue官方建议使用toRefs
api来将Proxy
转为一个个ref
使用,转为RefImpl
实例后,就可以使用结构了。