这篇文章主要介绍“vue3中如何使用ref和reactive”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“vue3中如何使用ref和reactive”文章能帮助大家解决问题。
1.前言
vue3新增了ref,reactive两个api用于响应式数据,Ref 系列毫无疑问是使用频率最高的 api 之一,响应式意味着数据变动,页面局部自动更新。数据类型有基本数据类型(string,number,boolean,undfined,null,symbol),引用数据类型(object,array,set,map等)。如何精准检测跟踪js中所有的数据类型变动,并且能够达到vnode的对比后真实dom的渲染?vue中是如何做到的呢?简单实例如下:
import{reactive,ref}from\"vue\";importtype{Ref}from\"vue\";//定义响应式数据constcount:Ref=ref(0);functioncountClick(){count.value++;//更新数据}
//定义引用类型数据标注interfaceTypeForm{name:string;num:number;list?:Array;}constformInline:TypeForm=reactive({name:\"\",num:0,});formInline.name=\'KinHKin\'formInline.num=100formInline.list=[1,2,3,4]
效果图:
2.比较
活动:慈云数据爆款香港服务器,CTG+CN2高速带宽、快速稳定、平均延迟10+ms 速度快,免备案,每月仅需19元!! 点击查看先做个ref和reactive的比较
不推荐使用
reactive()
的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。
ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:
constobj={foo:ref(1),bar:ref(2)}//该函数接收一个ref//需要通过.value取值//但它会保持响应性callSomeFunction(obj.foo)//仍然是响应式的const{foo,bar}=obj
简而言之,ref() 让我们能创造一种对任意值的 “引用”,并能够在不丢失响应性的前提下传递这些引用。这个功能很重要,因为它经常用于将逻辑提取到 组合函数 中。
当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value。下面是之前的计数器例子,用 ref() 代替:
{{count}}
请注意,仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包”。
3.ref源码解析
对于vue3.2.2x版本的源码位于node_moudles/@vue/reactivity/dist/reactivity.cjs.js文件中
执行顺序是ref ->createRef ->new RefImpl 生成实例对象,提供get,set方法
源码中我们可以看到:入口有两个函数默认深层次响应ref,浅层次使用shallowRef,参数一个false,一个是true。
functionref(value){returncreateRef(value,false);}functionshallowRef(value){returncreateRef(value,true);}
接下来就是走createRef这个方法:
functioncreateRef(rawValue,shallow){if(isRef(rawValue)){returnrawValue;}returnnewRefImpl(rawValue,shallow);}
这个createRef方法接受两个参数,一个是传入的基本类型的默认数值,一个是否是深层次响应的boolean值。
functionisRef(r){return!!(r&&r.__v_isRef===true);}
如果rawValue本就是ref类型的会立即返回rawValue,否则返回一个RefImpl实例。
RefImpl类:
classRefImpl{constructor(value,__v_isShallow){this.__v_isShallow=__v_isShallow;this.dep=undefined;this.__v_isRef=true;this._rawValue=__v_isShallow?value:toRaw(value);this._value=__v_isShallow?value:toReactive(value);}getvalue(){trackRefValue(this);returnthis._value;}setvalue(newVal){constuseDirectValue=this.__v_isShallow||isShallow(newVal)||isReadonly(newVal);newVal=useDirectValue?newVal:toRaw(newVal);if(shared.hasChanged(newVal,this._rawValue)){this._rawValue=newVal;this._value=useDirectValue?newVal:toReactive(newVal);triggerRefValue(this,newVal);}}}
RefImpl类在构造函数中,__v_isShallow表示是否是浅层次响应的属性, 私有的 _rawValue 变量,存放 ref 的旧值,_value是ref接受的最新的值。公共的只读变量 __v_isRef 是用来标识该对象是一个 ref 响应式对象的标记与在讲述 reactive api 时的 ReactiveFlag 相同。
在const toReactive = (value) => shared.isObject(value) ? reactive(value) : value;这个函数的内部判断是否传入的是一个对象,如果是一个对象就返回reactive返回代理对象,否则直接返回原参数。
当我们通过 ref.value 的形式读取该 ref 的值时,就会触发 value 的 getter 方法,在 getter 中会先通过 trackRefValue 收集该 ref 对象的 value 的依赖,收集完毕后返回该 ref 的值。
functiontrackRefValue(ref){if(shouldTrack&&activeEffect){ref=toRaw(ref);{trackEffects(ref.dep||(ref.dep=createDep()),{target:ref,type:\"get\"/*TrackOpTypes.GET*/,key:\'value\'});}}}
当我们对 ref.value 进行修改时,又会触发 value 的 setter 方法,会将新旧 value 进行比较,如果值不同需要更新,则先更新新旧 value,之后通过 triggerRefValue 派发该 ref 对象的 value 属性的更新,让依赖该 ref 的副作用函数执行更新。
functiontriggerRefValue(ref,newVal){ref=toRaw(ref);if(ref.dep){{triggerEffects(ref.dep,{target:ref,type:\"set\"/*TriggerOpTypes.SET*/,key:\'value\',newValue:newVal});}}}
4.reactive源码解析
对于vue3.2.2x版本的源码位于node_moudles/@vue/reactivity/dist/reactivity.cjs.js文件中
整体描述vue3的更新机制:
在 Vue3 中,通过 track 的处理器函数来收集依赖,通过 trigger 的处理器函数来派发更新,每个依赖的使用都会被包裹到一个副作用(effect)函数中,而派发更新后就会执行副作用函数,这样依赖处的值就被更新了。
Proxy 对象能够利用 handler 陷阱在 get、set 时捕获到任何变动,也能监听对数组索引的改动以及 数组 length 的改动。
执行顺序是:reactive -> createReactiveObject ->
functionreactive(target){//iftryingtoobserveareadonlyproxy,returnthereadonlyversion.if(isReadonly(target)){returntarget;}returncreateReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers,reactiveMap);}
第三行 isReadonly 函数 确定对象是否为只读对象,IS_READONLY key 确定对象是否为只读对象。ReactiveFlags 枚举会在源码中不断的与我们见面,所以有必要提前介绍一下 ReactiveFlags:
functionisReadonly(value){return!!(value&&value[\"__v_isReadonly\"/*ReactiveFlags.IS_READONLY*/]);}
exportconstenumReactiveFlags{SKIP=\'__v_skip\',//是否跳过响应式返回原始对象IS_REACTIVE=\'__v_isReactive\',//标记一个响应式对象IS_READONLY=\'__v_isReadonly\',//标记一个只读对象RAW=\'__v_raw\'//标记获取原始值IS_SHALLOW=\'__v_isShallow\'//是否浅层次拷贝}
在 ReactiveFlags 枚举中有 5 个枚举值,这五个枚举值的含义都在注释里。对于 ReactiveFlags 的使用是代理对象对 handler 中的 trap 陷阱非常好的应用,对象中并不存在这些 key,而通过 get 访问这些 key 时,返回值都是通过 get 陷阱的函数内处理的。介绍完 ReactiveFlags 后我们继续往下看。
createReactiveObject
入参部分:
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {}
先看 createReactiveObject 函数的签名,该函数接受 5 个参数:
1、target:目标对象,想要生成响应式的原始对象。
2、isReadonly:生成的代理对象是否只读。
3、baseHandlers:生成代理对象的 handler 参数。当 target 类型是 Array 或 Object 时使用该 handler。
4、collectionHandlers:当 target 类型是 Map、Set、WeakMap、WeakSet 时使用该 handler。
5、proxyMap:存储生成代理对象后的 Map 对象。
这里需要注意的是 baseHandlers 和 collectionHandlers 的区别,这两个参数会根据 target 的类型进行判断,最终选择将哪个参数传入 Proxy 的构造函数,当做 handler 参数使用。
逻辑部分:
functioncreateReactiveObject(target,isReadonly,baseHandlers,collectionHandlers,proxyMap){//如何不是对象曝出警告返回其原始值if(!shared.isObject(target)){{console.warn(`valuecannotbemadereactive:${String(target)}`);}returntarget;}//targetisalreadyaProxy,returnit.//exception:callingreadonly()onareactiveobject//如果目标已经是一个代理,直接返回KinHKin译//除非对一个响应式对象执行readonlyif(target[\"__v_raw\"/*ReactiveFlags.RAW*/]&&!(isReadonly&&target[\"__v_isReactive\"/*ReactiveFlags.IS_REACTIVE*/])){returntarget;}//targetalreadyhascorrespondingProxy//目标已经存在对应的代理对象KinHKin译constexistingProxy=proxyMap.get(target);if(existingProxy){returnexistingProxy;}//onlyspecificvaluetypescanbeobserved.//只有白名单里的类型才能被创建响应式对象KinHKin译consttargetType=getTargetType(target);if(targetType===0/*TargetType.INVALID*/){returntarget;}constproxy=newProxy(target,targetType===2/*TargetType.COLLECTION*/?collectionHandlers:baseHandlers);proxyMap.set(target,proxy);returnproxy;}
在该函数的逻辑部分,可以看到基础数据类型并不会被转换成代理对象,而是直接返回原始值。
并且会将已经生成的代理对象缓存进传入的 proxyMap,当这个代理对象已存在时不会重复生成,会直接返回已有对象。
也会通过 TargetType 来判断 target 目标对象的类型,Vue3 仅会对 Array、Object、Map、Set、WeakMap、WeakSet 生成代理,其他对象会被标记为 INVALID,并返回原始值。
当目标对象通过类型校验后,会通过 new Proxy() 生成一个代理对象 proxy,handler 参数的传入也是与 targetType 相关,并最终返回已生成的 proxy 对象。
所以回顾 reactive api,我们可能会得到一个代理对象,也可能只是获得传入的 target 目标对象的原始值。
handles的组成
在 @vue/reactive 库中有 baseHandlers 和 collectionHandlers 两个模块,分别生成 Proxy 代理的 handlers 中的 trap 陷阱。
例如在上面生成 reactive 的 api 中 baseHandlers 的参数传入了一个 mutableHandlers 对象,这个对象是这样的:
constmutableHandlers={get,set,deleteProperty,has,ownKeys};
通过变量名我们能知道 mutableHandlers 中存在 5 个 trap 陷阱。而在 baseHandlers 中,get 和 set 都是通过工厂函数生成的,以便于适配除 reactive 外的其他 api,例如 readonly、shallowReactive、shallowReadonly 等。
baseHandlers 是处理 Array、Object 的数据类型的,这也是我们绝大部分时间使用 Vue3 时使用的类型,所以笔者接下来着重的讲一下baseHandlers 中的 get 和 set 陷阱。
get陷阱
上一段提到 get 是由一个工厂函数生成的,先来看一下 get 陷阱的种类。
constget=/*#__PURE__*/createGetter();constshallowGet=/*#__PURE__*/createGetter(false,true);constreadonlyGet=/*#__PURE__*/createGetter(true);constshallowReadonlyGet=/*#__PURE__*/createGetter(true,true);
函数内部返回一个get函数,使用了闭包的方式,将get函数中的参数传到handlers中。
createGetter 的逻辑:
functioncreateGetter(isReadonly=false,shallow=false){returnfunctionget(target,key,receiver){//如果key是响应式的对象就返回不是只读*KinHKin注释*if(key===\"__v_isReactive\"/*ReactiveFlags.IS_REACTIVE*/){return!isReadonly;}//如果key是只读对象就返回只读是true*KinHKin注释*elseif(key===\"__v_isReadonly\"/*ReactiveFlags.IS_READONLY*/){returnisReadonly;}//如果key是浅层次响应对象就返回浅层次是true*KinHKin注释*elseif(key===\"__v_isShallow\"/*ReactiveFlags.IS_SHALLOW*/){returnshallow;}//如果key是原始值对象并且改变的值和原始标记一致就返回原始值*KinHKin注释*elseif(key===\"__v_raw\"/*ReactiveFlags.RAW*/&&receiver===(isReadonly?shallow?shallowReadonlyMap:readonlyMap:shallow?shallowReactiveMap:reactiveMap).get(target)){returntarget;}//判断传入的值是不是数组consttargetIsArray=shared.isArray(target);//如果不是只读并且是数组//arrayInstrumentations是一个对象,对象内保存了若干个被特殊处理的数组方法,并以键值对的形式存储。*KinHKin注释*if(!isReadonly&&targetIsArray&&shared.hasOwn(arrayInstrumentations,key)){//特殊处理数组返回结果returnReflect.get(arrayInstrumentations,key,receiver);}//获取Reflect执行的get默认结果constres=Reflect.get(target,key,receiver);//如果是key是Symbol,并且key是Symbol对象中的Symbol类型的key//或者key是不需要追踪的key:__proto__,__v_isRef,__isVue//直接返回get结果*KinHKin注释*if(shared.isSymbol(key)?builtInSymbols.has(key):isNonTrackableKeys(key)){returnres;}//不是只读对象执行track收集依赖*KinHKin注释*if(!isReadonly){track(target,\"get\"/*TrackOpTypes.GET*/,key);}//是浅层次响应直接返回get结果*KinHKin注释*if(shallow){returnres;}if(isRef(res)){//refunwrapping-skipunwrapforArray+integerkey.//如果是ref,则返回解包后的值-当target是数组,key是int类型时,不需要解包*KinHKin注释*returntargetIsArray&&shared.isIntegerKey(key)?res:res.value;}if(shared.isObject(res)){//Convertreturnedvalueintoaproxyaswell.wedotheisObjectcheck//heretoavoidinvalidvaluewarning.Alsoneedtolazyaccessreadonly//andreactiveheretoavoidcirculardependency.//将返回的值也转换成代理,我们在这里做isObject的检查以避免无效值警告。//也需要在这里惰性访问只读和星影视对象,以避免循环依赖。*KinHKin注释*returnisReadonly?readonly(res):reactive(res);}//不是object类型则直接返回get结果*KinHKin注释*returnres;};}
从这段 createGetter 逻辑中,之前专门介绍过的 ReactiveFlags 枚举在这就取得了妙用。其实目标对象中并没有这些 key,但是在 get 中Vue3 就对这些 key 做了特殊处理,当我们在对象上访问这几个特殊的枚举值时,就会返回特定意义的结果。而可以关注一下 ReactiveFlags.IS_REACTIVE 这个 key 的判断方式,为什么是只读标识的取反呢?因为当一个对象的访问能触发这个 get 陷阱时,说明这个对象必然已经是一个 Proxy 对象了,所以只要不是只读的,那么就可以认为是响应式对象了。
get 的后续逻辑:
继续判断 target 是否是一个数组,如果代理对象不是只读的,并且 target 是一个数组,并且访问的 key 在数组需要特殊处理的方法里,就会直接调用特殊处理的数组函数执行结果,并返回。
arrayInstrumentations 是一个对象,对象内保存了若干个被特殊处理的数组方法,并以键值对的形式存储。
我们之前说过 Vue2 以原型链的方式劫持了数组,而在这里也有类似地作用,下面是需要特殊处理的数组。
-
对索引敏感的数组方法
-
includes、indexOf、lastIndexOf
-
会改变自身长度的数组方法,需要避免 length 被依赖收集,因为这样可能会造成循环引用
-
push、pop、shift、unshift、splice
下面的几个key是不需要被依赖收集或者是返回响应式结果的:
__proto__
_v_isRef
__isVue
在处理完数组后,我们对 target 执行 Reflect.get 方法,获得默认行为的 get 返回值。
之后判断 当前 key 是否是 Symbol,或者是否是不需要追踪的 key,如果是的话直接返回 get 的结果 res。
接着判断当前代理对象是否是只读对象,如果不是只读的话,则运行笔者上文提及的 tarck 处理器函数收集依赖。
如果是 shallow 的浅层响应式,则不需要将内部的属性转换成代理,直接返回 res。
如果 res 是一个 Ref 类型的对象,就会自动解包返回,这里就能解释官方文档中提及的 ref 在 reactive 中会自动解包的特性了。而需要注意的是,当 target 是一个数组类型,并且 key 是 int 类型时,即使用索引访问数组元素时,不会被自动解包。
如果 res 是一个对象,就会将该对象转成响应式的 Proxy 代理对象返回,再结合我们之前分析的缓存已生成的 proxy 对象,可以知道这里的逻辑并不会重复生成相同的 res,也可以理解文档中提及的当我们访问 reactive 对象中的 key 是一个对象时,它也会自动的转换成响应式对象,而且由于在此处生成 reactive 或者 readonly 对象是一个延迟行为,不需要在第一时间就遍历 reactive 传入的对象中的所有 key,也对性能的提升是一个帮助。
当 res 都不满足上述条件时,直接返回 res 结果。例如基础数据类型就会直接返回结果,而不做特殊处理。最后,get 陷阱的逻辑全部结束了。
set陷阱
set 也有一个 createSetter 的工厂函数,也是通过柯里化的方式返回一个 set 函数。
set 的函数比较简短,所以这次一次性把写好注释的代码放上来,先看代码再讲逻辑。
//纯函数默认深层次响应函数不入参*KinHKin*constset=/*#__PURE__*/createSetter();//纯函数浅层次响应函数入参是true*KinHKin*constshallowSet=/*#__PURE__*/createSetter(true);functioncreateSetter(shallow=false){returnfunctionset(target,key,value,receiver){letoldValue=target[key];//如果原始值是只读and是ref类型and新的value属性不是ref类型直接返回if(isReadonly(oldValue)&&isRef(oldValue)&&!isRef(value)){returnfalse;}if(!shallow){//如果新的值不是浅层次响应对象,也不是只读更新旧值新值为普通对象*KinHKin*if(!isShallow(value)&&!isReadonly(value)){oldValue=toRaw(oldValue);value=toRaw(value);}//当不是只读模式时,判断旧值是否是Ref,如果是则直接更新旧值的value//因为ref有自己的setter*KinHKin*if(!shared.isArray(target)&&isRef(oldValue)&&!isRef(value)){oldValue.value=value;returntrue;}}//判断target中是否存在key*KinHKin*consthadKey=shared.isArray(target)&&shared.isIntegerKey(key)?Number(key)
还没有评论,来说两句吧...