ホームページ  >  記事  >  ウェブフロントエンド  >  Vue3 は初期レンダリングのために仮想ノードを Web ページにどのようにレンダリングしますか?

Vue3 は初期レンダリングのために仮想ノードを Web ページにどのようにレンダリングしますか?

WBOY
WBOY転載
2023-05-10 15:07:061062ブラウズ

Text

createApp 関数内の app.mount メソッドは、標準のクロスプラットフォーム コンポーネント レンダリング プロセスです。最初に VNode を作成し、次に VNode をレンダリングします。

Vue3 は初期レンダリングのために仮想ノードを Web ページにどのようにレンダリングしますか?

#仮想関数はいつ作成され、レンダリングされますか?

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
  }
}
Vue3 は初期レンダリングのために仮想ノードを Web ページにどのようにレンダリングしますか?マウント プロセス中、操作がマウントされていない場合、 const vnode = createVNode(rootComponent as ConcreteComponent,rootProps) は仮想ノードを作成し、vnode (仮想ノード)、rootContainer (ルート コンテナー)、および isSVG を次のように渡します。パラメータ. render 関数でレンダリングします。

VNode とは何ですか?

仮想ノードは実際には JavaScript のオブジェクトであり、DOM を記述するために使用されます。

ここでは、理解を助けるための実用的な簡単な例を書くことができます。以下は HTML の共通要素ノードです。

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

仮想ノードを使用してそれを表現するにはどうすればよいですか?

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
}

ここの公式ドキュメントにはアドバイスが記載されています: 完全な

VNode

インターフェイスには他の内部プロパティが含まれていますが、ここにリストされていないこれらのプロパティは使用しないことを強くお勧めします。これにより、内部プロパティの変更によって引き起こされる非互換性の問題が回避されます。 vue3 には、vnode タイプのより詳細な分類があります。 vnode を作成する前に、まず

shapeFlags

を理解してください。このクラスはそれに応じて型情報をエンコードします。そのため、パッチフェーズでは、対応する論理処理をさまざまなタイプで実行できます。同時に、型には要素、メソッド関数コンポーネント、状態を持つコンポーネント、サブクラスはテキストなどがあることもわかります。 予備情報

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

現在の仮想ノードの種類を示すために使用されます。

shapeFlag

に対してバイナリ演算を実行することで、現在のノード自体の型とその子ノードの型を記述することができます。 なぜ Vnode を使用するのか?

vnode はレンダリング プロセスを抽象化し、コンポーネントの抽象化能力を向上させることができるためです。 Vue はクロスプラットフォームである必要があるため、プラットフォーム自体を通じてノードの抽象化を実装でき、さまざまなプラットフォームでのレンダリングが容易になります。ただし、vnode を使用しているからといって、vnode のパフォーマンスが有利であるわけではないことに注意してください。たとえば、大きなコンポーネントが数千行のテーブルである場合、レンダリング プロセス中に、vnode の作成では必然的に数千の vnode 作成をトラバースし、さらに数千のパッチをトラバースする必要があり、テーブル データを更新するときに、必然的にラグが発生します。場合。パッチ内で diff を使用して DOM 操作の数を最適化した場合でも、操作は常に必要です。

Vnode はどのように作成されますか?

vue3 は、vnode を作成するための

h()

関数を提供します。<pre class="brush:js;">import {h} from &amp;#39;vue&amp;#39; h(&amp;#39;div&amp;#39;, { id: &amp;#39;foo&amp;#39; })</pre>本質は、

createVNode()

関数を呼び出すことでもあります。 <pre class="brush:js;"> const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)</pre>

createVNode()

core/packages/runtime-core/src/vnode.ts<pre class="brush:js;">//创建虚拟节点 export const createVNode = ( _createVNode) as typeof _createVNode function _createVNode( //标签类型 type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, //数据和vnode的属性 props: (Data &amp; 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 ) }</pre>にあります。開発中の内部ロジック処理のみが削除されています。ここで環境下でのみ実行されるコード:

まず

  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 Render VNode

baseCreateRenderer()

返されたオブジェクトには render() 関数があり、ハイドレートはサーバー レンダリングに使用されます。そしてcreateApp関数。 baseCreateRenderer()関数内には、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//将虚拟节点赋值到容器上
  }
Vue3 は初期レンダリングのために仮想ノードを Web ページにどのようにレンダリングしますか?patch VNode

ここでは、コンポーネントが次の場合に焦点を当てた、

patch()

関数のコードを見ていきます。最初にレンダリングされたプロセス。

// 注意:此闭包中的函数应使用 &#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)
    }
  }

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

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

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

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

  • 对新节点的类型判断

    • 文本类:processText

    • 注释类:processComment

    • 静态类:mountStaticNode

    • 片段类:processFragment

    • 默认

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

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

Vue3 は初期レンダリングのために仮想ノードを Web ページにどのようにレンダリングしますか?

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组件中是

标签包含一个

标签

<template>
	<div class="hello">
		<p>hello world</p>
	</div>
</template>

在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

Vue3 は初期レンダリングのために仮想ノードを Web ページにどのようにレンダリングしますか?

  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
      )
    }
  }

逻辑依旧,如果有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)
    }
  }

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

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

const {createElement:hostCreateElement } = options

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

Vue3 は初期レンダリングのために仮想ノードを Web ページにどのようにレンダリングしますか?

从这流程图可以清楚的知道,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
  },

从中可以看出,其实是调用了底层的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 !== &#39;foreignObject&#39;,
	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子节点的尾部。Vue3 は初期レンダリングのために仮想ノードを Web ページにどのようにレンダリングしますか?

以上がVue3 は初期レンダリングのために仮想ノードを Web ページにどのようにレンダリングしますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。