包阅导读总结
1.
关键词:Vue3、ref、reactive、响应式、局限性
2.
总结:本文从源码视角探讨 Vue3 中 ref 和 reactive 实现响应式数据的原理,指出 ref 适用所有数据类型,更具统一性、深层响应性和灵活性,还避免了 reactive 的局限性。
3.
主要内容:
– Vue3 中的响应式核心 API:ref 和 reactive
– ref 用于包装基本数据类型,返回响应式且可变的引用对象,通过 RefImpl 实例实现,包含依赖收集和触发更新。
– reactive 用于处理对象和数组,通过 Proxy 代理对象实现。
– ref 的更深入理解
– 类似于发布-订阅模式,包含依赖收集和触发更新的功能。
– reactive 的局限性
– 仅对引用数据类型有效,对基础数据类型无效。
– 直接赋值、替换、解构对象或对象属性赋值给变量可能会失去响应性,可使用 toRefs 函数解决。
– 推荐使用 ref 的原因
– 统一性,适用于所有类型数据。
– 支持深层响应性。
– 灵活性,处理普通赋值方便。
思维导图:
文章地址:https://mp.weixin.qq.com/s/YTDoMT8Q0x7dNOCtZZA5QA
文章来源:mp.weixin.qq.com
作者:Sailing
发布时间:2024/7/30 5:18
语言:中文
总字数:3137字
预计阅读时间:13分钟
评分:77分
标签:前端框架,Vue3,响应式数据,ref,reactive
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
点击公众关注号,“技术干货”及时达!
ref
和 reactive
是 Vue3 中实现响应式数据的核心 API。ref
用于包装基本数据类型,而 reactive 用于处理对象和数组。尽管 reactive
似乎更适合处理对象,但 Vue3 官方文档更推荐使用 ref
。
我的想法,ref
就是比reactive
好用,官方也是这么说的,不服来踩!下面我们从源码的角度详细讨论这两个 API,以及 Vue3 为什么推荐使用ref
而不是reactive
?
ref 的内部工作原理
ref
是一个函数,它接受一个内部值并返回一个响应式且可变的引用对象。这个引用对象有一个.value
属性,该属性指向内部值。
//深响应式
exportfunctionref(value?:unknown){
returncreateRef(value,false)
}
//浅响应式
exportfunctionshallowRef(value?:unknown){
returncreateRef(value,true)
}
functioncreateRef(rawValue:unknown,shallow:boolean){
//如果传入的值已经是一个ref,则直接返回它
if(isRef(rawValue)){
returnrawValue
}
//否则,创建一个新的RefImpl实例
returnnewRefImpl(rawValue,shallow)
}
classRefImpl<T>{
//存储响应式的值。我们追踪和更新的就是_value。(这个是重点)
private_value:T
//用于存储原始值,即未经任何响应式处理的值。(用于对比的,这块的内容可以不看)
private_rawValue:T
//用于依赖跟踪的Dep类实例
publicdep?:Dep=undefined
//一个标记,表示这是一个ref实例
publicreadonly__v_isRef=true
constructor(
value:T,
publicreadonly__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 正确追踪和更新。
//exportconsttoReactive=(value)=>isObject(value)?reactive(value):value
}
getvalue(){
//追踪依赖,这样当 ref 的值发生变化时,依赖这个 ref 的组件或副作用函数可以重新运行。
trackRefValue(this)
//返回存储的响应式值
returnthis._value
}
setvalue(newVal){
//判断是否应该使用新值的直接形式(浅响应式或只读)
constuseDirectValue=
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
可以声明任何数据类型的响应式状态,包括对象和数组。
import{ref}from'vue'
letstate=ref({count:0})
state.value.count++
注意,ref
核心是返回「响应式且可变的引用对象」,而reactive
核心是返回的是「响应式代理」,这是两者本质上的核心区别,也就导致了ref
优于reactive
,我们接着看下reactive
源码实现。
reactive 的内部工作原理
reactive
是一个函数,它接受一个对象并返回该对象的响应式代理,也就是 Proxy
。
functionreactive(target){
if(target&&target.__v_isReactive){
returntarget
}
returncreateReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
functioncreateReactiveObject(
target,
isReadonly,
baseHandlers,
collectionHandlers,
proxyMap
){
if(!isObject(target)){
returntarget
}
constexistingProxy=proxyMap.get(target)
if(existingProxy){
returnexistingProxy
}
constproxy=newProxy(target,baseHandlers)
proxyMap.set(target,proxy)
returnproxy
}
reactive
的源码相对就简单多了,reactive
通过 new Proxy(target, baseHandlers)
创建了一个代理。这个代理会拦截对目标对象的操作,从而实现响应式。
import{reactive}from'vue'
letstate=reactive({count:0})
state.count++
到这里我们可以看出 ref
和 reactive
在声明数据的响应式状态上,底层原理是不一样的。ref
采用 RefImpl
对象实例,reactive
采用Proxy
代理对象。
ref 更深入的理解
当你使用new RefImpl(value)
创建一个RefImpl
实例时,这个实例大致上会包含以下几部分:
-
「依赖收集」:实例需要跟踪所有依赖于它的效果(effect),例如计算属性或者副作用函数。这通常通过一个依赖列表或者集合来实现。 -
「触发更新」:当实例的值发生变化时,它需要通知所有依赖于它的效果,以便它们可以重新计算或执行。
RefImpl 类似于发布-订阅模式的设计,以下是一个简化的RefImpl
类的伪代码实现,展示这个实现过程:
classDep{
constructor(){
this.subscribers=newSet();
}
depend(){
if(activeEffect){
this.subscribers.add(activeEffect);
}
}
notify(){
this.subscribers.forEach(effect=>effect());
}
}
letactiveEffect=null;
functionwatchEffect(effect){
activeEffect=effect;
effect();
activeEffect=null;
}
classRefImpl{
constructor(value){
this._value=value;
this.dep=newDep();
}
getvalue(){
//当获取值时,进行依赖收集
this.dep.depend();
returnthis._value;
}
setvalue(newValue){
if(newValue!==this._value){
this._value=newValue;
//值改变时,触发更新
this.dep.notify();
}
}
}
//使用示例
letcount=newRefImpl(0);
watchEffect(()=>{
console.log(`Thecountis:${count.value}`);//订阅变化
});
count.value++;//修改值,触发通知,重新执行watchEffect中的函数
Dep 类负责管理一个依赖列表,并提供依赖收集和通知更新的功能。RefImpl 类包含一个内部值 _value 和一个 Dep 实例。当 value 被访问时,通过 get 方法进行依赖收集;当 value 被赋予新值时,通过 set 方法触发更新。
ref
和 reactive
尽管两者在内部实现上有所不同,但它们都能满足我们对于声明响应式变量的要求,但是 reactive
却存在一定的局限性。
reactive 的局限性
在 Vue3 中,reactive
API 通过 Proxy
实现了一种响应式数据的方法,尽管这种方法在性能上比 Vue2 有所提升,但 Proxy
的局限性也导致了 reactive
的局限性,这些局限性可能会影响开发者的使用体验。
仅对引用数据类型有效
reactive
主要适用于对象,包括数组和一些集合类型(如 Map
和 Set
)。对于基础数据类型(如 string
、number
和 boolean
),reactive
是无效的。这意味着如果你尝试使用 reactive
来处理这些基础数据类型,将会得到一个非响应式的对象。
import{reactive}from'vue';
conststate=reactive({count:0});
使用不当会失去响应
-
「直接赋值对象」:如果直接将一个响应式对象赋值给另一个变量,将会失去响应性。这是因为 reactive 返回的是对象本身,而不仅仅是代理。
import{reactive}from'vue';
letstate=reactive({count:0});
state={count:1};//失去响应性 -
「直接替换响应式对象」:同样,直接替换一个响应式对象也会导致失去响应性。
import{reactive}from'vue';
letstate=reactive({count:0});
state=reactive({count:1});//失去响应性 -
「直接解构对象」:在解构响应式对象时,如果直接解构对象属性,将会得到一个非响应式的变量。
conststate=reactive({count:0});
let{count}=state;
count++;//count仍然是0解决这个问题,需要使用
toRefs
函数来将响应式对象转换为ref
对象。import{toRefs}from'vue';
conststate=reactive({count:0});
let{count}=toRefs(state);
count++;//count现在是1 -
「将响应式对象的属性赋值给变量」:如果将响应式对象的属性赋值给一个变量,这个变量的值将不会是响应式的。
letstate=reactive({count:0})
letcount=state.count
count++//count仍然是0
console.log(state.count)
使用 reactive
声明响应式变量的确存在一些不便之处,尤其是对于喜欢使用解构赋值的开发者而言。这些局限性可能会导致意外的行为,因此在使用 reactive
时需要格外注意。相比之下,ref
API 提供了一种更灵活和统一的方式来处理响应式数据。
为什么推荐使用 ref ?
ref()
它为响应式编程提供了一种统一的解决方案,适用于所有类型的数据,包括基本数据类型和复杂对象。以下是推荐使用 ref 的几个关键原因:
统一性
ref
的核心优势之一是它的统一性。它提供了一种简单、一致的方式来处理所有类型的数据,无论是数字、字符串、对象还是数组。这种统一性极大地简化了开发者的代码,减少了在不同数据类型之间切换时的复杂性。
import{ref}from'vue';
letnum=ref(0);
letstr=ref('Hello');
letobj=ref({count:0});
//修改基本数据类型
num.value++;
str.value+='World';
//修改对象
obj.value.count++;
深层响应性
ref
支持深层响应性,这意味着它可以追踪和更新嵌套对象和数组中的变化。这种特性使得 ref
非常适合处理复杂的数据结构,如对象和数组。
import{ref}from'vue';
letobj=ref({
user:{
name:'xiaoming',
details:{
age:18
}
}
});
//修改嵌套对象
obj.value.user.details.age++;
当然,为了减少大型不可变数据的响应式开销,也可以通过使用shallowRef
来放弃深层响应性。
letshallowObj=shallowRef({
details:{age:18,},
});
灵活性
ref
提供了高度的灵活性,尤其在处理「普通赋值」方面。这种灵活性使得 ref 在开发中的使用更加方便,特别是在进行复杂的数据操作时。
import{ref}from'vue';
letstate=ref({
count:0,
name:'Vue'
});
//替换整个对象
state.value={
count:10,
name:'Vue4'
};
//修改对象内的属性
state.value.count=20;
state.value.name='Vue5';
//添加新的属性
state.value.newProperty='NewProperty';
//删除属性
deletestate.value.newProperty;
//使用解构更新属性(注意要保持响应性)
let{count,name}=state.value;
state.value={count:count+1,name};
//复杂操作,例如根据条件更新属性
if(someCondition){
state.value={
...state.value,
name:'UpdatedName'
};
}
console.log(state.value)
总结
ref
在 Vue3 中提供了一种更统一、灵活的响应式解决方案,还能避免了 reactive
的某些局限性。希望这篇文章对你有所帮助,有所借鉴。大家怎么认为呢,评论区我们一起讨论下!