Home > Web Front-end > Vue.js > How does Vue3 render virtual nodes to the web page for initial rendering?

How does Vue3 render virtual nodes to the web page for initial rendering?

WBOY
Release: 2023-05-10 15:07:06
forward
1183 people have browsed it

Text

The app.mount method inside the createApp function is a standard cross-platform component rendering process: first create a VNode, and then render the VNode.

How does Vue3 render virtual nodes to the web page for initial rendering?

When will virtual functions be created and rendered?

During the initialization process of vue3, createApp() points to the source code core/packages/runtime-core/src/apiCreateApp.ts

How does Vue3 render virtual nodes to the web page for initial rendering?

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
  }
}
Copy after login

During the mount process, when the operation is not mounted, const vnode = createVNode(rootComponent as ConcreteComponent,rootProps) creates a virtual node and passes vnode (virtual node), rootContainer (root container), and isSVG as parameters. Render in the render function.

What is VNode?

Virtual node is actually an object of JavaScript, used to describe the DOM.

Here you can write a practical simple example to assist understanding. The following is a common element node of a piece of html

<div class="title" >这是一个标题</div>
Copy after login

How to use virtual nodes to represent it?

const VNode ={
	type:&#39;div&#39;,
	props:{
		class:&#39;title&#39;,
		style:{
			fontSize:&#39;16px&#39;,
			width:&#39;100px&#39;
		}
	},
	children:&#39;这是一个标题&#39;,
	key:null
}
Copy after login

The official documentation here gives advice: The complete VNode interface contains other internal properties, but it is strongly recommended to avoid using these properties not listed here. This avoids incompatibility issues caused by internal property changes.

vue3 has a more detailed classification of vnode types. Before creating a vnode, first understand shapeFlags. This class encodes the type information accordingly. So that in the patch phase, corresponding logical processing can be performed through different types. At the same time, you can also see that type has elements, method function components, components with state, subclasses are text, etc.

Preliminary information

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
  //传送,将一个组件内部的模板‘传送&#39;到该组件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
Copy after login

It is used to indicate the type of the current virtual node. We can describe the type of the current node itself and the type of its child nodes by performing binary operations on shapeFlag.

Why use Vnode?

Because vnode can abstract, abstract the rendering process, and improve the abstraction ability of components. Then because Vue needs to be cross-platform, the node abstraction can be implemented through the platform itself, making it easier to render on various platforms. However, it should be noted that although vnode is used, this does not mean that the performance of vnode is more advantageous. For example, if a large component is a table with thousands of rows, during the rendering process, creating a vnode will inevitably have to traverse thousands of vnode creations, and then traverse thousands of patches. When updating table data, lags will inevitably occur. Case. Even if you use diff in the patch to optimize the number of DOM operations, operations are always required.

How is Vnode created?

vue3 provides a h() function for creating vnodes:

import {h} from &#39;vue&#39;
h(&#39;div&#39;, { id: &#39;foo&#39; })
Copy after login

The essence is also to call the createVNode() function.

 const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)
Copy after login

createVNode()Located in 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
  )
}
Copy after login

The internal logic processing just omitted, only in development is removed here Code that is run only under the environment:

First determine

  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
  }
Copy after login
  // 类组件规范化
  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)
    }
  }
Copy after login

and combine it with the previous shapeFlags enumeration class, assign the determined encoding to 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
Copy after login

and above, and set the virtual node After some of the attributes are processed, they are then passed into the basic virtual node creation function to create further and more detailed attribute objects.

createBaseVNode Virtual node initialization creation

Create a basic virtual node (JavaScript object), initialize and encapsulate a series of related attributes.

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
}
Copy after login

It can be seen that creating a vnode is a process of standardizing the content in props, then encoding the node type information, standardizing and encoding the type information of the child nodes, and finally creating the vnode object.

render Render VNode

baseCreateRenderer()In the returned object, there is the render() function, and hydrate is used for server rendering and createApp functions. In the baseCreateRenderer() function, the render() function is defined, and the content of render is not complicated.

The component will trigger mount() when it is mounted for the first time and subsequent updates, and these will actually call the render() rendering function. render() will first determine whether the vnode virtual node exists, and if it does not exist, perform the unmount() uninstall operation. If it exists, the patch() function will be called. Therefore, it can be inferred that during the process of patch(), the relevant components are processed.

