何时会进行虚拟函数的创建和渲染?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对象),初始化封装一系列相关的属性。
function createbasevnode( type: vnodetypes | classcomponent | typeof null_dynamic_component,//虚拟节点类型 props: (data & vnodeprops) | null = null,//内部的属性 children: unknown = null,//子节点内容 patchflag = 0,//patch标记 dynamicprops: string[] | null = null,//动态参数内容 shapeflag = type === fragment ? 0 : shapeflags.element,//节点类型的信息编码 isblocknode = false,//是否块节点 needfullchildrennormalization = false) {//声明一个vnode对象,并且将各种属性赋值,从而完成虚拟节点的初始化创建 const vnode = { __v_isvnode: true,//内部属性表示为vnode __v_skip: true,//表示跳过响应式转换 type, //虚拟节点类型 props,//虚拟节点内的属性和props key: props && normalizekey(props),//虚拟阶段的key用于diff ref: props && normalizeref(props),//引用 scopeid: currentscopeid,//作用域id slotscopeids: null,//插槽id children,//子节点内容,树形结构 component: null,//组件 suspense: null,//传送组件 sscontent: null, ssfallback: null, dirs: null,//目录 transition: null,//内置组件相关字段 el: null,//vnode实际被转换为dom元素的时候产生的元素,宿主 anchor: null,//锚点 target: null,//目标 targetanchor: null,//目标锚点 staticcount: 0,//静态节点数 shapeflag,//shape标记 patchflag,//patch标记 dynamicprops,//动态参数 dynamicchildren: null,//动态子节点 appcontext: null,//app上下文 ctx: currentrenderinginstance } as vnode //关于子节点和block节点的标准化和信息编码处理 return vnode}
由此可见,创建vnode就是一个对props中的内容进行标准化处理,然后对节点类型进行信息编码,对子节点的标准化处理和类型信息编码,最后创建vnode对象的过程。
render 渲染 vnodebasecreaterenderer()返回对象中,有render()函数,hydrate用于服务器渲染和createapp函数的。 在basecreaterenderer()函数中,定义了render()函数,render的内容不复杂。
组件在首次挂载,以及后续的更新等,都会触发mount(),而这些,其实都会调用render()渲染函数。render()会先判断vnode虚拟节点是否存在,如果不存在进行unmount()卸载操作。 如果存在则会调用patch()函数。因此可以推测,patch()的过程中,有关组件相关处理。
const render: rootrenderfunction = (vnode, container, issvg) => { if (vnode == null) {//判断是否传入虚拟节点,如果节点不存在则运行 if (container._vnode) {//判断容器中是否已有节点 unmount(container._vnode, null, null, true)//如果已有节点则卸载当前节点 } } else { //如果节点存在,则调用patch函数,从参数看,会传入新旧节点和容器 patch(container._vnode || null, vnode, container, null, null, null, issvg) } flushpreflushcbs() //组件更新前的回调 flushpostflushcbs()//组件更新后的回调 container._vnode = vnode//将虚拟节点赋值到容器上 }
patch vnode这里来看一下有关patch()函数的代码,侧重了解当组件初次渲染的时候的流程。
// 注意:此闭包中的函数应使用 'const xxx = () => {}'样式,以防止被小写器内联。// patch:进行diff算法,crateapp->vnode->elementconst patch: patchfn = ( n1,//老节点 n2,//新节点 container,//宿主元素 container anchor = null,//锚点,用来标识当我们对新旧节点做增删或移动等操作时,以哪个节点为参照物 parentcomponent = null,//父组件 parentsuspense = null,//父悬念 issvg = false, slotscopeids = null,//插槽 optimized = __dev__ && ishmrupdating ? false : !!n2.dynamicchildren ) => { if (n1 === n2) {// 如果新老节点相同则停止 return } // 打补丁且不是相同类型,则卸载旧节点,锚点后移 if (n1 && !issamevnodetype(n1, n2)) { anchor = getnexthostnode(n1) unmount(n1, parentcomponent, parentsuspense, true) n1 = null //n1复位 } //是否动态节点优化 if (n2.patchflag === patchflags.bail) { optimized = false n2.dynamicchildren = null } //结构n2新节点,获取新节点的类型 const { type, ref, shapeflag } = n2 switch (type) { case text: //文本类 processtext(n1, n2, container, anchor)//文本节点处理 break case comment://注释类 processcommentnode(n1, n2, container, anchor)//处理注释节点 break case static://静态类 if (n1 == null) {//如果老节点不存在 mountstaticnode(n2, container, anchor, issvg)//挂载静态节点 } break case fragment://片段类 processfragment( //进行片段处理 ) break default: if (shapeflag & shapeflags.element) {//如果类型编码是元素 processelement( n1, n2, container, anchor, parentcomponent, parentsuspense, issvg, slotscopeids, optimized ) } else if (shapeflag & shapeflags.component) {//如果类型编码是组件 processcomponent( n1, n2, container, anchor, parentcomponent, parentsuspense, issvg, slotscopeids, optimized ) } else if (shapeflag & shapeflags.teleport) { ;(type as typeof teleportimpl).process( // 如果类型是传送,进行处理 ) } else if (__feature_suspense__ && shapeflag & shapeflags.suspense) { ;(type as typeof suspenseimpl).process( //悬念处理 ) } } // 设置 参考 ref if (ref != null && parentcomponent) { setref(ref, n1 && n1.ref, parentsuspense, n2 || n1, !n2) } }
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 节点类型是组件下的处理 const processcomponent = ( n1: vnode | null,//老节点 n2: vnode,//新节点 container: rendererelement,//宿主 anchor: renderernode | null,//锚点 parentcomponent: componentinternalinstance | null,//父组件 parentsuspense: suspenseboundary | null,//父悬念 issvg: boolean, slotscopeids: string[] | null,//插槽 optimized: boolean ) => { n2.slotscopeids = slotscopeids if (n1 == null) {//如果老节点不存在,初次渲染的时候 //省略一部分n2其他情况下的处理 //挂载组件 mountcomponent( n2, container, anchor, parentcomponent, parentsuspense, issvg, optimized ) } else { //更新组件 updatecomponent(n1, n2, optimized) } }
老节点n1不存在null的时候,将挂载n2节点。如果老节点存在的时候,则更新组件。因此mountcomponent()最常见的就是在首次渲染的时候,那时旧节点都是空的。
接下来就是看如何挂载组件mountcomponent()
const mountcomponent: mountcomponentfn = ( initialvnode,//对应n2 新的节点 container,//对应宿主 anchor,//锚点 parentcomponent,//父组件 parentsuspense,//父传送 issvg,//是否svg optimized//是否优化 ) => { // 2.x编译器可以在实际安装前预先创建组件实例。 const compatmountinstance = //判断是不是根组件且是组件 __compat__ && initialvnode.iscompatroot && initialvnode.component const instance: componentinternalinstance = compatmountinstance || //创建组件实例 (initialvnode.component = createcomponentinstance( initialvnode, parentcomponent, parentsuspense )) // 如果新节点是缓存组件的话那么将internals赋值给期渲染函数 if (iskeepalive(initialvnode)) { ;(instance.ctx as keepalivecontext).renderer = internals } // 为了设置上下文处理props和slot插槽 if (!(__compat__ && compatmountinstance)) { //设置组件实例 setupcomponent(instance) } //setup()是异步的。这个组件在进行之前依赖于异步逻辑的解决 if (__feature_suspense__ && instance.asyncdep) { parentsuspense && parentsuspense.registerdep(instance, setuprendereffect) if (!initialvnode.el) {//如果n2没有宿主 const placeholder = (instance.subtree = createvnode(comment)) processcommentnode(null, placeholder, container!, anchor) } return } //设置运行渲染副作用函数 setuprendereffect( instance,//存储了新节点的组件上下文,props插槽等其他实例属性 initialvnode,//新节点n2 container,//容器 anchor,//锚点 parentsuspense,//父悬念 issvg,//是否svg optimized//是否优化 ) }
挂载组件中,除开缓存和悬挂上的函数处理,其逻辑上基本为:创建组件的实例createcomponentinstance(),设置组件实例 setupcomponent(instance)和设置运行渲染副作用函数setuprendereffect()。
创建组件实例,基本跟创建虚拟节点一样的,内部以对象的方式创建渲染组件实例。 设置组件实例,是将组件中许多数据,赋值给了instance,维护组件上下文,同时对props和插槽等属性初始化处理。
然后是setuprendereffect 设置渲染副作用函数;
const setuprendereffect: setuprendereffectfn = ( instance,//实例 initialvnode,//初始化节点 container,//容器 anchor,//锚点 parentsuspense,//父悬念 issvg,//是否是svg optimized//优化标记 ) => { //组件更新方法 const componentupdatefn = () => { //如果组件处于未挂载的状态下 if (!instance.ismounted) { let vnodehook: vnodehook | null | undefined //解构 const { el, props } = initialvnode const { bm, m, parent } = instance const isasyncwrappervnode = isasyncwrapper(initialvnode) togglerecurse(instance, false) // 挂载前的钩子 // 挂载前的节点 togglerecurse(instance, true) //这部分是跟服务器渲染相关的逻辑处理 //创建子树,同时 const subtree = (instance.subtree = rendercomponentroot(instance)) //递归 patch( null,//因为是挂载,所以n1这个老节点是空的。 subtree,//子树赋值到n2这个新节点 container,//挂载到container上 anchor, instance, parentsuspense, issvg ) //保留渲染生成的子树dom节点 initialvnode.el = subtree.el // 已挂载钩子 // 挂在后的节点 //激活为了缓存根的钩子 // #1742 激活的钩子必须在第一次渲染后被访问 因为该钩子可能会被子类的keep-alive注入。 instance.ismounted = true // #2458: deference mount-only object parameters to prevent memleaks // #2458: 遵从只挂载对象的参数以防止内存泄漏 initialvnode = container = anchor = null as any } else { // 更新组件 // 这是由组件自身状态的突变触发的(next: null)。或者父级调用processcomponent(下一个:vnode)。 } } // 创建用于渲染的响应式副作用 const effect = (instance.effect = new reactiveeffect( componentupdatefn, () => queuejob(update), instance.scope // 在组件的效果范围内跟踪它 )) //更新方法 const update: schedulerjob = (instance.update = () => effect.run()) //实例的uid赋值给更新的id update.id = instance.uid // 允许递归 // #1801, #2043 组件渲染效果应允许递归更新 togglerecurse(instance, true) update() }
setuprendereffect() 最后执行的了 update()方法,其实是运行了effect.run(),并且将其赋值给了instance.updata中。而 effect 涉及到了 vue3 的响应式模块,该模块的主要功能就是,让对象属性具有响应式功能,当其中的属性发生了变动,那effect副作用所包含的函数也会重新执行一遍,从而让界面重新渲染。这一块内容先不管。从effect函数看,明白了调用了componentupdatefn, 即组件更新方法,这个方法涉及了2个条件,一个是初次运行的挂载,而另一个是节点变动后的更新组件。 componentupdatefn中进行的初次渲染,主要是生成了subtree然后把subtree传递到patch进行了递归挂载到container上。
subtree是什么?subtree也是一个vnode对象,然而这里的subtree和initialvnode是不同的。以下面举个例子:
<template> <div class="app"> <p>title</p> <helloworld> </div></template>
而helloworld组件中是<div>标签包含一个<p>标签
<template> <div class="hello"> <p>hello world</p> </div></template>
在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
const processelement = ( n1: vnode | null, //老节点 n2: vnode,//新节点 container: rendererelement,//容器 anchor: renderernode | null,//锚点 parentcomponent: componentinternalinstance | null, parentsuspense: suspenseboundary | null, issvg: boolean, slotscopeids: string[] | null, optimized: boolean ) => { issvg = issvg || (n2.type as string) === 'svg' if (n1 == null) {//如果没有老节点,其实就是初次渲染,则运行mountelement mountelement( n2, container, anchor, parentcomponent, parentsuspense, issvg, slotscopeids, optimized ) } else { //如果是更新节点则运行patchelement patchelement( n1, n2, parentcomponent, parentsuspense, issvg, slotscopeids, optimized ) } }
逻辑依旧,如果有n1老节点为null的时候,运行挂载元素的逻辑,否则运行更新元素节点的方法。
以下是mountelement()的代码:
const mountelement = ( vnode: vnode, container: rendererelement, anchor: renderernode | null, parentcomponent: componentinternalinstance | null, parentsuspense: suspenseboundary | null, issvg: boolean, slotscopeids: string[] | null, optimized: boolean ) => { let el: rendererelement let vnodehook: vnodehook | undefined | null const { type, props, shapeflag, transition, dirs } = vnode //创建元素节点 el = vnode.el = hostcreateelement( vnode.type as string, issvg, props && props.is, props ) // 首先挂载子类,因为某些props依赖于子类内容 // 已经渲染, 例如 `<select value>` // 如果标记判断子节点类型是文本类型 if (shapeflag & shapeflags.text_children) { // 处理子节点是纯文本的情况 hostsetelementtext(el, vnode.children as string) //如果标记类型是数组子类 } else if (shapeflag & shapeflags.array_children) { //挂载子类,进行patch后进行挂载 mountchildren( vnode.children as vnodearraychildren, el, null, parentcomponent, parentsuspense, issvg && type !== 'foreignobject', slotscopeids, optimized ) } if (dirs) { invokedirectivehook(vnode, null, parentcomponent, 'created') } // 设置范围id setscopeid(el, vnode, vnode.scopeid, slotscopeids, parentcomponent) // props相关的处理,比如 class,style,event,key等属性 if (props) { for (const key in props) { if (key !== 'value' && !isreservedprop(key)) {//key值不等于value字符且不是 hostpatchprop( el, key, null, props[key], issvg, vnode.children as vnode[], parentcomponent, parentsuspense, unmountchildren ) } } if ('value' in props) { hostpatchprop(el, 'value', null, props.value) } if ((vnodehook = props.onvnodebeforemount)) { invokevnodehook(vnodehook, parentcomponent, vnode) } } object.defineproperty(el, '__vueparentcomponent', { value: parentcomponent, enumerable: false } } if (dirs) { invokedirectivehook(vnode, null, parentcomponent, 'beforemount') } // #1583 对于内部悬念+悬念未解决的情况,进入钩子应该在悬念解决时调用。 // #1689 对于内部悬念+悬念解决的情况,只需调用它 const needcalltransitionhooks = (!parentsuspense || (parentsuspense && !parentsuspense.pendingbranch)) && transition && !transition.persisted if (needcalltransitionhooks) { transition!.beforeenter(el) } //把创建的元素el挂载到container容器上。 hostinsert(el, container, anchor) if ( (vnodehook = props && props.onvnodemounted) || needcalltransitionhooks || dirs ) { queuepostrendereffect(() => { vnodehook && invokevnodehook(vnodehook, parentcomponent, vnode) needcalltransitionhooks && transition!.enter(el) dirs && invokedirectivehook(vnode, null, parentcomponent, 'mounted') }, parentsuspense) } }
mountelement挂载元素主要做了,创建dom元素节点,处理节点子节点,挂载子节点,同时对props相关处理。
所以根据代码,首先是通过hostcreateelement方法创建了dom元素节点。
const {createelement:hostcreateelement } = options
是从options这个实参中解构并重命名为hostcreateelement方法的,那么这个实参是从哪里来 需要追溯一下,回到初次渲染开始的流程中去。
从这流程图可以清楚的知道,options中createelement方法是从nodeops.ts文件中导出的并传入basecreaterender()方法内的。
该文件位于:core/packages/runtime-dom/src/nodeops.ts
createelement: (tag, issvg, is, props): element => { const el = issvg ? doc.createelementns(svgns, tag) : doc.createelement(tag, is ? { is } : undefined) if (tag === 'select' && props && props.multiple != null) { ;(el as htmlselectelement).setattribute('multiple', props.multiple) } return el },
从中可以看出,其实是调用了底层的dom api document.createelement创建元素。
说回上面,创建完dom节点元素之后,接下来是继续判断子节点的类型,如果子节点是文本类型的,则调用处理文本hostsetelementtext()方法。
const {setelementtext: hostsetelementtext} = optionsetelementtext: (el, text) => { el.textcontent = text },
与前面的createelement一样,setelementtext方法是通过设置dom元素的textcontent属性设置文本。
而如果子节点的类型是数组类,则执行mountchildren方法,对子节点进行挂载:
const mountchildren: mountchildrenfn = ( children,//子节点数组里的内容 container,//容器 anchor, parentcomponent, parentsuspense, issvg, slotscopeids, optimized,//优化标记 start = 0 ) => { //遍历子节点中的内容 for (let i = start; i < children.length; i++) { //根据优化标记进行判断进行克隆或者节点初始化处理。 const child = (children[i] = optimized ? cloneifmounted(children[i] as vnode) : normalizevnode(children[i])) //执行patch方法,递归挂载child patch( null,//因为是初次挂载所以没有老的节点 child,//虚拟子节点 container,//容器 anchor, parentcomponent, parentsuspense, issvg, slotscopeids, optimized ) } }
子节点的挂载逻辑看起来会非常眼熟,在对children数组进行遍历之后获取到的每一个child,进行预处理后并对其执行挂载方法。 结合之前调用mountchildren()方法传入的实参和其形参之间的对比。
mountchildren( vnode.children as vnodearraychildren, //节点中子节点的内容 el,//dom元素 null, parentcomponent, parentsuspense, issvg && type !== 'foreignobject', slotscopeids, optimized) const mountchildren: mountchildrenfn = ( children,//子节点数组里的内容 container,//容器 anchor, parentcomponent, parentsuspense, issvg, slotscopeids, optimized,//优化标记 start = 0 )
明确的对应上了第二个参数是container,而调用mountchildren方法时传入第二个参数的是在调用mountelement()时创建的dom节点,这样便建立起了父子关系。 而且,后续的继续递归patch(),能深度遍历树的方式,可以完整的把dom树遍历出来,完成渲染。
处理完节点的后,最后会调用 hostinsert(el, container, anchor)
const {insert: hostinsert} = optioninsert: (child, parent, anchor) => { parent.insertbefore(child, anchor || null)},
再次就用调用dom方法将子类的内容挂载到parent,也就是把child挂载到parent下,完成节点的挂载。
注意点:node.insertbefore(newnode,existingnode)中_existingnode_虽然是可选的对象,但是实际上,在不同的浏览器会有不同的表现形式,所以如果没有existingnode值的情况下,填入null会将新的节点添加到node子节点的尾部。
以上就是vue3怎么将虚拟节点渲染到网页初次渲染的详细内容。