当前位置: 首页 > Web前端 > vue.js

CompositionAPI原理-Vue面试响应式封装对象_0

时间:2023-03-31 23:34:48 vue.js

本文主要分以下两部分对CompositionAPI原理进行解读:ReactAPI原理refAPI原理reactAPI原理打开源码找到入口reactive,在composition-api/src/reactivity/reactive.ts中,我们从函数入口开始分析reactive中发生了什么。通过前面的学习,我们知道reactive是用来创建一个reactive对象的,需要传递一个普通的对象作为参数。exportfunctionreactive(obj:T):UnwrapRef{if(process.env.NODE_ENV!=='production'&&!obj){warn('"reactive()"被调用时没有提供“目的”。');//@ts-忽略返回;}if(!isPlainObject(obj)||isReactive(obj)||isNonReactive(obj)||!Object.isExtensible(obj)){将obj作为任何对象返回;}//创建一个反应对象constobserved=observe(obj);//将对象标记为响应式对象def(observed,ReactiveIdentifierKey,ReactiveIdentifier);//初始化对象的访问控制,方便访问ref属性时自动解析PackagesetupAccessControl(observed);returnobservedasUnwrapRef;}首先,在开发环境中,会进行传参测试。如果没有传递相应的obj参数,开发者会在开发环境中得到警告。在这种情况下,为了不影响生产环境,在生产环境中会忽略该警告。函数入口会检查类型,首先调用isPlainObject检查是否是对象。如果不是对象,则直接返回参数,因为非对象类型是不可观察的。然后调用isReactive判断对象是否已经是响应式对象。下面是isReactive的原型:&&obj[ReactiveIdentifierKey]===ReactiveIdentifier;}通过上面的代码我们知道ReactiveIdentifierKey和ReactiveIdentifier都是一个Symbol。打开composition-api/src/symbols.ts可以看到ReactiveIdentifierKey并且ReactiveIdentifier已经定义Symbol:import{hasSymbol}from'./utils';functioncreateSymbol(name:string):string{returnhasSymbol?(Symbol.for(name)asany):name;}exportconstWatcherPreFlushQueueKey=createSymbol('vfa.key.preFlushQueue');exportconstWatcherPostFlushQueueKey=createSymbol('vfa.key.postFlushQueue');exportconstAccessControlIdentifierKey=createSymbol('vfa.key.accessControlIdentifier');exportconstReactiveIdentifierKey=createSymbol('vfa.key.accessControlIdentifier');.key.reactiveIdentifier');导出常量NonReactiveIdentifierKey=createSymbol('vfa.key.nonReactiveIdentifier');//必须是字符串,reactiveexport中忽略符号键constRefKey='vfa.key.refKey';这里我们可以大致猜想,在定义一个响应式对象时,VueCompositionAPI会在响应式对象上设置一个Symbol属性,该属性值为Symbol(vfa.key.reactiveIdentifier),所以我们可以判断对象是否有Symbol(vfa.key.reactiveIdentifier)对象上。是一个响应对象。同样,因为VueCompositionAPI内部使用的nonReactive是用来保证一个对象是无响应的,类似于isReactive,也是通过检查对象是否有对应的Symbol来实现的,即Symbol(vfa.key.非反应性标识符)。functionisNonReactive(obj:any):boolean{return(hasOwn(obj,NonReactiveIdentifierKey)&&obj[NonReactiveIdentifierKey]===NonReactiveIdentifier);}另外,由于创建响应式对象需要扩展对象属性,可以通过Object.isExtensible,当对象是不可扩展对象时,将无法创建反应对象。接下来容错判断逻辑结束后,通过observe创建响应对象。从文档和源码我们知道reactive相当于Vue2.6+中的Vue.observable,VueCompositionAPI会尽可能通过Vue.observable创建响应式对象。object,但是如果Vue版本低于2.6,会通过newVue创建一个Vue组件,并使用obj作为组件的内部状态,保证其响应性。关于如何在Vue2.x中实现响应式对象,笔者之前写过一篇文章,这里不再赘述。参考:前端vue面试题详解functionobserve(obj:T):T{constVue=getCurrentVue();让我们观察到:T;if(Vue.observable){observed=Vue.observable(obj);}else{constvm=createComponentInstance(Vue,{data:{?state:obj,},});observed=vm._data.?state;}returnobserved;}接下来,Symbol(vfa.key.reactiveIdentifier)property,def是一个效用函数,其实就是Object.defineProperty:exportfunctiondef(obj:Object,key:string,val:any,enumerable?:boolean){Object.defineProperty(obj,key,{value:val,enumerable:!!enumerable,writable:true,configurable:true,});}接下来调用setupAccessControl(observed)是reactive的核心部分。我们从上一篇文章中知道,必须使用.value,但是,如果包装对象被用作另一个响应对象的属性,当访问响应对象的属性值时,Vue会自动在内部展开包装对象。同时,在模板渲染的上下文中,也会自动展开。setupAccessControl就是帮我们做的:/***Proxingpropertyaccessoftarget.*我们可以在这里做展开和其他事情。*/functionsetupAccessControl(target:AnyObject):void{//首先要保证设置的访问控制参数的合法性//另外同样要保证响应式对象target是对象类型而不是非Reactive对象//还要保证对象不是数组(因为数组元素不能设置属性描述符)//还要保证不是ref对象(因为ref的value属性是用来保证响应性的属性),并且不能是Vue组件实例。if(!isPlainObject(target)||isNonReactive(target)||Array.isArray(target)||isRef(target)||isComponentInstance(target)){return;}//一旦初始化了这个属性的访问控制,响应对象目标上也会设置Symbol(vfa.key.accessControlIdentifier)的一个属性。//用于标记对象,初始化已经自动解包的访问控制。if(hasOwn(target,AccessControlIdentifierKey)&&target[AccessControlIdentifierKey]===AccessControlIdentifier){返回;}if(Object.isExtensible(target)){def(target,AccessControlIdentifierKey,AccessControlIdentifier);}constkeys=Object.keys(目标);//遍历对象本身的可枚举属性,这里注意:def方法定义的Symbol标记不是可枚举属性for(leti=0;i任何)|不明确的;让setter:((x:any)=>void)|不明确的;常量属性=Object.getOwnPropertyDescriptor(target,key);if(property){//保证改变目标对象property的自身属性描述符:如果对象自身的属性描述符的configurable为false,则不能为该属性设置属性描述符,getter和setter不能被设置if(property.configurable===false){return;}getter=property.get;setter=property.set;//arguments.length===2表示没有传入val参数,不是只读对象。此时属性的值:可以直接获取响应对象的属性。//传入val的情况是使用vue.set,composition也提供setapiif((!getter||setter)/*不仅有getter*/&&arguments.length===2){val=target[key];}}//在嵌套对象的情况下,实际上setupAccessControl是对setupAccessControl(val)的递归调用;Object.defineProperty(target,key,{enumerable:true,configurable:true,get:functiongetterHandler(){constvalue=getter?getter.call(target):val;//如果key等于RefKey,跳过unwraplogic//取ref对象的值时,属性名不是ref对象的Symbol标记RefKey,getterHandler返回被包装对象的值,即`value.value`if(key!==RefKey&&isRef(value)){returnvalue.value;}else{//不是ref对象,getterHandler直接返回它的值,即`value`returnvalue;}},set:functionsetterHandler(newVal){//属性没有setter,证明这个属性没有被Vue观察到,直接返回call(target):val;//如果键等于RefKey,则跳过解包逻辑//如果并且只有当“value”是ref而“newVal”不是ref时,//赋值应该被代理到“value”ref。//给ref对象赋值时,属性名不是ref对象的Symbol标志RefKey,如果newVal不是ref对象,setterHandler会委托给ref对象的value属性赋值,那就是`value.value=newVal`if(key!==RefKey&&isRef(value)&&!isRef(newVal)){value.value=newVal;}elseif(setter){//对象有setter,直接调用setter//会通知依赖该属性状态的对象更新setter.call(target,newVal);}elseif(isRef(newVal)){//在既没有getter也没有setter的情况下,普通键值,直接赋值val=newVal;}//每次重新赋值,考虑嵌套对象的情况:重新初始化newVal的访问控制setupAccessControl(newVal);},});}通过上面的代码我们可以看到,为了自动解包ref对象,defineAccessControl会为reactive对象重新设置getter和setter。考虑到嵌套对象,当初始化响应式对象并重置为响应式对象的某个属性赋值时,会递归执行setupAccessControl,以保证整个嵌套对象的所有层级的ref属性都能自动解包refAPI原理ref的入口在composition-api/src/reactivity/ref.ts,我们先看ref函数:classRefImplimplementsRef{publicvalue!:T;构造函数({get,set}:RefOption){proxy(this,'value',{get,set,});}}exportfunctioncreateRef(options:RefOption){//密封ref,这可以防止ref被观察到//密封ref是安全的,因为我们真的不应该扩展它。//相关问题:#79//密封ref以确保其安全returnObject.seal(newRefImpl(options));}exportfunctionref(raw?:any):any{//先创建一个可观察对象,这个值实际上是VueCompositionAPI内部使用的局部变量,不会暴露给开发者constvalue=reactive({[RefKey]:raw});//创建ref,其值实际委托给valuereturncreateRef({get:()=>value[RefKey]asany,set:v=>((value[RefKey]asany)=v),});}看到ref的入口首先调用reactive创建了一个observable对象,这个值其实是VueCompositionAPI内部使用的局部变量,不会暴露给开发者。它有一个属性值RefKey,其实就是一个Symbol,然后调用createRef。ref返回由createRef创建的ref对象。ref对象实际上被代理为由constvalue=reactive({[RefKey]:raw});创建的局部变量值的值。通过getter和setter,这样我们就可以拿到ref包装对象的值了。另外为了保证ref对象的安全,不被开发者不小心篡改,保证Vue不会为ref对象创建代理(因为被包裹对象的value属性不需要待观察),所以调用Object.seal密封对象。保证只能改变它的值,不能扩展它的属性。isRef很简单,通过判断传入的参数是否继承自RefImpl:result对象上的每个属性都是一个ref引用对象,指向原始对象中对应的属性,这在复合函数返回响应式状态时非常有用,保证了开发者在使用对象时不会丢失原来的响应式状态解构或扩展运算符对象的响应。实际上,它只是递归调用了createRef。导出函数toRefs(obj:T):Refs{if(!isPlainObject(obj))returnobjasany;constres:Refs={}作为任何;Object.keys(obj).forEach(key=>{letval:any=obj[key];//使用ref来代理属性if(!isRef(val)){val=createRef({get:()=>obj[key],set:v=>(obj[keyaskeyofT]=v),});}//todores[keyaskeyofT]=val;});returnres;}总结描述VueCompositionAPI的响应部分的代码。reactive和ref都是基于Vue响应式对象重新封装的。ref的内部实际上是一个响应式对象。ref的value属性将被代理到这个响应对象。这种response类型对象对开发者是不可见的,使得调用过程相对友好,并且reactive提供了自动解包ref的功能,提高开发者的开发体验。