How does Vue3 render virtual nodes to the web page for initial rendering?

 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//将虚拟节点赋值到容器上
  }
Copy after login

patch VNode

Here is a look at the code for the patch() function, focusing on when the component is first rendered. process.

How does Vue3 render virtual nodes to the web page for initial rendering?

// 注意:此闭包中的函数应使用 &#39;const xxx = () => {}&#39;样式,以防止被小写器内联。
// 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)
    }
  }
Copy after login

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

  • 新旧节点相同则直接返回

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

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

  • 对新节点的类型判断

    • 文本类:processText

    • 注释类:processComment

    • 静态类:mountStaticNode

    • 片段类:processFragment

    • 默认

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

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

How does Vue3 render virtual nodes to the web page for initial rendering?

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)
    }
  }
Copy after login

老节点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//是否优化
    )
  }
Copy after login

挂载组件中,除开缓存和悬挂上的函数处理,其逻辑上基本为:创建组件的实例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() 
  }
Copy after login

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>
Copy after login

而helloWorld组件中是

标签包含一个

标签

<template>
	<div class="hello">
		<p>hello world</p>
	</div>
</template>
Copy after login

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

标签,而对应的subTree就是普通元素vnode,接下来就是堆普通Element处理的流程。

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

How does Vue3 render virtual nodes to the web page for initial rendering?

  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) === &#39;svg&#39;
    if (n1 == null) {//如果没有老节点,其实就是初次渲染,则运行mountElement
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
	   //如果是更新节点则运行patchElement
      patchElement(
        n1,
        n2,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }
Copy after login

逻辑依旧,如果有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 !== &#39;foreignObject&#39;,
        slotScopeIds,
        optimized
      )
    }
    if (dirs) {
      invokeDirectiveHook(vnode, null, parentComponent, &#39;created&#39;)
    }
    // 设置范围id
    setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
    // props相关的处理,比如 class,style,event,key等属性
    if (props) { 
      for (const key in props) { 
        if (key !== &#39;value&#39; && !isReservedProp(key)) {//key值不等于value字符且不是
          hostPatchProp(
            el,
            key,
            null,
            props[key],
            isSVG,
            vnode.children as VNode[],
            parentComponent,
            parentSuspense,
            unmountChildren
          )
        }
      }
      
      if (&#39;value&#39; in props) {
        hostPatchProp(el, &#39;value&#39;, null, props.value)
      }
      if ((vnodeHook = props.onVnodeBeforeMount)) {
        invokeVNodeHook(vnodeHook, parentComponent, vnode)
      }
    }
      Object.defineProperty(el, &#39;__vueParentComponent&#39;, {
        value: parentComponent,
        enumerable: false
      }
    }
    if (dirs) {
      invokeDirectiveHook(vnode, null, parentComponent, &#39;beforeMount&#39;)
    }
    // #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, &#39;mounted&#39;)
      }, parentSuspense)
    }
  }
Copy after login

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

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

const {createElement:hostCreateElement } = options
Copy after login

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

How does Vue3 render virtual nodes to the web page for initial rendering?

从这流程图可以清楚的知道,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 === &#39;select&#39; && props && props.multiple != null) {
      ;(el as HTMLSelectElement).setAttribute(&#39;multiple&#39;, props.multiple)
    }
    return el
  },
Copy after login

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

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

const {setElementText: hostSetElementText} = option
setElementText: (el, text) => {
    el.textContent = text
  },
Copy after login

与前面的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
      )
    }
  }
Copy after login

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

mountChildren(
	vnode.children as VNodeArrayChildren, //节点中子节点的内容
	el,//DOM元素
	null,
	parentComponent,
	parentSuspense,
	isSVG && type !== &#39;foreignObject&#39;,
	slotScopeIds,
	optimized
)
      
const mountChildren: MountChildrenFn = (
	children,//子节点数组里的内容
	container,//容器
	anchor,
	parentComponent,
	parentSuspense,
	isSVG,
	slotScopeIds,
	optimized,//优化标记
	start = 0
  )
Copy after login

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

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

const {insert: hostInsert} = option
insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
},
Copy after login

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

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

The above is the detailed content of How does Vue3 render virtual nodes to the web page for initial rendering?. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:yisu.com
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template