vue3 ref实现原理
我们都知道在vue中实现响应式有:1、reactive
;2、ref
。在上一篇文章中,我们了解了vue3响应式实现和reactive的工作原理,这篇文章中我们来看看ref是怎样实现响应式的。
我们知道,对于 reactive
函数而言,它会把传入的 object 作为 proxy 的 target 参数,而对于 proxy 而言,他只能代理对象,而不能代理简单数据类型,所以说:我们不可以使用 reactive 函数,构建简单数据类型的响应性。reactive
只能用于对象类型(包括对象、数组以及 Map 和 Set 等集合类型),不能用于原始类型如 string、number 或 boolean。下面我们从源码来看看ref是怎么运行的。
ref源码
这里测试ref创建对象类型的响应式数据
我们从Vue中导入ref方法来测试一下,下面是测试代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const { ref, effect} = Vue
// 这里测试ref创建对象类型的响应式数据
const obj = ref({
name: '张三'
})
effect(()=> {
document.querySelector('#app').innerHTML = obj.value.name
})
setTimeout(()=>{
obj.value.name = '李四'
},2000)
</script>
</html>
首先是ref
方法,它接收一个value,通过createRef
创建一个RefImpl
类型的实例
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
// isRef通过判断__v_isRef属性来判断当前传入的rawValue是否已经创建过,如果创建过就直接返回就信
if (isRef(rawValue)) {
return rawValue
}
// 创建RefImpl类实例
return new RefImpl(rawValue, shallow)
}
下面来看看RefImpl
这个类:
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
// 从上面的代码来看__v_isShallow是false,所以执行的是toReactive。该方法将对象类型数据通过reactive方法创建响应式对象,将普通类型数据直接返回赋值给this._value
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
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)
}
}
}
RelImpl类的构造函数中,执行了一个 toReactive
的方法,传入了 value
并把返回值赋值给了 this._value
,那么我们来看看 toReactive
的作用:
// 判断当前传入创建的参数是否是对象,如果是对象就通过reaction()方法来创建响应式对象,如果不是对象类型的数据就直接返回原值。
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
由以上代码ref创建对象类型数据逻辑可知:
-
对于
ref
而言,主要生成了RefImpl
的实例 -
在构造函数中对传入的数据进行了处理:复杂数据类型:转为响应性的
proxy
实例,简单数据类型:不去处理。 -
RefImpl
分别提供了get value()
、set value()
以此来完成对getter
和setter
的监听,注意这里并没有使用proxy
。
当 ref
函数执行完成之后,测试实例开始执行 effect
函数。effect
函数我们上一篇文章说过它的执行流程,我们知道整个 effect
主要做了3 件事情:
-
生成
ReactiveEffect
实例 -
触发
fn
方法,从而激活getter
-
建立了
targetMap
和activeEffect
之间的联系
-
-
dep.add(activeEffect)
-
activeEffect.deps.push(dep)
-
通过以上可知,effect
中会触发 fn
函数,也就是说会执行 obj.value.name
,那么根据 get value()
机制,此时会触发 RefImpl
的 get value()
方法。
我们来看看get value()
的执行:
get value() {
// 触发trackRefValue方法,收集依赖
trackRefValue(this)
return this._value
}
export function trackRefValue(ref: RefBase<any>) {
// 触发 trackEffects 函数,并且在此时为 ref 新增了一个 dep 属性, trackEffects 主要的作用就是:收集所有的依赖
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()))
}
}
}
至此 get value()
执行完毕,然后就是执行:
setTimeout(()=>{
obj.value.name = '李四'
},2000)
这里执行时可以看成:
const value = obj.value
value.name = '李四'
所以在这里并不会触发RefImpl
的set value()
,而是触发get value()
。
在 get value()
函数中:
-
再次执行
trackRefValue
函数: 但是此时activeEffect
为undefined
,所以不会执行后续逻辑 -
返回
this._value
:通过 构造函数,我们可知,此时的this._value
是经过toReactive
函数过滤之后的数据,在当前实例中为proxy
实例。 -
get value()
执行完成。
总结:
-
对于
ref
函数,会返回RefImpl
类型的实例 -
在该实例中,会根据传入的数据类型进行分开处理
-
-
复杂数据类型:转化为
reactive
返回的proxy
实例 -
简单数据类型:不做处理
-
-
无论我们执行
obj.value.name
还是obj.value.name = xxx
本质上都是触发了get value()
-
之所以会进行 响应性 是因为
obj.value
是一个reactive
函数生成的proxy
下面测试ref创建简单数据类型的响应式数据
下面是测试代码:
<script>
const { ref, effect} = Vue
const obj = ref('张三')
effect(()=> {
document.querySelector('#app').innerHTML = obj.value
})
setTimeout(()=>{
obj.value = '李四'
},2000)
</script>
代码运行过程中整个 ref
初始化的流程与之前完全相同,但是有一个不同的地方,需要 特别注意:因为当前不是复杂数据类型,所以在 toReactive
函数中,不会通过 reactive
函数处理 value
。所以 this._value
不是 一个 proxy
。
接着是effect函数执行:整个 effect
函数的流程与之前创建对象数据类型响应式相同。effect函数中obj.value
会触发get value()
这里的流程也和上面相同就略过。
不同的是在简单数据类型中修改obj.value = '李四'
时触发的是set value()
。这里也是 ref
可以监听到简单数据类型响应性的关键。
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
// 比较新值与旧值是否发生变化,如果发生变化就触发依赖 triggerRefValue。
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
// 在triggerRefValue里又触发了triggerEffects去触发依赖,完成响应性(triggerEffects在《vue3响应式实现》中看过)
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)
}
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
// 执行 computed 依赖
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))
}
// 最主要的是以下代码,这里是执行依赖函数:如果 effect 是一个调度器,就会执行 scheduler
if (effect.scheduler) {
effect.scheduler()
} else {
// 否则直接执行 effect.run()
effect.run()
}
}
}
至此测试代码完成,由以上代码可知:
-
简单数据类型的响应性,不是基于
proxy
或Object.defineProperty
进行实现的,而是通过:set value()
语法,将对象属性绑定到查询该属性时将被调用的函数 上,使其触发xxx.value = '李四'
属性时,其实是调用了set value('李四')
函数。 -
在
value
函数中,触发依赖
所以,我们可以说:对于 ref
标记的简单数据类型而言,它其实 “并不具备响应性”,所谓的响应性只不过是因为我们 主动触发了 value
方法 而已。