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

Vue3源码:探索toRefs-ref

时间:2023-04-01 11:40:54 vue.js

今天项目中,在vant的弹窗组件popup中又增加了一层作为子组件,实现了弹窗的自定义。父组件Father将数据传递给Child。props:{showDialog:Boolean}props是一个代理对象。子组件可以将props.showDialog转换为计算属性。constvisible=computed({get:()=>props.showDialog,set:newVal=>{context.emit('toggleStatus',newVal)}})不过今天主要是看看ref做了什么。接下来分为两部分:setuphook中refDOM节点的ref属性1.refsetup(props,context){const{showDialog}=toRefs(props)constvisible=ref(showDialog)watch中setuphook(()=>visible.value,newVal=>{context.emit('toggleStatus',newVal)})return{visible,}}对象结构会使其无响应,toRefs包裹的对象可以避免这种情况.子组件创建一个可见变量来控制弹出组件是否显示。使用ref接收showDailog属性并返回响应式和可变的ref对象。然后通过监听visible.value值的变化,触发父组件showDialog更新值。嗯,看起来与源的反应性连接已保持!我的子组件一定会收到父亲的更新!但是这样做有一个问题。控制台很快会报出??reactivity.esm-bundler.js:336Setoperationonkey"showDialog"failed:targetisreadonly。Proxy{showDialog:true}toRefs文档原文:将响应式对象转换为普通对象,其中result对象的每个属性都是一个ref,指向原始对象对应的属性。那么,showDailog是如何变成ref对象的呢?toRefs方法遍历其参数对象,为对象的每个值添加__v_isRef=true属性:_key:K){}getvalue(){returnthis._object[this._key]}setvalue(newVal){this._object[this._key]=newVal}}__v_isRef将此对象标记为ref对象。接下来我们看一下ref方法的作用。导出函数ref(value?:unknown){returncreateRef(value)}functioncreateRef(rawValue:unknown,shallow=false){if(isRef(rawValue)){returnrawValue}returnnewRefImpl(rawValue,shallow)}refreceiveshowDailog,判断为ref对象时,返回自身。弹窗组件关闭时直接给visible赋值false,其实是在挑战props的readonly属性。控制台警告并拒绝执行您的任务。这种情况下,初始化visible为false,然后监听showDialog触发visible。constvisible=ref(false)consttoggle=(newVal:boolean)=>(visible.value=newVal)watch(()=>showDialog.value,newVal=>{toggle(newVal)})watch(()=>visible.value,newVal=>{console.log('visible=------',visible)context.emit('toggleStatus',newVal)})问题还没有结束。如果ref函数接收到的参数不是ref对象怎么办?换句话说,newRefImpl(rawValue,shallow)是做什么的?classRefImpl{private_value:Tpublicreadonly__v_isRef=trueconstructor(private_rawValue:T,publicreadonly_shallow=false){this._value=_shallow?_rawValue:convert(_rawValue)}getvalue(){track(toRaw(this),TrackOpTypes.GET,'value')返回this._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)}}}主要看方法setvalue(newVal)。ref方法不传递shallow参数,convert方法将这个值转换为响应式代理对象。constconvert=(val:T):T=>isObject(val)?reactive(val):val二、DOM节点的ref属性考虑一个问题,VUE3的ref是如何获取DOM元素的?比如一个DOM节点绑定ref='schedule'

接下来,在setup中,我们需要创建一个ref对象,别忘了返回这个ref对象。setup(){constschedule=ref(null)return{schedule}}这样我们就可以在onMounted生命周期中拿到这个DOM节点了。onMounted(()=>{console.log(schedule)})这一切不禁让看惯this.$refs.schedule的作者陷入深思。你是怎么做到的?你为什么要这样做?是不是有点反直觉?接下来,让我们走进源码康益康。第一步从项目入口main.ts开始:import{createApp}from'vue'importAppfrom'./App.vue'importrouterfrom'./router'createApp(App).use(router)。mount('#app')我们要找到createAppexportconstcreateApp=((...args)=>{constapp=ensureRenderer().createApp(...args)//..下面省略}functionensureRenderer(){returnrenderer||(renderer=createRenderer(rendererOptions))}导出函数createRenderer(options:RendererOptions){returnbaseCreateRenderer(options)}baseCreateRenderer方法暴露了createApp方法:return{render,hydrate,createApp:createAppAPI(render,hydrate)}createAppAPI暴露了app,有一个mount方法,在mount方法中执行render函数,render函数接收两个参数,如果第一个参数vnode不为null,则执行patch函数:patch(container._vnode||null,vnode,container)在runtime-core>renderer.tsL452找到patch函数的定义,这里,终于找到目的地了。n2是patch函数的第二个参数vnodeconst{type,ref,shapeFlag}=n2//...省略几千行switch//setrefif(ref!=null&&parentComponent){setRef(ref,n1&&n1.ref,parentSuspense,n2)}接下来,让我们看一下setRefif(isString(ref)){constdoSet=()=>{refs[ref]=valueif(hasOwn(setupState,ref)){setupState[ref]=value}}如果ref是字符串,判断setup暴露的setupState中是否有这个ref对象,如果有,则给ref赋值。而这个值就是vnode.el。value=vnode.el让我们再看看setupState。const{i:owner,r:ref}=rawRefconstsetupState=owner.setupStaterawRef是setRef方法的第一个参数:VNode|null)=>{//...}在vnode.tsL305中可以看到VNode的属性ref是VNodeNormalizedRef类型,如果props存在,调用normalizeRefconstnormalizeRef=({ref}:VNodeProps):VNodeNormalizedRefAtom|null=>{return(ref!=null?isString(ref)||isRef(ref)||isFunction(ref)?{i:currentRenderingInstance,r:ref}:ref:null)asany}VNodeProps是一个数组VNode节点的props对象,在编译DOM节点时创建。在compile-core>src>parse.ts文件中,找到了这个方法。exportfunctionbaseParse(content:string,options:ParserOptions={}):RootNode{constcontext=createParserContext(content,options)conststart=getCursor(context)返回createRoot(parseChildren(context,TextModes.DATA,[]),getSelection(context,start))}当dom节点绑定ref属性时,在编译时,在baseCompile方法中,会传入两个参数:template和options。如果判断模板是字符串,则对模板进行词法分析,生成AST抽象语法树。在这个过程baseParse>parseChildren>parseElement>parseTag>parseAttrs>parseAttr中,parseAttr生成一个包含name=ref的对象,parseAttrs方法将这个对象压入props数组。接下来,转换AST。然后调用transform方法,第一个参数是ast,第二个参数扩展了baseCompile的第二个参数options,包括每个节点属性类型对应的transform方法。其中有一个transformElement方法,将props转换为对象,带有type、loc、properties等参数,并为node添加codegenNode属性。node.codegenNode=createVNodeCall(context,vnodeTag,vnodeProps,vnodeChildren,vnodePatchFlag,vnodeDynamicProps,vnodeDirectives,!!shouldUseBlock,false/*disableTracking*/,node.loc)该属性对应的值将有助于在生成阶段进行优化。generate方法将从ast生成可执行的js代码。最后,总结一下。ref涉及到源码的模板编译、运行时和响应部分。模板编译将ref压入VNode的props属性数组。运行时在setupState上找到对应的ref响应对象,赋值为DOM节点。以上是关于ref的源码。新的开始,期待您的指导~