首页 > 文章列表 > Vue3怎么将虚拟节点渲染到网页初次渲染

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

Vue3
173 2023-05-15

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 _createVNode

function _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 渲染 VNode

baseCreateRenderer()返回对象中,有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->element

const 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方法的,那么这个实参是从哪里来 需要追溯一下,回到初次渲染开始的流程中去。

从这流程图可以清楚的知道,optionscreateElement方法是从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} = option

setElementText: (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} = option

insert: (child, parent, anchor) => {

    parent.insertBefore(child, anchor || null)

},

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

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