Vue3怎么将虚拟节点渲染到网页初次渲染

广告:宝塔Linux面板高效运维的服务器管理软件 点击【 https://www.bt.cn/p/uNLv1L 】立即购买

Vue3怎么将虚拟节点渲染到网页初次渲染

正文

createApp函数内部的app.mount方法是一个标准的可跨平台的组件渲染流程:先创建VNode,再渲染VNode。

何时会进行虚拟函数的创建和渲染?

vue3初始化过程中,createApp()指向的源码 core/packages/runtime-core/src/apiCreateApp.ts中

export function createAppAPI<HostElement>(  render: RootRenderFunction<HostElement>,//由之前的baseCreateRenderer中的render传入  hydrate?: RootHydrateFunction): CreateAppFunction<HostElement> {return function createApp(rootComponent, rootProps = null) {//rootComponent根组件    let isMounted = false    //生成一个具体的对象,提供对应的API和相关属性    const app: App = (context.app = {//将以下参数传入到context中的app里      //...省略其他逻辑处理      //挂载      mount(        rootContainer: HostElement,        isHydrate?: boolean,//是用来判断是否用于服务器渲染,这里不讲所以省略        isSVG?: boolean      ): any {      //如果处于未挂载完毕状态下运行      if (!isMounted) {      //创建一个新的虚拟节点传入根组件和根属性          const vnode = createVNode(            rootComponent as ConcreteComponent,            rootProps          )          // 存储app上下文到根虚拟节点,这将在初始挂载时设置在根实例上。          vnode.appContext = context          }          //渲染虚拟节点,根容器          render(vnode, rootContainer, isSVG)          isMounted = true //将状态改变成为已挂载          app._container = rootContainer          // for devtools and telemetry          ;(rootContainer as any).__vue_app__ = app          return getExposeProxy(vnode.component!) || vnode.component!.proxy      }},    })    return app  }}
登录后复制

在mount的过程中,当运行处于未挂载时, const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)创建虚拟节点并且将 vnode(虚拟节点)、rootContainer(根容器),isSVG作为参数传入render函数中去进行渲染。

什么是VNode?

虚拟节点其实就是JavaScript的一个对象,用来描述DOM。

这里可以编写一个实际的简单例子来辅助理解,下面是一段html的普通元素节点

<div class="title" >这是一个标题</div>
登录后复制

如何用虚拟节点来表示?

const VNode ={type:'div',props:{class:'title',style:{fontSize:'16px',width:'100px'}},children:'这是一个标题',key:null}
登录后复制

这里官方文档给出了建议:完整的 VNode 接口包含其他内部属性,但是强烈建议避免使用这些没有在这里列举出的属性。这样能够避免因内部属性变更而导致的不兼容性问题。

vue3对vnode的type做了更详细的分类。在创建vnode之前先了解一下shapeFlags,这个类对type的类型信息做了对应的编码。以便之后在patch阶段,可以通过不同的类型执行对应的逻辑处理。同时也能看到type有元素,方法函数组件,带状态的组件,子类是文本等。

前置须知ShapeFlags
// package/shared/src/shapeFlags.ts//这是一个ts的枚举类,从中也能了解到虚拟节点的类型export const enum ShapeFlags {//DOM元素 HTML  ELEMENT = 1,  //函数式组件  FUNCTIONAL_COMPONENT = 1 << 1, //2  //带状态的组件  STATEFUL_COMPONENT = 1 << 2,//4  //子节点是文本  TEXT_CHILDREN = 1 << 3,//8  //子节点是数组  ARRAY_CHILDREN = 1 << 4,//16  //子节点带有插槽  SLOTS_CHILDREN = 1 << 5,//32  //传送,将一个组件内部的模板‘传送'到该组件DOM结构外层中去,例如遮罩层的使用  TELEPORT = 1 << 6,//64  //悬念,用于等待异步组件时渲染一些额外的内容,比如骨架屏,不过目前是实验性功能  SUSPENSE = 1 << 7,//128  //要缓存的组件  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,//256  //已缓存的组件  COMPONENT_KEPT_ALIVE = 1 << 9,//512  //组件  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT}//4 | 2
登录后复制

它用来表示当前虚拟节点的类型。我们可以通过对shapeFlag做二进制运算来描述当前节点的本身是什么类型、子节点是什么类型。

