『引言』
🌟ref和reactive都是Vue3中新增的响应式API,用于实现组件的数据响应式更新。那它们二者分别是怎么使用的?二者的区别又是什么呢?……🤔,接下来我们带着疑问,一起来探索一下。💪💪
『Ref函数👇』
『作用』
一般用来定义一个基本类型的响应式数据(String、Number、Boolean……),返回的是一个ref对象,对象中有一个value属性。如果需要对数据进行操作,需要使用该ref对象的value属性。
当然,ref也可以接受对象类型。将通过 reactive()转为具有深层次响应式的对象。
ref响应式原理是依赖于Object.defineProperty()的get()和set()。
『语法』
const xxx = ref()
『⚠️注意』
- 对数据进行操作:
xxx.value - 模板中读取数据:
<div>{{ xxx }}</div> - 在ref接受对象类型时,若要避免这深层次的转换,请使用
shallowRef()来替代。
『示例』
关于ref()基本的知识了解的差不多了,现在我们就动手使用ref(),实现一个效果。
如下所示👇
那话不多说,下面我们来动手实现一下💪💪
<template>
<div>
<h1>人物简介</h1>
<p>姓名:{{name}}</p>
<p>年龄:{{age}}岁</p>
<p>爱好:{{hobbies.join('、')}}</p>
<p>地址:{{address.provice}} - {{ address.city }} </p>
<p>描述:{{description}}</p>
<button @click="modifyInfo">
修改信息
</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const name = ref('pupu')
const age = ref(10)
const hobbies = ref(['唱歌', '画画'])
const address = ref({
provice: '浙江省',
city: '杭州市'
})
const description = ref('一点也不可爱,不喜欢吃蜂蜜!')
const modifyInfo = () => {
name.value = 'wnxx'
age.value = 3
hobbies.value = ['打羽毛球', '旅游']
address.value.provice = '云南省'
address.value.city = '丽江市'
description.value = '非常的可爱,特别喜欢吃蜂蜜!'
console.log(name,hobbies,address)
}
</script>
上面的示例看起来非常的简单,没有很复杂,并且从代码中我们可以看出数据类型举例的很全面,包括值类型,字符等等,还有数组、对象类型。
我们在点击修改信息按钮之后,在控制台打印了一下name,hobbies,addres三个信息。
如下图所示👇:
『Reactive函数👇』
『作用』
定义一个对象类型的响应式数据,reactive定义的响应式数据是"深层次的"。
底层本质是将传入的数据包装成一个Proxy。
『语法』
const 代理对象 = reactive(源对象)
const proxy = reactive(obj)
『⚠️注意』
- 对数据进行操作和模板中读取数据:
都不需要.value - 若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,请使用 shallowReactive() 作替代。
- 不能通过 ...data 方式,这样会丢失响应式。
『示例』
还是实现上述ref()实现的一个效果。
如下所示👇
<template>
<div>
<h1>人物简介</h1>
<p>姓名:{{data.name}}</p>
<p>年龄:{{data.age}}岁</p>
<p>爱好:{{data.hobbies.join('、')}}</p>
<p>地址:{{data.addres.provice}} - {{data.addres.city}}</p>
<p>描述:{{data.description}}</p>
<button @click="modifyInfo">
修改信息
</button>
</div>
</template>
<script setup>
import { reactive } from 'vue'
const data = reactive ({
name: 'pupu',
age: 10,
hobbies: ['唱歌', '画画'],
addres: {
provice: '浙江省',
city: '杭州市'
},
description: '一点也不可爱,不喜欢吃蜂蜜!'
})
const modifyInfo = () => {
data.name = 'wnxx'
data.age = 3
data.hobbies = ['打羽毛球', '旅游']
data.addres.provice = '云南省'
data.addres.city = '丽江市'
data.description = '非常的可爱,特别喜欢吃蜂蜜!'
console.log(data.name,data.hobbies,data.addres)
}
</script>
同样的,点击修改信息按钮之后,在控制台打印了一下name,hobbies,addres三个信息。打印出来的结果明显会发现是一个Proxy对象。
如下图所示👇:
『总结👇』
我们通过几个方面对ref()和reactive()进行一下对比,这样会更方便理解。
『ref和reactive的关系』
ref(obj)等价于reactive({value: obj})
『ref()和reactive()对比』
『定义数据』
ref()主要用于定义基本数据类型。
reactive()主要用于定义对象类型(数组类型)。
ref()也可以定义对象或数组类型,内部会通过reactive转为代理对象。
『使用方式』
ref()在对数据进行操作时,需要.value,模版中不需要。
reactive()则都不需要。
『原理』
ref()本质是Object.defineProperty()的get()和set()。
reactive()使用proxy实现数据代理,并通过Reflect操作源对象内部的数据。
『答疑1』
为什么ref在对数据进行操作时,需要.value🤔❓
原因就是ref是通过.value属性的get和set来实现响应式的。
『ref源码分析』
『ref』
入口函数如下:
export function ref(value?: unknown) {
return createRef(value, false)
}
『createRef』
接下来走的是createRef()这个方法。
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
createRef()有两个参数,一个是rawValue传入的基本数据类型的默认值,另一个是shallow是否是深层次响应的boolean值。
isRef()是判断传入的rawValue是否是ref对象,是则直接返回;如果传入的rawValue不是ref对象,则返回一个RefImpl实例。
最后会调用RefImpl类构造函数,将rawValue, shallow传入。
『new RefImpl 生成实例对象』
class RefImpl<T> {
// _value:表示ref接收的最新的值
private _value: T
// 私有的_rawValue:表示存放ref旧值
private _rawValue: T
public dep?: Dep = undefined
// 公共的只读变量__v_isRef:表示标识该对象是一个ref响应式对象的标记
public readonly __v_isRef = true
// __v_isShallow:表示是否是浅层次响应的属性
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
}
在RefImpl类构造函数中,会判断是否是浅层次响应的属性,如果是,则直接把value的值赋值,不是则使用toReactive()。
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
toReactive()就是看传入的value是否是引用类型,是则用reactive(),不是则直接返回。
『get方法』
当通过ref.value读取ref值时,就走get方法。
get value() {
trackRefValue(this)
return this._value
}
在get方法中会调用trackRefValue方法。
export function trackRefValue(ref: RefBase<any>) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref)
if (__DEV__) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
} else {
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
}
『set方法』
当修改ref.value的值时,就走set方法,会对新旧值进行比较,不同就需要更新,新旧值更新之后会调用triggerRefValue方法。
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)
}
}
triggerRefValue方法中,让依赖该 ref 的副作用函数执行更新。
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
const dep = ref.dep
if (dep) {
if (__DEV__) {
triggerEffects(dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(dep)
}
}
}
关于ref源码分析就到这🔚了。
『答疑2』
上面我们介绍reactive()的时候,注意点写到不能通过...data的方式,这样会丢失响应式。那这是为什么❓具体是什么原因❓如何解决这个问题🤔❓
首先我们先来看一看下面的这一段源码。
function createReactiveObject(
target: Target, // 传入的目标
isReadonly: boolean, // 是否只读
baseHandlers: ProxyHandler<any>, // 实现代理的核心,是将对象转换成代理对象的一些程序
collectionHandlers: ProxyHandler<any>,//是将集合、数组转换成代理对象的一些程序
proxyMap: WeakMap<Target, any> // 存储 target 对象的
) {
if (!isObject(target)) { // 先判断 reactive 代理的是不是一个对象,因为proxy只能代理一个对象
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 优化:如果目标数据已经被代理了,直接返回
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// proxyMap 中已经存入过 target,直接返回
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 只有特定类型的值才能被 observe.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 如果上面的条件都不满足就通过 proxy 来代理一个响应式对象
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
这段源码是实现响应式代理的核心方法:createReactiveObject(),主要是通过调用createReactiveObject()函数来创建reactive对象。
从上面的源码中我们可以看出,该函数会先判断reactive()传入的参数是否为一个对象,如果不是则返回原值。这么说的话,其实reactive()可以传入基本类型的参数,只是这时候的数据会失去响应式。
『示例』
我们通过2个小示例来进行展示说明,这样会更加直观的感受到不一样的地方。
我们先定义一个原始的数据data,再传入reactive(),我们观察一下原始数据和响应式数据的变化。
<template>
<div>
<h1>{{ state.name }}</h1>
<button @click="modifyInfo">
修改信息
</button>
</div>
</template>
<script setup>
import { reactive } from 'vue'
let data = { name: 'pupu' }
let state = reactive(data)
const modifyInfo = () => {
data.name = 'wnxx'
console.log(state.name) // wnxx,但template里仍为pupu
}
</script>
从上面这段代码中可以看到,修改原始数据data.name,随之响应式数据state.name也会发生变化,我们点击修改信息按钮,但是会出现视图未更新的情况。
再来看一下下面这段代码👇:
<template>
<div>
<h1>{{ state.name }}</h1>
<button @click="modifyInfo">
修改信息
</button>
</div>
</template>
<script setup>
import { reactive } from 'vue'
let data = { name: 'pupu' }
let state = reactive(data)
const modifyInfo = () => {
state.name = 'wnxx'
console.log(data.name) // wnxx,template里为wnxx
}
</script>
这段代码中,修改响应式数据state.name的值,原始数据data.name也会发生变化,点击修改信息的按钮,同时视图会更新。
我们继续刚才的源码分析,在判断完目标是否为一个对象之后,会判断目标是否已经被代理过,还会判断判断 proxyMap 中是否已经存入过 target,再然后判断特定类型 getTargetType()等等。具体的可以看官方对于 reactive 函数相关的代码,点击此处查看。
『原因』
我们已经知道reactive() 函数会返回 Proxy 响应式的对象,如果直接解构的话,返回的数据不再是一个Proxy 响应式的对象,也就不具有响应式特点。
『示例』
onst target = {name: 'wnxx'}
const handler = {
get(target, key) {
if (key in target) {
return target[key]
} else {
return new ReferenceError(key + '属性不存在')
}
}
}
const data = new Proxy(target, handler)
const newdata = {...data}
console.log(newdata, 'newdata')
console.log(target, 'target')
console.log(data, 'data')
看一下控制台打印信息图:
通过查看控制台打印的信息,我们可以看到data一个Proxy对象经过 ... 解构以后的新对象
newdata 已经不是 Proxy 对象了。
『解决办法』
对于reactive解构,失去响应性问题。它的解决办法就是使用toRefs()。关于toRefs()在下一篇文章里会具体介绍的。