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

Vue3源码分析图的组件渲染,VNode如何转化为真正的DOM

时间:2023-03-15 16:28:04 科技观察

1上面写了。在VUE中,组件是一个非常重要的概念。整个应用页面都是通过component来渲染的,但是我们写的是Components,它们内部是怎么工作的呢?从我们开始编写组件到最终将它们变成真正的DOM的转换过程是怎样的?那么我们首先应该了解组件在vue3中是如何渲染的?2Components组件是一个抽象的概念,它是对DOM树的抽象,在页面上写一个组件节点:,它不会在页面上渲染这个被调用的标签。当我们写一个组件的时候,它的内部应该是这样的:需要以下几个步骤:CreateVNodeRenderVNodeGeneraterealDOM这里所说的VNode其实是一个Javascript对象,可以描述组件信息。3应用程序初始化一个组件可以通过“模板+对象描述”来创建,创建后如何调用和初始化?因为整个组件树都是从根组件开始渲染的,所以要找到根组件的渲染入口,需要从应用的初始化过程开始分析。下面看看vue2和vue3初始化应用代码的区别,其实区别不大。//vue2importVuefrom"vue";importAppfrom"./App";constapp=newVue({render:h=>h(App);})app.$mount("#app");//vue3import{createApp}from"vue";importAppfrom"./app";constapp=createApp(App);app.mount("#app");接下来我们看一下createApp的内部实现:}const{mount}=app//重写挂载方法app.mount=(containerOrSelector:Element|string):any=>{constcontainer=normalizeContainer(containerOrSelector)if(!container)returnconstcomponent=app._componentif(!isFunction(component)&&!component.render&&!component.template){component.template=container.innerHTML}//clearcontentbeforemountingcontainer.innerHTML=''constproxy=mount(container)container.removeAttribute('v-cloak')returnproxy}returnapp})asCreateAppFunction我们看到constapp=ensureRenderer().createApp(...args)是用来创建app对象的,那么它内部是如何实现的://一些渲染相关的配置,比如:更新属性的方法,操作DOM的方法constrendererOptions={patchProp,//处理props属性...nodeOps//处理DOM节点操作}//lazycreatetherenderer-thismakescorerendererlogictree-shakable//incasetheuseronlyimportsreactivityutilitiesfromVue.letrenderer:Renderer|HydrationRendererletenabledHydration=false//我们看到中文翻译是:createarendererwithadelay。当用户只依赖响应包时,不会立即创建渲染器。//核心可以通过tree-shakable去掉渲染逻辑相关代码functionensureRenderer(){returnrenderer||(renderer=createRenderer(rendererOptions))}渲染器,为跨平台渲染做准备,简单理解就是:JS对象包含平台渲染逻辑我们看到createrendering设备是通过调用createRenderer实现的,它通过调用baseCreateRenderer函数返回,里面包含了我们要找的createApp:createAppAPI(render,hydrate)。exportfunctioncreateRenderer(options:RendererOptions){returnbaseCreateRenderer(options)}//functionbaseCreateRenderer(options:RendererOptions,createHydrationFnsy:typeofFunccreate{:hostInsert,remove:hostRemove,复制代码patchProp:hostPatchProp,createElement:hostCreateElement,createText:hostCreateText,createComment:hostCreateComment,setText:hostSetText,setElementText:hostSetElementText,parentNode:hostParentNode,nextSibling:hostNextSibling,setScopeId:hostSetScopeId=NOOP,cloneNode:hostSCloneticNode,ins:hostInsertStaticContent}=选项//....这里省略两千行,我们先忽略return{render,hydrate,createApp:createAppAPI(render,hydrate)}}我们看到createAppAPI(render,hydrate)方法接受两个参数:根组件渲染函数render,可选参数hydrate应用于SSR场景,这里不用关注。exportfunctioncreateAppAPI(render:RootRenderFunction,hydrate?:RootHydrateFunction):CreateAppFunction{//createApp方法接受两个参数:根组件的对象和propreturnfunctioncreateApp(rootComponent,rootProps=null){if(rootProps!=null&&!isObject(rootProps)){__DEV__&&warn(`rootpropspassedtoapp.mount()mustbeanobject.`)rootProps=null}//创建默认APP配置constcontext=createAppContext()constinstalledPlugins=newSet()letisMounted=falseconstapp:App={_component:rootComponentasComponent,_props:rootProps,_container:null,_context:context,getconfig(){returncontext.config},setconfig(v){if(__DEV__){warn(`app.configcannotberelaced.Modifyindividualoptionsinstead.`)}},//是一些熟悉的方法use(){},mixin(){},component(){},directive(){},//用于挂载组件mount(rootContainer){//创建根组件VNodeconstvnode=createVNode(rootComponent,rootProps);//使用渲染器渲染VNodelender(vnode,rootContainer);app._container=rootComponent;returnvnode.component.proxy;}//...}returnapp}}在整个app对象的创建过程中,vue.js利用闭包和函数柯里化技术很好地实现了参数保持。比如在执行app.mount时,不需要传入renderer,因为renderer参数在creatingAppAPI执行时已经预留了。向下。我们知道vue源码中已经封装了mount方法,但是为什么我们在使用的时候需要重写,而不是直接在app对象的mount方法中实现相关逻辑呢?重写的目的是:实现既能使用户在使用API??时更加灵活,又能兼容Vue2的写法。这是因为vue.js不仅仅服务于web平台,它的设计目标是“星辰大海”——支持跨平台渲染,里面不能包含任何指定平台的内容。createApp函数里面的app.mount方法是一个标准的跨平台组件渲染流程:先创建VNode,再渲染VNode。mount(rootContainer){//创建根组件的VNodeconstvnode=createVNode(rootComponent,rootProps);//使用渲染器渲染VNodeler(vnode,rootContainer);app._container=rootComponent;returnvnode.component.proxy;}我们看到app被.mount改写的代码如下://重写挂载方法app.mount=(containerOrSelector:Element|string):any=>{//规范化容器constcontainer=normalizeContainer(containerOrSelector)//如果container为空对象,直接returnif(!container)returnconstcomponent=app._component//如果组件对象没有定义render函数和template模板,则直接取出容器的innerHTML方法作为组件templatecontentif(!isFunction(component)&&!component.render&&!component.template){component.template=container.innerHTML}//挂载前清除容器内容clearcontentbeforemountingcontainer.innerHTML=''//实现真正的挂载constproxy=安装(容器)容器。removeAttribute('v-cloak')returnproxy}4核心渲染过程:创建VNode并渲染VNodevnode本质上是用来描述DOM的Javascript对象。可以描述Vue中不同的节点,如:普通元素节点、组件节点等。我们可以用vnode来表示按钮标签:type:标签的类型props:标签的DOM属性信息children:DOM的子节点,标签类型的vnode数组constvnode={//type:》button",//标签的DOM属性Informationprops:{"class":"btn",style:{width:"100px",height:"100px"}},//dom的children,vnode数组children:"Confirm"}那么,我们可以使用vnode来描述抽象的东西,比如表示组件标签,页面实际上并不渲染一个叫做HelloWorld的标签元素,而是渲染组件内部定义的原生HTML标签元素。constHelloWorld={//定义组件对象信息}constvnode={type:HelloWorld,props:{msg:"test"}}我们在想:vnode有什么优势,为什么一定要像vnode那样设计成数据结构呢?摘要:vnode的引入可以对渲染过程进行抽象,从而提高组件的抽象能力。跨平台:因为不同的平台可以有自己的patchvnode流程的实现,给vnode做服务端渲染,weex平台,小程序平台渲染。但是,请注意:使用vnode并不意味着您不需要操作真实的DOM。很多人误以为vnode的性能会比手动操作DOM更好,其实并不能确定。这是因为:基于vnode实现的MVVM框架,在每次render到vnode的过程中,渲染组件都会消耗一定的javascript时间,尤其是大型组件。当我们更新组件时,我们可以感觉到明显的滞后现象。虽然diff算法在减少DOM操作上已经足够好了,但最终还是免不了要对DOM进行操作,所以性能上不能说是绝对优势。创建虚拟节点。我们之前看过源码,知道在vue中,根组件的vnode是通过createVNode函数创建的。constvnode=createVNode(rootComponent,rootProps);//createVNode函数的大概实现过程functioncreateVNode(type,props=null,children=null){if(props){//处理props的相关逻辑,标准化class和style}//对于vnode类型信息编码constshapeFlag=isString(type)?1/*ELEMENT*/:isSuspense(type)?128/*SUSPENSE*/:isTeleport(type)?64/*TELEPORT*/:isObject(type)?4/*STATEFUL_COMPONENT*/:isFunction(type)?2/*FUNCTIONAL_COMPONENT*/:0constvnode={type,props,shapeFlag,//其他属性}//标准化子节点,将不同数据类型的子节点转化为数组或文本类型normalizeChildren(vnode,children)returnvnode}renderingVNodeler(vnode,rootContainer)functionrender(vnode,rootContainer){//判断是否为空if(vnode==null){//如果为空则执行销毁逻辑componentif(container._vnode){unmount(container._vnode,null,null,true)}}else{//创建或更新组件patch(container._vnode||null,vnode,container)}//Cachevnode节点,表示容器已经渲染完成。_vnode=vnode}那么如何实现渲染vnode过程中涉及的patchpatch函数:functionpatch(n1,//旧vnode,当n1==null时,表示一次性挂载n2的过程,//新vnode会根据vnode类型容器执行不同的处理逻辑,//代表dom容器,vnode渲染生成DOM后,会挂载到容器下anchor=null,parentComponent=null,parentSuspense=null,isSVG=false,optimized=false){//如果有新旧节点,且新旧节点类型不同,销毁旧节点if(n1&&!isSameVNodeType(n1,n2)){anchor=getNextHostNode(n1);unmount(n1,parentComponent,parentSuspense,true);n1=null;}const{type,shapeFlag}=n2;switch(type){caseTest://处理文本节点breakcaseComment://处理注释节点breakcaseStatic://处理静态NodebreakcaseFragment://处理Fragment元素breakdefault:if(shapeFlag&1/*ELEMENT*/){//处理普通DOM元素processElemnt(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,optimized)}elseif(shapeFlag&64/*TELEPORT*/){//处理普通的TELEPORTprocessElemnt(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,optimized)}elseif(){}elseif(){}elseif(){}}}来看处理components实现parentComponent函数:functionparentComponent(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,optimized){if(n1==null){//挂载组件mountComponent(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,optimized)}else{//更新组件updateComponent(n1,n2,parentComponent,optimized)}}关于组件实例:Cre吃组件实例:内部通过对象创建当前渲染的组件实例的方式是设置组件实例:instance保留了很多组件相关的数据,维护了组件的上下文,包括props、slot等实例属性的初始化处理。初始渲染主要做了两件事:渲染组件生成一个subTree。将子树挂载到容器中,回到我们梦想开始的地方。我们可以看到在HelloWorld组件内部,整个DOM节点对应的vnode执行renderComponentRoot渲染生成对应的subTree。我们可以称它为“子树vnode”如果是别的平台这样如weex等,hostCreateElment方法不再是操作DOM,而是一个平台相关的API,这些平台相关的方法在renderer创建阶段作为参数传入,DOM节点创建完成后,需要先判断是否有props,然后在DOM节点中添加相关的class、style、event等属性,在hostPatchProp函数内部进行相关的处理逻辑。5嵌套组件生产开发中,App和hello组件的例子有组件嵌套场景,组件vnode主要维护组件的定义对象,组件上的各种props,组件本身就是一个抽象节点,其自身的渲染实际上是通过执行组件定义的render函数来渲染生成的子树vnode,然后通过patch的递归方式,无论组件嵌套层次有多深,都可以完成整个组件树的渲染。6参考文章《Vue3核心源码解析》《Vue中文社区》?7写在最后这篇文章主要是对组件的渲染过程进行分析和总结。我们从入口开始,逐层分析组件渲染过程的源码。我们知道一个组件需要被渲染到DOM中。以下三步:CreateVNodeRenderVNodeGeneraterealDOM