为什么要使用Vnode?

因为vnode可以抽象,把渲染的过程抽象化,使组件的抽象能力也得到提升。 然后因为vue需要可以跨平台,讲节点抽象化后可以通过平台自己的实现,使之在各个平台上渲染更容易。 不过同时需要注意的一点,虽然使用的是vnode,但是这并不意味着vnode的性能更具有优势。比如很大的组件,是表格上千行的表格,在render过程中,创建vnode势必得遍历上千次vnode的创建,然后遍历上千次的patch,在更新表格数据中,势必会出现卡顿的情况。即便是在patch中使用diff优化了对DOM操作次数,但是始终需要操作。

Vnode是如何创建的?

vue3 提供了一个 h() 函数用于创建 vnodes:

import {h} from 'vue'h('div', { id: 'foo' })
登录后复制

其本质也是调用 createVNode()函数。

 const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)
登录后复制

createVNode()位于 core/packages/runtime-core/src/vnode.ts

//创建虚拟节点export const createVNode = ( _createVNode) as typeof _createVNodefunction _createVNode(//标签类型  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,  //数据和vnode的属性  props: (Data & VNodeProps) | null = null,  //子节点  children: unknown = null,  //patch标记  patchFlag: number = 0,  //动态参数  dynamicProps: string[] | null = null,  //是否是block节点  isBlockNode = false): VNode {  //内部逻辑处理    //使用更基层的createBaseVNode对各项参数进行处理  return createBaseVNode(    type,    props,    children,    patchFlag,    dynamicProps,    shapeFlag,    isBlockNode,    true  )}
登录后复制

刚才省略的内部逻辑处理,这里去除了只有在开发环境下才运行的代码:

