vue3正式发布以后,相信有很多小伙伴都迫不及待的体验了一波,在熟悉新的composition api的同时,我们发现官方文档提供了两种设置响应式数据的API:ref()和reactive(),官方文档中对这两个API的一些描述总感觉差点意思,有很多同学得出结论ref()用于基本类型的响应式处理,reactive()用于引用类型的响应式处理,实际是否有这样呢?今天让我们从源码看看是不是能推导出相对简洁实用的用法
体验了一段时间vue3,在阅读官方文档时,有些新的API说明一带而过,导致在使用中大家的用法五花八门,也会出现一些疑问,比如:
- ref可以生成基本类型的响应式副本,也可以定义引用类型的响应式副本吗
- reactive只能接收对象吗,接收其他类型的值会有什么问题
- 具体什么时候使用ref或reactive
- 被ref包装的值为什么要用.value操作,reactive包装的值使用.value会有什么现象
- 解构会断开响应式的追踪
等等问题
一、测试
测试一
import { reactive, ref } from "vue";
let test = reactive('zhangsan');
let testObj = reactive({
a: 1
})
setTimeout(() => {
test = 'lisi'
testObj.a = 5
console.log('======>test', test)
console.log('======>testobj', testObj)
}, 2000)
结论是会发现
testObj都变成响应式的了,test并没有响应式更新,打印变量发现test就是一个普通的字符串,而testObj是一个Proxy对象,同时报了一个waring
测试二
import { reactive, ref } from "vue";
let test = ref('zahngsan');
let testObj = ref({
a: 1
})
setTimeout(() => {
test.value = 'lisi'
testObj.value.a = 5
console.log('======>test', test)
console.log('======>testobj', testObj)
}, 2000)
使用ref的话
test、testObj都变成响应式的了,页面引用会更新,不过得需要使用.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的具体值,页面已经失去了响应式,是因为使用了解构const { money, happy } = useGrowUp();
下面就针对这些现象,看看源码里做了什么事情
二、源码解析
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)
}
}
三、结论及建议
至此,本文对vue3.0中重要的两个api
ref,reactive主要的执行过程就分析完了,可以看出
-
reactive只对引用类型进行响应化处理; -
ref可以对任何类型进行响应化处理,但是缺点也很明显,需要用.value来操作,这个问题在最新的提案(中文RFC,英文原版,知乎讨论)中尤大提出解决方案(ref sugar)了,使用一种新的语法糖,具体如下:
ref:count = 1
count++
目前社区还在争论中,不过最后无论以什么方式出现,这个.value应该会解决的;
使用建议:
1、很多时候可以使用ref来创建任何响应式数据;
2、如果有很多基本类型需要定义的,可以包装成一个对象,一次性使用reactive转化成响应式
3、解决reactive生成的proxy对象解构会失去响应式的问题,可以使用toRefs响应式对象转换为一个个的ref使用,在测试三中,改成如下即可
const people = reactive({
timer: null,
happy: 100,
money: 0
})
...
return toRefs(people)
如果有任何问题,欢迎留言,也欢迎关注我的博客
(完)