vue中响应式数据写法
【VUE2】
在Vue2中,所有的数据都通过一个Data进行统一的return返回,并且在data中对某个组件要用的数据进行统一的管理,常见的使用形式是这样的:
<template>
<div>
<el-form :model="form">
<el-form-item label="话术标题" prop="title">
<el-input v-model="form.title" placeholder="请输入话术标题"></el-input>
</el-form-item>
<el-form-item label="话术内容" prop="content">
<el-input
v-model="form.content"
placeholder="请输入话术内容"
></el-input>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
form: {
name: "",
title: "",
content: "",
},
};
},
}
</script>
可以看出来这里定义的内容都在一个数组中进行,或者是一个函数,将要使用的数据返回出来,这里无论怎么进行操作处理,最终进行数据代理的时候得到的都是一个对象,Vue2中直接通过defineProperty进行处理,并绑定对应的监听事件进行响应式的处理。
【VUE3】
而Vue3中,数据的定义可以是单独的,Vue可以让我们随时需要随时定义,我们需要的一个数据可能不是对象。在使用ref的时候,它最终给我们返回的东西是什么?
<template>
<div class="box">
<h1>ref demo</h1>
<div>
name: {{ name }}
</div>
</div>
</template>
<script setup>
import {ref} from 'vue';
const name = ref('hahaha');
console.log('name', name);
console.log('name.value', name.value);
</script>
<style scoped>
.box {
width: 500px;
height: 800px;
padding: 20px;
background-color: #FADBD8;
}
</style>
我们在这里定义了一个name是string类型的。
我们可以看到,如果直接打印name出来,是没有得到这个hahaha的,只有我们log .value之后才能得到这个我们对应的内容。第一个是我们直接打印出来的内容,它里面是个对象,我们可以从上面这个对象得知,ref 函数实际执行的是 createRef 方法,而该方法实际是返回了一个 RefImpl 构造函数的实例对象其实这个对象是通过 RefImpl 这个类去实例化出来的对象, ref 会返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值,这就是 ref 名称的来源。该对象只包含一个名为 value 的 property。
ref 究竟是如何实现的呢?
我们就一起来看 ref 的实现。
export function ref(value?: unknown) {
return createRef(value, false)
}
从 ref Api 的函数签名中,可以看到 ref 函数,实际执行的是 createRef 方法,接收一个任意类型的值作为它的 value 参数,并返回一个 Ref 类型的值。
export interface Ref<T = any> {
value: T
[RefSymbol]: true
_shallow?: boolean
dep?: Dep
}
Ref 接口定义了 ref 函数返回的类型签名,value 属性保存着 ref 的原始值,RefSymbol 是内部定义的唯一符号用于类型区分,还有一个标识是否为 shallowRef 的_shallow 标识标志这个这个 ref 是否为一个浅层 ref,是布尔类型的属性。
函数体内直接返回了 createRef 函数的返回值。
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
const ref: any = {}
def(ref, RefFlag, true)
def(ref, ReactiveFlags.IS_SHALLOW, shallow)
def(
ref,
'dep',
defineReactive(ref, 'value', rawValue, null, shallow, isServerRendering())
)
return ref
}
createRef 的实现也很简单,通过 createRef 创建 ref,入参为 rawValue 与 shallow,rawValue 记录的创建 ref 的原始值,而 shallow 则是表明是否为 shallowRef 的浅层响应式 api。
函数的逻辑为先使用 isRef 判断,如果传入的 rawValue 本身就是一个 ref 的话,如果是的话则直接返回这个 ref 对象。否则返回一个新创建的 RefImpl 类的实例对象。
在 createRef 中,要判断这个 val 是否已经代理过,那么如何才能知道是否已经代理过呢?
那就要来看RefImpl类了,我们可以在RefImpl类中定义一个属性,如果被代理,就让他为true。
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow: boolean) {
// 如果是 shallow 浅层响应,则直接将 _value 置为 _rawValue,否则通过 convert 处理 _rawValue
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
// 读取 value 前,先通过 track 收集 value 依赖
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
// 如果需要更新
if (hasChanged(toRaw(newVal), this._rawValue)) {
// 更新 _rawValue 与 _value
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
// 通过 trigger 派发 value 更新
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
在 RefImpl 类中,有一个私有变量 _value 用来存储 ref 的最新的值;公共的只读变量 __v_isRef 是用来标识该对象是一个 ref 响应式对象的标记与在讲述 reactive api 时的 ReactiveFlag 相同。
而在 RefImpl 的构造函数中,constructor 接受一个私有的 _rawValue 变量,存放 ref 的旧值;公共的 _shallow 变量是区分是否为浅层响应的。在构造函数内部,先判断 _shallow 是否为 true,如果是 shallowRef ,则直接将原始值赋值给 _value,为 false 的情况下,将 rawValue 进行通过 convert 进行转换 reactive 再赋值。
function convert(val) {
if (typeof val !== 'object' || val === null) { // 不是对象
return val
} else {
return reactive(val)
}
}
在 convert 函数的内部,其实就是判断传入的参数是否是一个对象,如果是一个对象则通过 reactive api 创建一个代理对象并返回,否则直接返回原参数。
get value() {
// 读取 value 前,先通过 track 收集 value 依赖
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
// 如果需要更新
if (hasChanged(toRaw(newVal), this._rawValue)) {
// 更新 _rawValue 与 _value
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
// 通过 trigger 派发 value 更新
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
__v_isRef 标识 一个对象是否是 ref,当我们在vue当中去执行一个ref .value读取 value 的时候,它就会触发这里的get value ,在 getter 中会先通过 track 收集该 ref 对象的 value 的依赖,收集完毕后返回该 ref 的值。
<template>
<div class="box">
<h1>ref demo</h1>
<div>
name: {{ name }}
</div>
</div>
</template>
<script setup>
import {ref} from 'vue';
const name = ref('hahaha');
console.log('name', name);
console.log('name.value', name.value);
setTimeout(() => {
name.value = 'xixixi'
}, 1000);
</script>
当我们触发修改 ref 的值的时候,就会触发这里的set value,会将新旧 value 进行比较,如果值不同需要更新,则先更新新旧 value,之后通过 trigger 派发该 ref 对象的 value 属性的更新,让依赖该 ref 的副作用函数执行更新。通过这样一个机制,其实就实现了一个响应式。
为什么要处理set value 跟 get value 呢?
<script setup>
const obj = {};
cons proxyObj = new Proxy(obj, {
set() {
console.log('触发了set');
},
get() {
console.log('触发了get');
}
})
</script>
其实就是去模拟Proxy一样的机制,在Proxy里面就是监听了getter和setter的行为,但是由于Proxy是不能处理const name = ref('hahaha');像字符串这种原始数据类型,所以class RefImpl这里通过get value 和 set value的形式来模拟Proxy里面的getter和setter,这样子就使得原始数据类型也可以监听对应的getter和setter。
另外我们还需知道,对于基本类型的数据 ref 是不具备数据监听的,当赋值或修改值时主动触发了 get 和 set 方法。之后执行 effect 函数,传入一个匿名函数,接着执行赋值行为触发 get 方法。
get value() {
// 依赖收集
trackRefValue(this)
return this._value
}
get 方法核心 trackRefValue(this) 实际触发了 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()))
}
}
}
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack({
effect: activeEffect!,
...debuggerEventExtraInfo!
})
}
}
}
这块逻辑同 reactive,给指定属性绑定对应的 fn,目的是 dep 对象与 ReactiveEffect 相关联,完成整个依赖收集的过程。
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
// 新旧值比较
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
// 依赖触发
triggerRefValue(this, newVal)
}
}
set 方法中 triggerRefValue(this, newVal) 进行依赖触发。
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
if (ref.dep) {
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(ref.dep)
}
}
}
triggerRefValue 方法实际执行了 triggerEffects。
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
最终执行的是每个 effect.run 方法,即传入的匿名函数,从而触发赋值操作,此时整个依赖触发的过程完成。
Ref为什么需要.value
const name = ref('hahaha');
console.log('name.value', name.value);
name.value = 'xixixi'
比如我们name.value的时候,才能够走到get value这个函数来监听它读取值的这个行为。当我们name.value = 'xixixi'赋值或者修改值的时候,才可以走到set value这样一个函数来监听它设置值的行为。我们要拿到ref对象的值,要通过.value访问,但是.value其实是一个函数, 在这里的get就是让我们不用手动调用函数,而是直接访问。
简单总结:
ref 其实就是 reactive 包了一层,读取值要通过 ref.value 进行读取,同时进行 track ,而设置值的时候,也会先判断相对于旧值是否有变化,有变化才进行设置,以及 trigger。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。