记录--源码视角,Vue3为什么推荐使用ref而不是reactive
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
ref
和 reactive
是 Vue3 中实现响应式数据的核心 API。ref
用于包装基本数据类型,而 reactive 用于处理对象和数组。尽管 reactive
似乎更适合处理对象,但 Vue3 官方文档更推荐使用 ref
。
我的想法,ref
就是比reactive
好用,官方也是这么说的,不服来踩!下面我们从源码的角度详细讨论这两个 API,以及 Vue3 为什么推荐使用ref
而不是reactive
?
ref 的内部工作原理
ref
是一个函数,它接受一个内部值并返回一个响应式且可变的引用对象。这个引用对象有一个 .value
属性,该属性指向内部值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | // 深响应式 export function ref (value?: unknown) { return createRef(value, false ) } // 浅响应式 export function shallowRef(value?: unknown) { return createRef(value, true ) } function createRef(rawValue: unknown, shallow: boolean) { // 如果传入的值已经是一个 ref,则直接返回它 if (isRef(rawValue)) { return rawValue } // 否则,创建一个新的 RefImpl 实例 return new RefImpl(rawValue, shallow) } class RefImpl<T> { // 存储响应式的值。我们追踪和更新的就是_value。(这个是重点) private _value: T // 用于存储原始值,即未经任何响应式处理的值。(用于对比的,这块的内容可以不看) private _rawValue: T // 用于依赖跟踪的 Dep 类实例 public dep?: Dep = undefined // 一个标记,表示这是一个 ref 实例 public readonly __v_isRef = true constructor( value: T, public readonly __v_isShallow: boolean, ) { // 如果是浅响应式,直接使用原始值,否则转换为非响应式原始值 this ._rawValue = __v_isShallow ? value : toRaw(value) // 如果是浅响应式,直接使用原始值,否则转换为响应式值 this ._value = __v_isShallow ? value : toReactive(value) // toRaw 用于将响应式引用转换回原始值 // toReactive 函数用于将传入的值转换为响应式对象。对于基本数据类型,toReactive 直接返回原始值。 // 对于对象和数组,toReactive 内部会调用 reactive 来创建一个响应式代理。 // 因此,对于 ref 来说,基本数据类型的值会被 RefImpl 直接包装,而对象和数组 // 会被 reactive 转换为响应式代理,最后也会被 RefImpl 包装。 // 这样,无论是哪种类型的数据,ref 都可以提供响应式的 value 属性, // 使得数据变化可以被 Vue 正确追踪和更新。 // export const toReactive = (value) => isObject(value) ? reactive(value) : value } get value() { // 追踪依赖,这样当 ref 的值发生变化时,依赖这个 ref 的组件或副作用函数可以重新运行。 trackRefValue( this ) // 返回存储的响应式值 return this ._value } set value(newVal) { // 判断是否应该使用新值的直接形式(浅响应式或只读) const useDirectValue = this .__v_isShallow || isShallow(newVal) || isReadonly(newVal) // 如果需要,将新值转换为非响应式原始值 newVal = useDirectValue ? newVal : toRaw(newVal) // 如果新值与旧值不同,更新 _rawValue 和 _value if (hasChanged(newVal, this ._rawValue)) { this ._rawValue = newVal this ._value = useDirectValue ? newVal : toReactive(newVal) // 触发依赖更新 triggerRefValue( this , DirtyLevels.Dirty, newVal) } } } |
在上述代码中,ref
函数通过 new RefImpl(value)
创建了一个新的 RefImpl
实例。这个实例包含 getter 和 setter,分别用于追踪依赖和触发更新。使用 ref
可以声明任何数据类型的响应式状态,包括对象和数组。
1 2 3 4 | import { ref } from 'vue' const state = ref ({ count: 0 }) state.value.count++ |
注意,ref
核心是返回响应式且可变的引用对象,而reactive
核心是返回的是响应式代理,这是两者本质上的核心区别,也就导致了ref
优于reactive
,我们接着看下reactive
源码实现。
reactive 的内部工作原理
reactive
是一个函数,它接受一个对象并返回该对象的响应式代理,也就是 Proxy
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | function reactive(target) { if (target && target.__v_isReactive) { return target } return createReactiveObject( target, false , mutableHandlers, mutableCollectionHandlers, reactiveMap ) } function createReactiveObject( target, isReadonly, baseHandlers, collectionHandlers, proxyMap ) { if (!isObject(target)) { return target } const existingProxy = proxyMap. get (target) if (existingProxy) { return existingProxy } const proxy = new Proxy(target, baseHandlers) proxyMap. set (target, proxy) return proxy } |
reactive
的源码相对就简单多了,reactive
通过 new Proxy(target, baseHandlers)
创建了一个代理。这个代理会拦截对目标对象的操作,从而实现响应式。1 2 3 4 | import { reactive } from 'vue' const state = reactive({ count: 0 }) state.count++ |
到这里我们可以看出 ref
和 reactive
在声明数据的响应式状态上,底层原理是不一样的。ref
采用 RefImpl
对象实例,reactive
采用Proxy
代理对象。
ref 更深入的理解
当你使用 new RefImpl(value)
创建一个 RefImpl
实例时,这个实例大致上会包含以下几部分:
- 内部值:实例存储了传递给构造函数的初始值。
- 依赖收集:实例需要跟踪所有依赖于它的效果(effect),例如计算属性或者副作用函数。这通常通过一个依赖列表或者集合来实现。
- 触发更新:当实例的值发生变化时,它需要通知所有依赖于它的效果,以便它们可以重新计算或执行。
RefImpl 类似于发布-订阅模式的设计,以下是一个简化的 RefImpl
类的伪代码实现,展示这个实现过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | class Dep { constructor() { this .subscribers = new Set(); } depend() { if (activeEffect) { this .subscribers.add(activeEffect); } } notify() { this .subscribers.forEach(effect => effect()); } } let activeEffect = null ; function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null ; } class RefImpl { constructor(value) { this ._value = value; this .dep = new Dep(); } get value() { // 当获取值时,进行依赖收集 this .dep.depend(); return this ._value; } set value(newValue) { if (newValue !== this ._value) { this ._value = newValue; // 值改变时,触发更新 this .dep.notify(); } } } // 使用示例 const count = new RefImpl(0); watchEffect(() => { console.log(`The count is : ${count.value}`); // 订阅变化 }); count.value++; // 修改值,触发通知,重新执行watchEffect中的函数 |
ref
和 reactive
尽管两者在内部实现上有所不同,但它们都能满足我们对于声明响应式变量的要求,但是 reactive
却存在一定的局限性。
reactive 的局限性
在 Vue3 中,reactive
API 通过 Proxy
实现了一种响应式数据的方法,尽管这种方法在性能上比 Vue2 有所提升,但 Proxy
的局限性也导致了 reactive
的局限性,这些局限性可能会影响开发者的使用体验。
仅对引用数据类型有效
reactive
主要适用于对象,包括数组和一些集合类型(如 Map
和 Set
)。对于基础数据类型(如 string
、number
和 boolean
),reactive
是无效的。这意味着如果你尝试使用 reactive
来处理这些基础数据类型,将会得到一个非响应式的对象。
1 2 | import { reactive } from 'vue' ; const state = reactive({ count: 0 }); |
使用不当会失去响应
-
直接赋值对象:如果直接将一个响应式对象赋值给另一个变量,将会失去响应性。这是因为 reactive 返回的是对象本身,而不仅仅是代理。
1 2 3 4 | import { reactive } from 'vue' ; const state = reactive({ count: 0 }); state = { count: 1 }; // 失去响应性 |
- 直接替换响应式对象:同样,直接替换一个响应式对象也会导致失去响应性。
1 2 3 4 | import { reactive } from 'vue' ; const state = reactive({ count: 0 }); state = reactive({ count: 1 }); // 失去响应性 |
- 直接解构对象:在解构响应式对象时,如果直接解构对象属性,将会得到一个非响应式的变量。
1 2 3 4 | const state = reactive({ count: 0 }); let { count } = state; count++; // count 仍然是 0 |
toRefs
函数来将响应式对象转换为 ref
对象。1 2 3 4 5 | import { toRefs } from 'vue' ; const state = reactive({ count: 0 }); let { count } = toRefs(state); count++; // count 现在是 1 |
首先来说,太不方便了!而且使用toRefs()
,将响应式变量换成 ref 的形式,那我还不如直接使用ref()
了,大家说是不是?
-
将响应式对象的属性赋值给变量:如果将响应式对象的属性赋值给一个变量,这个变量的值将不会是响应式的。
1 2 3 4 | const state = reactive({ count: 0 }) let count = state.count count++ // count 仍然是 0 |
使用 reactive
声明响应式变量的确存在一些不便之处,尤其是对于喜欢使用解构赋值的开发者而言。这些局限性可能会导致意外的行为,因此在使用 reactive
时需要格外注意。相比之下,ref
API 提供了一种更灵活和统一的方式来处理响应式数据。
为什么推荐使用 ref ?
ref()
它为响应式编程提供了一种统一的解决方案,适用于所有类型的数据,包括基本数据类型和复杂对象。以下是推荐使用 ref 的几个关键原因:
统一性
ref
的核心优势之一是它的统一性。它提供了一种简单、一致的方式来处理所有类型的数据,无论是数字、字符串、对象还是数组。这种统一性极大地简化了开发者的代码,减少了在不同数据类型之间切换时的复杂性。
1 2 3 4 5 6 7 8 9 10 11 12 | import { ref } from 'vue' ; const num = ref (0); const str = ref ( 'Hello' ); const obj = ref ({ count: 0 }); // 修改基本数据类型 num.value++; str.value += ' World' ; // 修改对象 obj.value.count++; |
深层响应性
ref
支持深层响应性,这意味着它可以追踪和更新嵌套对象和数组中的变化。这种特性使得 ref
非常适合处理复杂的数据结构,如对象和数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import { ref } from 'vue' ; const obj = ref ({ user: { name: 'xiaoming' , details: { age: 18 } } }); // 修改嵌套对象 obj.value.user.details.age++; |
shallowRef
来放弃深层响应性。1 2 3 | const shallowObj = shallowRef({ details: { age: 18, }, }); |
灵活性
ref
提供了高度的灵活性,尤其在处理普通赋值和解构赋值方面。这种灵活性使得 ref 在开发中的使用更加方便,特别是在进行复杂的数据操作时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import { ref } from 'vue' ; const state = ref ({ count: 0, name: 'Vue' }); // 解构赋值 const { count, name } = state.value; // 直接修改解构后的变量 count++; name = 'Vue3' ; // 替换整个对象 state.value = { count: 10, name: 'Vue4' }; |
总结
ref
在 Vue3 中提供了一种更统一、灵活的响应式解决方案,还能避免了 reactive
的某些局限性。希望这篇文章对你有所帮助,有所借鉴。大家怎么认为呢,评论区我们一起讨论下!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 【全网最全教程】使用最强DeepSeekR1+联网的火山引擎,没有生成长度限制,DeepSeek本体
2023-02-20 记录--什么时候会阻塞HTML文档渲染过程?