当前位置: 首页 > 科技观察

Vue3.0进阶深度学习响应式RefsAPI

时间:2023-03-12 04:21:18 科技观察

本文转载自微信公众号《修仙之路》阿宝哥。转载本文请联系全栈修真之路公众号。本文是Vue3.0进阶系列的第九篇。RefsAPI经常出现在组合API设置配置项中,例如ref、toRef或toRefs。那么这些常用的API有哪些功能和使用场景呢?它们背后的实现原理是什么?接下来阿宝哥就带着这些问题,和大家一起深入研究响应式RefsAPI。1.ref这个函数接受一个内部值,返回一个响应式的、可变的ref对象,里面包含一个value属性。1.1例子const{ref}=Vueconstskill=ref("Vue3")console.log(skill.value)//Vue3skill.value="Vite2"console.log(skill.value)//Vite21.2函数实现//包/reactivity/src/ref.tsexportfunctionref(value:T):ToRefexportfunctionref(value:T):Ref>exportfunctionref():Refexportfunctionref(value?:unknown){returncreateRef(value)}从上面的代码我们可以看出ref函数的value参数不仅支持基本数据类型,还支持非基本数据类型的参数。在ref函数内部,调用createRef工厂函数来创建ref对象。createRef函数的具体实现如下://packages/reactivity/src/ref.tsfunctioncreateRef(rawValue:unknown,shallow=false){if(isRef(rawValue)){returnrawValue}returnnewRefImpl(rawValue,shallow)}时创建一个ref对象,如果发现rawValue参数本身就是一个ref对象,则直接返回该对象。否则,将调用RefImpl构造函数来创建一个ref对象。以使用ref为例,ref("Vue3")创建的ref对象的内部结构如下图所示:从上图中我们可以清楚的看到ref对象包含__v_isRef,_rawValue和_value等属性。那么这些属性有什么用呢?这里先介绍一下__v_isRef属性的作用。2.isRef该函数用于检查指定值是否为ref对象。2.1使用例子const{ref,isRef}=Vueconstname=ref("Abaoge")console.log(isRef(name))//true2.2函数实现//packages/reactivity/src/ref.tsexportfunctionisRef(r:Ref|unknown):risRefexportfunctionisRef(r:any):risRef{returnBoolean(r&&r.__v_isRef===true)}从上面的代码我们可以看出isRef函数内部是通过r&&r。__v_isRef===true表达式判断参数r是否为ref对象。我们之前分析过ref函数,已经知道ref对象的本质是RefImpl类的一个实例。那么这个实例的__v_isRef成员属性是什么时候设置的呢?我们从源码中寻找这个问题的答案://packages/reactivity/src/ref.tsclassRefImpl{private_value:Tpublicreadonly__v_isRef=true//省略了大部分代码}观察上面的代码,我们可以看出__v_isRef属性是公共只读属性,其值始终为真。好了,现在我们知道了如何创建ref对象,以及如何检查指定值是否为ref对象,下面我们来介绍下一个函数——unref。3.unref这个函数接受一个参数。如果参数是ref对象,则返回对象的内部值,否则返回参数本身。它是val=isRef(val)的语法糖函数吗?值:值。3.1使用示例const{ref,unref}=Vueconstname=ref("阿宝哥")console.log(unref(name))//"阿宝哥"3.2函数实现//packages/reactivity/src/ref.tsexportfunctionunref(ref:T):TextendsRef?V:T{returnisRef(ref)?(ref.valueasany):ref}在unref函数内部,会使用isRef函数判断ref参数是否为ref对象,如果是,则返回ref.value的值,否则返回参数本身。4.toRef这个函数可以用来为源响应对象的属性创建一个新的ref。之后,ref可以传递,它将保持与其source属性的反应连接。4.1使用示例const{reactive,toRef}=Vueconstman=reactive({name:"Abaoge",skill:"Vue3"})constskillRef=toRef(man,'skill');console.log(`skillRef.value:${skillRef.value}`);//skillRef.value:Vue3skillRef.value="Vite2";console.log(`man.skill:${man.skill}`);//man.skill:Vite24.2函数实现//packages/reactivity/src/ref.tsexportfunctiontoRef(object:T,key:K):ToRef{returnisRef(object[key])?object[key]:(newObjectRefImpl(object,key)asany)}toRef函数接受object和key作为两个参数,以及对象参数是非原始类型。函数中会判断object[key]对象是否为ref对象,如果是则直接返回object[key]的值,否则调用ObjectRefImpl类的构造函数并返回该类的实例://packages/reactivity/src/ref.tsclassObjectRefImpl{publicreadonly__v_isRef=true//用于标识ref对象构造函数(privatereadonly_object:T,privatereadonly_key:K){}getvalue(){returnthis._object[this._key]}setvalue(newVal){this._object[this._key]=newVal}}ObjectRefImpl类的定义非常简单。该类包含一个公共的只读属性__v_isRef,用于标识该类的实例是否为ref对象。此外,它还包含用于操作value值的setter和getter方法。5.toRefs该函数用于将响应式对象转换为普通对象,其中结果对象的每个属性都是一个ref,指向原始对象的相应属性。5.1使用示例const{reactive,toRefs}=Vueconstman=reactive({name:"Abaoge",skill:"Vue3"})console.log(toRefs(man));5.2函数实现//packages/reactivity/src/ref.tsexportfunctiontoRefs(object:T):ToRefs{if(__DEV__&&!isProxy(object)){console.warn(`toRefs()expectsareactiveobjectbutreceivedaplainone.`)}constret:any=isArray(object)?newArray(object.length):{}for(constkeyinobject){ret[key]=toRef(object,key)//调用toRef函数为对象的属性创建一个新的ref}returnret}从上面的代码中,toRefs主要用于将Reactive对象转换为普通对象,转换过程中通过调用toRef函数处理对象的各个属性。需要注意的是,toRefs函数为源对象中包含的属性生成refs。如果要为特定属性创建ref,则应使用toRef函数。当toRefs例子的代码执行成功后,控制台会输出如下结果:那么toRefs函数在实际项目中有什么用呢?toRefs在setup函数返回一个reactive对象时非常有用,可以让组件在不失响应性的情况下,解构返回的对象:

在上面的例子中,我们在useLoginInfo函数里面使用toRefs函数来转换响应式man对象。如果直接返回响应式man对象,解构时name和skill的值如下图:通过toRefs函数转换响应式对象man后,解构时name和skill的值如下如下:对比发现使用toRefs函数后,可以在不损失响应性的情况下进行解构操作。6.shallowRef这个函数用来创建一个ref,它跟踪自身.value的变化,但不让它的值响应。6.1使用示例const{shallowRef,isReactive}=VueconstshallowMan=shallowRef({name:"Abaoge",skill:"Vue3"})console.log(isReactive(shallowMan.value))//false6.2函数实现//packages/反应性/src/ref.tsexportfunctionshallowRef(value:T):TextendsRef?T:RefexportfunctionshallowRef(value:T):RefexportfunctionshallowRef():RefexportfunctionshallowRef(value?:unknown){returncreateRef(value,true)}ref对象也是通过调用shallowRef函数内部的createRef工厂函数创建的。与上面介绍的ref函数不同的是,createRef函数的第二个参数没有在ref函数内部设置,该参数默认值为false,如下:functioncreateRef(rawValue:unknown,shallow=false){if(isRef(rawValue)){returnrawValue}returnnewRefImpl(rawValue,shallow)}从shallowRef示例的输出来看,如果shallow参数的值为true,则shallowMan.value对象不是响应对象。如果我们想进一步了解shallowRef函数和ref函数的区别,我们需要分析RefImpl类的具体实现://packages/reactivity/src/ref.tsclassRefImpl{private_value:Tpublicreadonly__v_isRef=true//用于判断是否为ref对象constructor(private_rawValue:T,publicreadonly_shallow=false){this._value=_shallow?_rawValue:convert(_rawValue)//是否将_rawValue对象转换为响应式对象}getvalue(){track(toRaw(this),TrackOpTypes.GET,'value')//跟踪get操作returnthis._value}setvalue(newVal){if(hasChanged(toRaw(newVal),this._rawValue)){//判断是否取值已更改this._rawValue=newValthis。_value=this._shallow?newVal:convert(newVal)trigger(toRaw(this),TriggerOpTypes.SET,'value',newVal)//触发集合操作}}}在RefImpl构造函数中,如果_shallow的值为true,它会直接将_rawValue的值赋值给this._value属性,否则会先调用convert函数对_rawValue的值进行转换,再进行赋值操作。转换函数定义在reactivity/src/ref.ts文件中://packages/reactivity/src/ref.tsconstconvert=(val:T):T=>isObject(val)?reactive(val):val如果要转换的值是对象类型,则使用reactive函数将val对象转换为reactive对象,否则不做任何处理直接返回val对象。了解了shallowRef函数后,你可能想知道在实际项目中使用shallowRef函数会发生什么?下面是一个具体的例子:const{shallowRef,watchEffect}=VueconstshallowMan=shallowRef({name:"阿宝哥",skill:"Vue3"})watchEffect(()=>{console.log(shallowMan.value.skill)})shallowMan.value.skill="Vite2"上面代码执行后,控制台只会输出一次"Vue3",但是此时shallowMan.value的值已经更新了:shallowMan.value{name:"Abaoge",skill:"Vite2"}那么如何及时输出最新的值呢?对于这个问题,可以使用triggerRef函数。七、triggerRef该函数用于手动执行与shallowRef相关的任何副作用。7.1使用示例const{shallowRef,watchEffect,triggerRef}=VueconstshallowMan=shallowRef({name:"Abaoge",skill:"Vue3"})watchEffect(()=>{console.log(shallowMan.value.skill)})shallowMan.value.skill="Vite2"triggerRef(shallowMan)//ManuallyperformanysideeffectsassociatedwithshallowMan添加triggerRef(shallowMan)行后,以上代码运行成功后,控制台会输出如下:Vue3Vite27.2函数在triggerRef函数内部实现了exportfunctiontriggerRef(ref:Ref){trigger(toRaw(ref),TriggerOpTypes.SET,'value',__DEV__?ref.value:void0)},会调用effect.ts文件中的export触发器触发TriggerOpTypes.SET操作的函数。其中,TriggerOpTypes是一个枚举对象,用于定义触发操作的类型。除了SET,还有ADD、DELETE、CLEAR等操作://packages/reactivity/src/operations.tsexportconstenumTriggerOpTypes{SET='set',ADD='add',DELETE='delete',CLEAR='clear'}好了,这里先介绍一下triggerRef函数。至于触发函数内部是如何工作的,阿宝哥会在后续的文章中揭秘。最后,我们来到customRef函数。8.customRef该函数用于创建自定义ref并显式控制其依赖跟踪和更新触发。它需要一个工厂函数,该函数将跟踪和触发函数作为参数并返回一个带有get和set的对象。8.1使用示例
在上面的例子中,我们使用了customRef函数创建一个提供去抖动功能的自定义引用。8.2函数实现//packages/reactivity/src/ref.tsexportfunctioncustomRef(factory:CustomRefFactory):Ref{returnnewCustomRefImpl(factory)asany}customRef函数接受一个类型为CustomRefFactory的工厂参数://packages/reactivity/src/ref.tsexporttypeCustomRefFactory=(track:()=>void,trigger:()=>void)=>{get:()=>Tset:(value:T)=>void}根据上面的类型定义,自定义ref工厂函数接收track和trigger函数作为参数,通过get和set返回一个对象。在customRef函数内部,将使用传递的工厂函数作为参数调用CustomRefImpl类的构造函数,并返回新创建的实例。在reactivity/src/ref.ts文件中定义了CustomRefImpl类,具体实现如下://packages/reactivity/src/ref.tsclassCustomRefImpl{privatereadonly_get:ReturnType>['get']privatereadonly_set:ReturnType>['set']publicreadonly__v_isRef=true//用于标识ref对象构造函数(factory:CustomRefFactory){const{get,set}=factory(()=>track(this,TrackOpTypes.GET,'value'),//跟踪获取操作()=>trigger(this,TriggerOpTypes.SET,'value')//触发设置操作)this._get=getthis._set=set}getvalue(){returnthis._get()}setvalue(newVal){this._set(newVal)}}在CustomRefImpl类的构造函数中,会先调用传入的工厂函数,再调用工厂返回的对象功能将被解构。以前面customRef的例子为例,get和set对应的函数如下:customRef((track,trigger)=>{return{get(){track()//()=>track(this,TrackOpTypes.GET,'value')returnvalue},set(newValue){clearTimeout(timeout)timeout=setTimeout(()=>{value=newValuetrigger()//()=>trigger(this,TriggerOpTypes.SET,'value')},delay)}}})至此,RefsAPI已经介绍完毕,细心的朋友应该会发现,effect.ts文件中定义的track和trigger函数是在多个RefAPI中使用的,出于篇幅的考虑阿宝哥这两个功能就不继续介绍了。在Vue3响应式原理的后续专题中,阿宝哥会详细分析这两个功能。感兴趣的朋友记得关注阿宝哥的Vue3进阶专栏。9.参考资源Vue3官网-Refs