vue3.x数据响应式的流程是怎样的
vue3.x数据响应式的流程是怎样的
本篇内容介绍了“vue3.x数据响应式的流程是怎样的”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
什么是数据响应式
从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom。
换句话说就是 Vue 自己知道哪个数据状态发生了变化及哪里有用到这个数据需要随之修改。
因此实现数据响应式有两个重点问题:
如何知道数据发生了变化?
如何知道数据变化后哪里需要修改?
对于第一个问题,如何知道数据发生了变化,Vue3 之前使用了 ES5 的一个 API Object.defineProperty Vue3 中使用了 ES6 的 Proxy,都是对需要侦测的数据进行 变化侦测 ,添加 getter 和 setter ,这样就可以知道数据何时被读取和修改。
第二个问题,如何知道数据变化后哪里需要修改,Vue 对于每个数据都收集了与之相关的 依赖 ,这里的依赖其实就是一个对象,保存有该数据的旧值及数据变化后需要执行的函数。每个响应式的数据变化时会遍历通知其对应的每个依赖,依赖收到通知后会判断一下新旧值有没有发生变化,如果变化则执行回调函数响应数据变化(比如修改 dom)。
数据响应式的大体流程
在vue3.0的响应式的部分,我们需要找的核心文件是vue3.0源码的packages里面的runtime-core下面的src里面的;我们今天研究的这条线,就是沿着render这条线走下去的;
return{render,hydrate,createApp:createAppAPI(render,hydrate)}
在该文件下找到render函数,如下所示;该函数的作用是渲染传入vnode,到指定容器中;
constrender:RootRenderFunction=(vnode,container)=>{if(vnode==null){if(container._vnode){unmount(container._vnode,null,null,true)}}else{patch(container._vnode||null,vnode,container)}flushPostFlushCbs()container._vnode=vnode}
查看patch方法,初始化的话会走else if (shapeFlag & ShapeFlags.COMPONENT)
constpatch:PatchFn=(n1,n2,container,anchor=null,parentComponent=null,parentSuspense=null,isSVG=false,optimized=false)=>{//patching¬sametype,unmountoldtreeif(n1&&!isSameVNodeType(n1,n2)){anchor=getNextHostNode(n1)unmount(n1,parentComponent,parentSuspense,true)n1=null}if(n2.patchFlag===PatchFlags.BAIL){optimized=falsen2.dynamicChildren=null}const{type,ref,shapeFlag}=n2switch(type){caseText:processText(n1,n2,container,anchor)breakcaseComment:processCommentNode(n1,n2,container,anchor)breakcaseStatic:if(n1==null){mountStaticNode(n2,container,anchor,isSVG)}elseif(__DEV__){patchStaticNode(n1,n2,container,isSVG)}breakcaseFragment:processFragment(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,optimized)breakdefault:if(shapeFlag&ShapeFlags.ELEMENT){processElement(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,optimized)}elseif(shapeFlag&ShapeFlags.COMPONENT){//初始化走这个processComponent(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,optimized)}elseif(shapeFlag&ShapeFlags.TELEPORT){;(typeastypeofTeleportImpl).process(n1asTeleportVNode,n2asTeleportVNode,container,anchor,parentComponent,parentSuspense,isSVG,optimized,internals)}elseif(__FEATURE_SUSPENSE__&&shapeFlag&ShapeFlags.SUSPENSE){;(typeastypeofSuspenseImpl).process(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,optimized,internals)}elseif(__DEV__){warn('InvalidVNodetype:',type,`(${typeoftype})`)}}//setrefif(ref!=null&&parentComponent){setRef(ref,n1&&n1.ref,parentComponent,parentSuspense,n2)}}
接下来查看processComponent方法,接下来走我们熟悉的mountComponent
constprocessComponent=(n1:VNode|null,n2:VNode,container:RendererElement,anchor:RendererNode|null,parentComponent:ComponentInternalInstance|null,parentSuspense:SuspenseBoundary|null,isSVG:boolean,optimized:boolean)=>{if(n1==null){if(n2.shapeFlag&ShapeFlags.COMPONENT_KEPT_ALIVE){;(parentComponent!.ctxasKeepAliveContext).activate(n2,container,anchor,isSVG,optimized)}else{//初始化走挂载流程mountComponent(n2,container,anchor,parentComponent,parentSuspense,isSVG,optimized)}}else{updateComponent(n1,n2,optimized)}}
进入mountComponent方法,其中比较重要的instance为创建组件实例,setupComponent为安装组件准备的;做选项处理用的;setupRenderEffec用于建立渲染函数副作用,在依赖收集的时候使用。
constmountComponent:MountComponentFn=(initialVNode,container,anchor,parentComponent,parentSuspense,isSVG,optimized)=>{//创建组件实例constinstance:ComponentInternalInstance=(initialVNode.component=createComponentInstance(initialVNode,parentComponent,parentSuspense))if(__DEV__&&instance.type.__hmrId){registerHMR(instance)}if(__DEV__){pushWarningContext(initialVNode)startMeasure(instance,`mount`)}//injectrendererinternalsforkeepAliveif(isKeepAlive(initialVNode)){;(instance.ctxasKeepAliveContext).renderer=internals}//resolvepropsandslotsforsetupcontextif(__DEV__){startMeasure(instance,`init`)}//安装组件:选项处理setupComponent(instance)if(__DEV__){endMeasure(instance,`init`)}//setup()isasync.Thiscomponentreliesonasynclogictoberesolved//beforeproceedingif(__FEATURE_SUSPENSE__&&instance.asyncDep){parentSuspense&&parentSuspense.registerDep(instance,setupRenderEffect)//Giveitaplaceholderifthisisnothydration//TODOhandleself-definedfallbackif(!initialVNode.el){constplaceholder=(instance.subTree=createVNode(Comment))processCommentNode(null,placeholder,container!,anchor)}return}//建立渲染函数副作用:依赖收集setupRenderEffect(instance,initialVNode,container,anchor,parentSuspense,isSVG,optimized)if(__DEV__){popWarningContext()endMeasure(instance,`mount`)}}
进入到setupComponent函数里面,观看setupComponent函数的内部逻辑,在这里面有属性插槽的初始化; 在这里面可以看到setupStatefulComponent方法,它就是用来处理响应式的。
exportfunctionsetupComponent(instance:ComponentInternalInstance,isSSR=false){isInSSRComponentSetup=isSSRconst{props,children,shapeFlag}=instance.vnodeconstisStateful=shapeFlag&ShapeFlags.STATEFUL_COMPONENTinitProps(instance,props,isStateful,isSSR)initSlots(instance,children)constsetupResult=isStateful?setupStatefulComponent(instance,isSSR):undefinedisInSSRComponentSetup=falsereturnsetupResult}
进入方法setupStatefulComponent,其中const Component = instance.type as ComponentOptions用于组件配置。其中instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)用于代理,data,$等都是在这里处理的。
functionsetupStatefulComponent(instance:ComponentInternalInstance,isSSR:boolean){//组件配置constComponent=instance.typeasComponentOptionsif(__DEV__){if(Component.name){validateComponentName(Component.name,instance.appContext.config)}if(Component.components){constnames=Object.keys(Component.components)for(leti=0;i<names.length;i++){validateComponentName(names[i],instance.appContext.config)}}if(Component.directives){constnames=Object.keys(Component.directives)for(leti=0;i<names.length;i++){validateDirectiveName(names[i])}}}//0.createrenderproxypropertyaccesscacheinstance.accessCache={}//1.createpublicinstance/renderproxy//alsomarkitrawsoit'sneverobservedinstance.proxy=newProxy(instance.ctx,PublicInstanceProxyHandlers)if(__DEV__){exposePropsOnRenderContext(instance)}//2.callsetup()const{setup}=Componentif(setup){constsetupContext=(instance.setupContext=setup.length>1?createSetupContext(instance):null)currentInstance=instancepauseTracking()constsetupResult=callWithErrorHandling(setup,instance,ErrorCodes.SETUP_FUNCTION,[__DEV__?shallowReadonly(instance.props):instance.props,setupContext])resetTracking()currentInstance=nullif(isPromise(setupResult)){if(isSSR){//returnthepromisesoserver-renderercanwaitonitreturnsetupResult.then((resolvedResult:unknown)=>{handleSetupResult(instance,resolvedResult,isSSR)})}elseif(__FEATURE_SUSPENSE__){//asyncsetupreturnedPromise.//bailhereandwaitforre-entry.instance.asyncDep=setupResult}elseif(__DEV__){warn(`setup()returnedaPromise,buttheversionofVueyouareusing`+`doesnotsupportityet.`)}}else{handleSetupResult(instance,setupResult,isSSR)}}else{//处理选项等事务finishComponentSetup(instance,isSSR)}}
由于咱们的案例里面没有setup,所以会执行 finishComponentSetup(instance, isSSR)来处理选项式api相关的东西。进入该函数里面查看代码逻辑,会看到如下的代码,该部分的代码用于处理选项式API相关的东西,用于支持vue2.x的版本。
//supportfor2.xoptions//支持选项APIif(__FEATURE_OPTIONS_API__){currentInstance=instanceapplyOptions(instance,Component)currentInstance=null}
进入applyOptions方法里面;往下翻,会看到这几行注释,这几行注释清晰地解释了vue2.x里面各个选项的优先级,其中包括props、inject、methods、data等。
//optionsinitializationorder(tobeconsistentwithVue2)://-props(alreadydoneoutsideofthisfunction)//-inject//-methods//-data(deferredsinceitrelieson`this`access)//-computed//-watch(deferredsinceitrelieson`this`access)
继续往下看,会看到这几行代码,我们这里面用的不是混入的形式,所以这行这一系列的代码,,其中涉及到数据相应式的代码都在resolveData方法里面。
if(!asMixin){if(deferredData.length){deferredData.forEach(dataFn=>resolveData(instance,dataFn,publicThis))}if(dataOptions){//数据响应式resolveData(instance,dataOptions,publicThis)}
进入resolveData里面,可以看到const data = dataFn.call(publicThis, publicThis),这一行代码用于获取数据对象。instance.data = reactive(data)这一行代码用于对data做响应式处理。其中核心的就是reactive,该方法用于做响应式的处理。选项式api也好,setup也罢,最终走的都是reactive方法,用该方法来做响应式处理。
functionresolveData(instance:ComponentInternalInstance,dataFn:DataFn,publicThis:ComponentPublicInstance){if(__DEV__&&!isFunction(dataFn)){warn(`Thedataoptionmustbeafunction.`+`Plainobjectusageisnolongersupported.`)}//获取数据对象constdata=dataFn.call(publicThis,publicThis)if(__DEV__&&isPromise(data)){warn(`data()returnedaPromise-notedata()cannotbeasync;Ifyou`+`intendtoperformdatafetchingbeforecomponentrenders,use`+`asyncsetup()+<Suspense>.`)}if(!isObject(data)){__DEV__&&warn(`data()shouldreturnanobject.`)}elseif(instance.data===EMPTY_OBJ){//对data做响应式处理instance.data=reactive(data)}else{//existingdata:thisisamixinorextends.extend(instance.data,data)}}
进入到reactive里面,观察其中的代码逻辑;这里面的createReactiveObject用于对数据进行处理。其中target是最终要转化的东西。
returncreateReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers)
其中mutableHandlers里面有一些get、set、deleteProperty等方法。mutableCollectionHandlers会创建依赖收集之类的操作。
vue2.x数据响应式和3.x响应式对比
到这里,我们先回顾一下vue2.x是如何处理响应式的。是用defineReactive来拦截每个key,从而可以检测数据变化,这一套处理方式是有问题的,当数据是一层嵌套一层的时候,就会进行层层递归,从而消耗大量的内存。由此来看,这一套处理方式算不上友好。Vue3里面也是用用defineReactive来拦截每个key,与此不同的是,在vue3.x里面的defineReactive里面用proxy做了一层代理,相当于加了一层关卡。Vue2.x里面需要进行递归对象所有key,速度慢。数组响应式需要额外实现。而且新增或删除属性无法监听,需要使用特殊api。而现在,直接一个new proxy直接把所有的问题都给解决了。与此同时,之前的那一套方法不知Map,Set、Class等数据结构。
大致流程图
然后我们梳理一下到响应式的过程中顺序
实现依赖收集
在实现响应式的过程中,依赖收集是和其紧密相连的东西,其中setupRenderEffect函数中使用effect函数做依赖收集。进入setupRenderEffect函数内部,在上面的代码中有这个函数,这里不一一赘述,我们继续往下看。进入到该函数内部,会看到如下代码。effect可以建立一个依赖关系:传入effect的回调函数和响应式数据之间;effect就相当于的vue2里面的dep,然后vue3里面没有watcher了。
instance.update=effect(functioncomponentEffect(){if(!instance.isMounted){letvnodeHook:VNodeHook|null|undefinedconst{el,props}=initialVNodeconst{bm,m,parent}=instance
继续往下看,会看到如下代码,subTree是当前组件vnode,其中renderComponentRoot方法用于实现渲染组件的根。
constsubTree=(instance.subTree=renderComponentRoot(instance))
“vue3.x数据响应式的流程是怎样的”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注恰卡编程网网站,小编将为大家输出更多高质量的实用文章!
推荐阅读
-
vue表格组件教程学习(vue proxytable只能在开发环境跨域吗)
vueproxytable只能在开发环境跨域吗?跨域问题来源于JavaScript的同源策略,即只有协议主机名端口号(如...
-
Vue组件的自定义事件和全局事件总线怎么使用
-
vue中消息订阅与发布如何使用
vue中消息订阅与发布如何使用这篇文章主要介绍“vue中消息订阅与...
-
Vue显示图片的方式有哪些
-
vue引入静态jquery报错如何解决
vue引入静态jquery报错如何解决这篇文章主要介绍“vue引入...
-
vue-cropper怎么实现裁剪图片
-
怎么用Vue+NodeJS实现大文件上传
-
Vue如何实现简易跑马灯效果
-
Vue怎么指定不编译的文件夹和favicon.ico
Vue怎么指定不编译的文件夹和favicon.ico这篇文章主要介...
-
Vue中的插槽怎么使用