先是判断
  if (isVNode(type)) {//创建虚拟节点接收到已存在的节点,这种情况发生在诸如 <component :is="vnode"/>    // #2078 确保在克隆过程中合并refs,而不是覆盖它。    const cloned = cloneVNode(type, props, true /* mergeRef: true */)    //如果拥有子节点,将子节点规范化处理    if (children) {normalizeChildren(cloned, children)}://将拷贝的对象存入currentBlock中    if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {      if (cloned.shapeFlag & ShapeFlags.COMPONENT) {        currentBlock[currentBlock.indexOf(type)] = cloned      } else {        currentBlock.push(cloned)      }    }    cloned.patchFlag |= PatchFlags.BAIL    //返回克隆    return cloned  }
登录后复制
  // 类组件规范化  if (isClassComponent(type)) {    type = type.__vccOpts   }  // 类(class)和风格(style) 规范化.  if (props) {    //对于响应式或者代理的对象,我们需要克隆来处理,以防止触发响应式和代理的变动    props = guardReactiveProps(props)!    let { class: klass, style } = props    if (klass && !isString(klass)) {      props.class = normalizeClass(klass)    }    if (isObject(style)) {     // 响应式对象需要克隆后再处理,以免触发响应式。      if (isProxy(style) && !isArray(style)) {        style = extend({}, style)      }      props.style = normalizeStyle(style)    }  }
登录后复制

与之前的shapeFlags枚举类结合,将定好的编码赋值给shapeFlag

  // 将虚拟节点的类型信息编码成一个位图(bitmap)  // 根据type类型来确定shapeFlag的属性值  const shapeFlag = isString(type)//是否是字符串    ? ShapeFlags.ELEMENT//传值1    : __FEATURE_SUSPENSE__ && isSuspense(type)//是否是悬念类型    ? ShapeFlags.SUSPENSE//传值128    : isTeleport(type)//是否是传送类型    ? ShapeFlags.TELEPORT//传值64    : isObject(type)//是否是对象类型    ? ShapeFlags.STATEFUL_COMPONENT//传值4    : isFunction(type)//是否是方法类型    ? ShapeFlags.FUNCTIONAL_COMPONENT//传值2    : 0//都不是以上类型 传值0
登录后复制

以上,将虚拟节点其中一部分的属性处理好之后,再传入创建基础虚拟节点函数中,做更进一步和更详细的属性对象创建。

createBaseVNode 虚拟节点初始化创建

创建基础虚拟节点(JavaScript对象),初始化封装一系列相关的属性。

<div class="title" >这是一个标题</div>0
登录后复制

由此可见,创建vnode就是一个对props中的内容进行标准化处理,然后对节点类型进行信息编码,对子节点的标准化处理和类型信息编码,最后创建vnode对象的过程。

render 渲染 VNode

baseCreateRenderer()返回对象中,有render()函数,hydrate用于服务器渲染和createApp函数的。 在baseCreateRenderer()函数中,定义了render()函数,render的内容不复杂。

组件在首次挂载,以及后续的更新等,都会触发mount(),而这些,其实都会调用render()渲染函数。render()会先判断vnode虚拟节点是否存在,如果不存在进行unmount()卸载操作。 如果存在则会调用patch()函数。因此可以推测,patch()的过程中,有关组件相关处理。

<div class="title" >这是一个标题</div>1
登录后复制patch VNode

这里来看一下有关patch()函数的代码,侧重了解当组件初次渲染的时候的流程。

<div class="title" >这是一个标题</div>2
登录后复制

patch函数可见,主要做的就是 新旧虚拟节点之间的对比,这也是常说的diff算法,结合render(vnode, rootContainer, isSVG)可以看出vnode对应的是n1也就是新节点,而rootContainer对应n2,也就是老节点。其做的逻辑判断是。

新旧节点相同则直接返回

旧节点存在,且新节点和旧节点的类型不同,旧节点将被卸载unmount且复位清空null。锚点移向下个节点。

新节点是否是动态值优化标记

对新节点的类型判断

文本类:processText

注释类:processComment

静态类:mountStaticNode

片段类:processFragment

默认

而这个默认才是主要的部分也是最常用到的部分。里面包含了对类型是元素element、组件component、传送teleport、悬念suspense的处理。这次主要讲的是虚拟节点到组件和普通元素渲染的过程,其他类型的暂时不提,内容展开过于杂乱。

实际上第一次初始运行的时候,patch判断vnode类型根节点,因为vue3书写的时候,都是以组件的形式体现,所以第一次的类型势必是component类型。

processComponent 节点类型是组件下的处理
<div class="title" >这是一个标题</div>3
登录后复制

老节点n1不存在null的时候,将挂载n2节点。如果老节点存在的时候,则更新组件。因此mountComponent()最常见的就是在首次渲染的时候,那时旧节点都是空的。

接下来就是看如何挂载组件mountComponent()

<div class="title" >这是一个标题</div>4
登录后复制

挂载组件中,除开缓存和悬挂上的函数处理,其逻辑上基本为:创建组件的实例createComponentInstance(),设置组件实例 setupComponent(instance)和设置运行渲染副作用函数setupRenderEffect()

创建组件实例,基本跟创建虚拟节点一样的,内部以对象的方式创建渲染组件实例。 设置组件实例,是将组件中许多数据,赋值给了instance,维护组件上下文,同时对props和插槽等属性初始化处理。

然后是setupRenderEffect 设置渲染副作用函数;

<div class="title" >这是一个标题</div>5
登录后复制

setupRenderEffect() 最后执行的了 update()方法,其实是运行了effect.run(),并且将其赋值给了instance.updata中。而 effect 涉及到了 vue3 的响应式模块,该模块的主要功能就是,让对象属性具有响应式功能,当其中的属性发生了变动,那effect副作用所包含的函数也会重新执行一遍,从而让界面重新渲染。这一块内容先不管。从effect函数看,明白了调用了componentUpdateFn, 即组件更新方法,这个方法涉及了2个条件,一个是初次运行的挂载,而另一个是节点变动后的更新组件。 componentUpdateFn中进行的初次渲染,主要是生成了subTree然后把subTree传递到patch进行了递归挂载到container上。

subTree是什么?

subTree也是一个vnode对象,然而这里的subTree和initialVNode是不同的。以下面举个例子:

<div class="title" >这是一个标题</div>6
登录后复制

而helloWorld组件中是<div>标签包含一个<p>标签

<div class="title" >这是一个标题</div>7
登录后复制

在App组件中,<helloWorld> 节点渲染渲染生成的vnode就是 helloWorld组件的initialVNode,而这个组件内部所有的DOM节点就是vnode通过执行renderComponentRoot渲染生成的的subTree。 每个组件渲染的时候都会运行render函数,renderComponentRoot就是去执行render函数创建整个组件内部的vnode,然后进行标准化就得到了该函数的返回结果:子树vnode。 生成子树后,接下来就是继续调用patch函数把子树vnode挂载到container上去。 回到patch后,就会继续对子树vnode进行判断,例如上面的App组件的根节点是<div>标签,而对应的subTree就是普通元素vnode,接下来就是堆普通Element处理的流程。

当节点的类型是普通元素DOM时候,patch判断运行processElement

<div class="title" >这是一个标题</div>8
登录后复制

逻辑依旧,如果有n1老节点为null的时候,运行挂载元素的逻辑,否则运行更新元素节点的方法。

以下是mountElement()的代码:

<div class="title" >这是一个标题</div>9
登录后复制

mountElement挂载元素主要做了,创建DOM元素节点,处理节点子节点,挂载子节点,同时对props相关处理。

所以根据代码,首先是通过hostCreateElement方法创建了DOM元素节点。

const VNode ={type:'div',props:{class:'title',style:{fontSize:'16px',width:'100px'}},children:'这是一个标题',key:null}0
登录后复制

是从options这个实参中解构并重命名为hostCreateElement方法的,那么这个实参是从哪里来 需要追溯一下,回到初次渲染开始的流程中去。

从这流程图可以清楚的知道,optionscreateElement方法是从nodeOps.ts文件中导出的并传入baseCreateRender()方法内的。

该文件位于:core/packages/runtime-dom/src/nodeOps.ts

const VNode ={type:'div',props:{class:'title',style:{fontSize:'16px',width:'100px'}},children:'这是一个标题',key:null}1
登录后复制

从中可以看出,其实是调用了底层的DOM API document.createElement创建元素。

说回上面,创建完DOM节点元素之后,接下来是继续判断子节点的类型,如果子节点是文本类型的,则调用处理文本hostSetElementText()方法。

const VNode ={type:'div',props:{class:'title',style:{fontSize:'16px',width:'100px'}},children:'这是一个标题',key:null}2
登录后复制

与前面的createElement一样,setElementText方法是通过设置DOM元素的textContent属性设置文本。

而如果子节点的类型是数组类,则执行mountChildren方法,对子节点进行挂载:

const VNode ={type:'div',props:{class:'title',style:{fontSize:'16px',width:'100px'}},children:'这是一个标题',key:null}3
登录后复制

子节点的挂载逻辑看起来会非常眼熟,在对children数组进行遍历之后获取到的每一个child,进行预处理后并对其执行挂载方法。 结合之前调用mountChildren()方法传入的实参和其形参之间的对比。

const VNode ={type:'div',props:{class:'title',style:{fontSize:'16px',width:'100px'}},children:'这是一个标题',key:null}4
登录后复制

明确的对应上了第二个参数是container,而调用mountChildren方法时传入第二个参数的是在调用mountElement()时创建的DOM节点,这样便建立起了父子关系。 而且,后续的继续递归patch(),能深度遍历树的方式,可以完整的把DOM树遍历出来,完成渲染。

处理完节点的后,最后会调用 hostInsert(el, container, anchor)

const VNode ={type:'div',props:{class:'title',style:{fontSize:'16px',width:'100px'}},children:'这是一个标题',key:null}5
登录后复制

再次就用调用DOM方法将子类的内容挂载到parent,也就是把child挂载到parent下,完成节点的挂载。

注意点:node.insertBefore(newnode,existingnode)中_existingnode_虽然是可选的对象,但是实际上,在不同的浏览器会有不同的表现形式,所以如果没有existingnode值的情况下,填入null会将新的节点添加到node子节点的尾部。

以上就是Vue3怎么将虚拟节点渲染到网页初次渲染的详细内容,更多请关注9543建站博客其它相关文章!

广告:SSL证书一年128.66元起,点击购买~~~

9543建站博客
一个专注于网站开发、微信开发的技术类纯净博客。
作者头像
admin创始人

肥猫,知名SEO博客站长,14年SEO经验。

上一篇:jquery怎样实现点击跳转页面
下一篇:laravel的redis用法

发表评论

评论列表

2026-03-05 03:01:41

楼主好聪明啊!https://www.pc-helloworld.com.cn

关闭广告
关闭广告