【Vue.js】900- Vue 3.0 進階之 VNode 探祕

本文是 Vue 3.0 進階系列 的第五篇文章,在這篇文章中,阿寶哥將介紹 Vue 3 中的核心對象 —— VNode,該對象用於描述節點的信息,它的全稱是虛擬節點(virtual node)。與 “虛擬節點” 相關聯的另一個概念是 “虛擬 DOM”,它是我們對由 Vue 組件樹建立起來的整個 VNode 樹的稱呼。通常一個 Vue 應用會以一棵嵌套的組件樹的形式來組織:

(圖片來源:https://v3.cn.vuejs.org/)

所以 “虛擬 DOM” 對 Vue 應用來說,是至關重要的。而 “虛擬 DOM” 又是由 VNode 組成的,它是 Vue 底層的核心基石。接下來,阿寶哥將帶大家一起來探索 Vue 3 中與 VNode 相關的一些知識。

一、VNode 長什麼樣?

// packages/runtime-core/src/vnode.ts
export interface VNode<
  HostNode = RendererNode,
  HostElement = RendererElement,
  ExtraProps = { [key: string]: any }
> {
 // 省略內部的屬性
}

runtime-core/src/vnode.ts 文件中,我們找到了 VNode 的類型定義。通過 VNode 的類型定義可知,VNode 本質是一個對象,該對象中按照屬性的作用,分爲 5 大類。這裏阿寶哥只詳細介紹其中常見的兩大類型屬性 —— 內部屬性DOM 屬性

1.1 內部屬性

__v_isVNode: true // 標識是否爲VNode
[ReactiveFlags.SKIP]: true // 標識VNode不是observable
type: VNodeTypes // VNode 類型
props: (VNodeProps & ExtraProps) | null // 屬性信息
key: string | number | null // 特殊 attribute 主要用在 Vue 的虛擬 DOM 算法
ref: VNodeNormalizedRef | null // 被用來給元素或子組件註冊引用信息。
scopeId: string | null // SFC only
children: VNodeNormalizedChildren // 保存子節點
component: ComponentInternalInstance | null // 指向VNode對應的組件實例
dirs: DirectiveBinding[] | null // 保存應用在VNode的指令信息
transition: TransitionHooks<HostElement> | null // 存儲過渡效果信息

1.2 DOM 屬性

el: HostNode | null // element 
anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target
targetAnchor: HostNode | null // teleport target anchor
staticCount: number // number of elements contained in a static vnode

1.3 suspense 屬性

suspense: SuspenseBoundary | null
ssContent: VNode | null
ssFallback: VNode | null

1.4 optimization 屬性

shapeFlag: number
patchFlag: number
dynamicProps: string[] | null
dynamicChildren: VNode[] | null

1.5 應用上下文屬性

appContext: AppContext | null

二、如何創建 VNode?

要創建 VNode 對象的話,我們可以使用 Vue 提供的 h 函數。也許可以更準確地將其命名爲 createVNode(),但由於頻繁使用和簡潔,它被稱爲 h() 。該函數接受三個參數:

// packages/runtime-core/src/h.ts
export function h(typeany, propsOrChildren?: any, children?: any): VNode {
  const l = arguments.length
  if (l === 2) { 
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { 
      // single vnode without props
      if (isVNode(propsOrChildren)) {
        return createVNode(typenull, [propsOrChildren])
      }
      // 只包含屬性不含有子元素
      return createVNode(type, propsOrChildren) // h('div', { id: 'foo' })
    } else {
      // 忽略屬性
      return createVNode(typenull, propsOrChildren) // h('div', ['foo'])
    }
  } else {
    if (l > 3) {
      children = Array.prototype.slice.call(arguments2)
    } else if (l === 3 && isVNode(children)) {
      children = [children]
    }
    return createVNode(type, propsOrChildren, children)
  }
}

觀察以上代碼可知, h 函數內部的主要處理邏輯就是根據參數個數和參數類型,執行相應處理操作,但最終都是通過調用 createVNode 函數來創建 VNode 對象。在開始介紹 createVNode 函數前,阿寶哥先舉一些實際開發中的示例:

const app = createApp({ // 示例一
  render() => h('div''我是阿寶哥')
})

const Comp = () => h("p""我是阿寶哥"); // 示例二

app.component('component-a', { // 示例三
  template"<p>我是阿寶哥</p>"
})

示例一和示例二很明顯都使用了 h 函數,而示例三並未看到 hcreateVNode 函數的身影。爲了一探究竟,我們需要藉助 Vue 3 Template Explorer 這個在線工具來編譯一下 "<p>我是阿寶哥</p>" 模板,該模板編譯後的結果如下(函數模式):

// https://vue-next-template-explorer.netlify.app/
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options{
  with (_ctx) {
    const { createVNode: _createVNode, openBlock: _openBlock,
      createBlock: _createBlock } = _Vue
    return (_openBlock(), _createBlock("p"null"我是阿寶哥"))
  }
}

由以上編譯結果可知, "<p>我是阿寶哥</p>" 模板被編譯生成了一個 render 函數,調用該函數後會返回 createBlock 函數的調用結果。其中 createBlock 函數的實現如下所示:

// packages/runtime-core/src/vnode.ts
export function createBlock(
  type: VNodeTypes | ClassComponent,
  props?: Record<stringany> | null,
  children?: any,
  patchFlag?: number,
  dynamicProps?: string[]
): VNode 
{
  const vnode = createVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    true /* isBlock: prevent a block from tracking itself */
  )
  // 省略部分代碼
  return vnode
}

createBlock 函數內部,我們終於看到了 createVNode 函數的身影。顧名思義,該函數的作用就是用於創建 VNode,接下來我們來分析一下它。

三、createVNode 函數內部做了啥?

下面我們將從參數說明和邏輯說明兩方面來介紹 createVNode 函數:

3.1 參數說明

createVNode 被定義在 runtime-core/src/vnode.ts 文件中:

// packages/runtime-core/src/vnode.ts
export const createVNode = (__DEV__
  ? createVNodeWithArgsTransform
  : _createVNode) as typeof _createVNode

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode 
{
  // 
  return vnode
}

在分析該函數的具體代碼前,我們先來看一下它的參數。該函數可以接收 6 個參數,這裏阿寶哥用思維導圖來重點介紹前面 2 個參數:

type 參數
// packages/runtime-core/src/vnode.ts
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  // 省略其他參數
): VNode 
{ ... }

由上圖可知,type 參數支持很多類型,比如常用的 stringVNodeComponent 等。此外,也有一些陌生的面孔,比如 TextCommentStaticFragment 等類型,它們的定義如下:

// packages/runtime-core/src/vnode.ts
export const Text = Symbol(__DEV__ ? 'Text' : undefined)
export const Comment = Symbol(__DEV__ ? 'Comment' : undefined)
export const Static = Symbol(__DEV__ ? 'Static' : undefined)

export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefinedas anyas {
  __isFragment: true
  new (): {
    $props: VNodeProps
  }
}

那麼定義那麼多的類型有什麼意義呢?這是因爲在 patch 階段,會根據不同的 VNode 類型來執行不同的操作:

// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any 
{
  const patch: PatchFn = (
    n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null,
    isSVG = false, optimized = false
  ) => {
    // 省略部分代碼
    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)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment: // 處理Fragment節點
        processFragment(...)
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) { // 元素類型
          processElement(...)
        } else if (shapeFlag & ShapeFlags.COMPONENT) { // 組件類型
          processComponent(...)
        } else if (shapeFlag & ShapeFlags.TELEPORT) { // teleport內置組件
          ;(type as typeof TeleportImpl).process(...)
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(...)
        }
    }
  }
}

介紹完 type 參數後,接下來我們來看 props 參數,具體如下圖所示:

props 參數
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
): VNode 
{ ... }

props 參數的類型是聯合類型,這裏我們來分析 Data & VNodeProps 交叉類型:

其中 Data 類型是通過 TypeScript 內置的工具類型 Record 來定義的:

export type Data = Record<string, unknown>
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

VNodeProps 類型是通過類型別名來定義的,除了含有 keyref 屬性之外,其他的屬性主要是定義了與生命週期有關的鉤子:

// packages/runtime-core/src/vnode.ts
export type VNodeProps = {
  key?: string | number
  ref?: VNodeRef

  // vnode hooks
  onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[]
  onVnodeMounted?: VNodeMountHook | VNodeMountHook[]
  onVnodeBeforeUpdate?: VNodeUpdateHook | VNodeUpdateHook[]
  onVnodeUpdated?: VNodeUpdateHook | VNodeUpdateHook[]
  onVnodeBeforeUnmount?: VNodeMountHook | VNodeMountHook[]
  onVnodeUnmounted?: VNodeMountHook | VNodeMountHook[]
}

3.2 邏輯說明

createVNode 函數內部涉及較多的處理邏輯,這裏我們只分析主要的邏輯:

// packages/runtime-core/src/vnode.ts
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode 
{
  // 處理VNode類型,比如處理動態組件的場景:<component :is="vnode"/>
  if (isVNode(type)) {
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    return cloned
  }

  // 類組件規範化處理
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }

  // 類和樣式規範化處理
  if (props) {
    // 省略相關代碼
  }

  // 把vnode的類型信息轉換爲位圖
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT // ELEMENT = 1
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE // SUSPENSE = 1 << 7,
      : isTeleport(type)
        ? ShapeFlags.TELEPORT // TELEPORT = 1 << 6,
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT // STATEFUL_COMPONENT = 1 << 2,
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT // FUNCTIONAL_COMPONENT = 1 << 1,
            : 0

  // 創建VNode對象
  const vnode: VNode = {
    __v_isVNode: true,
    [ReactiveFlags.SKIP]: true,
    type,
    props,
    // ...
  }

  // 子元素規範化處理
  normalizeChildren(vnode, children)
  return vnode
}

介紹完 createVNode 函數之後,阿寶哥再來介紹另一個比較重要的函數 —— normalizeVNode

四、如何創建規範的 VNode 對象?

normalizeVNode 函數的作用,用於將傳入的 child 參數轉換爲規範的 VNode 對象。

// packages/runtime-core/src/vnode.ts
export function normalizeVNode(child: VNodeChild): VNode {
  if (child == null || typeof child === 'boolean') { // null/undefined/boolean -> Comment
    return createVNode(Comment)
  } else if (isArray(child)) { // array -> Fragment
    return createVNode(Fragment, null, child)
  } else if (typeof child === 'object') { // VNode -> VNode or mounted VNode -> cloned VNode
    return child.el === null ? child : cloneVNode(child)
  } else { // primitive types:'foo' or 1
    return createVNode(Text, nullString(child))
  }
}

由以上代碼可知,normalizeVNode 函數內部會根據 child 參數的類型進行不同的處理:

4.1 null / undefined -> Comment

expect(normalizeVNode(null)).toMatchObject({ type: Comment })
expect(normalizeVNode(undefined)).toMatchObject({ type: Comment })

4.2 boolean -> Comment

expect(normalizeVNode(true)).toMatchObject({ type: Comment })
expect(normalizeVNode(false)).toMatchObject({ type: Comment })

4.3 array -> Fragment

expect(normalizeVNode(['foo'])).toMatchObject({ type: Fragment })

4.4 VNode -> VNode

const vnode = createVNode('div')
expect(normalizeVNode(vnode)).toBe(vnode)

4.5 mounted VNode -> cloned VNode

const mounted = createVNode('div')
mounted.el = {}
const normalized = normalizeVNode(mounted)
expect(normalized).not.toBe(mounted)
expect(normalized).toEqual(mounted)

4.6 primitive types

expect(normalizeVNode('foo')).toMatchObject({ type: Text, children: `foo` })
expect(normalizeVNode(1)).toMatchObject({ type: Text, children: `1` })

五、阿寶哥有話說

5.1 如何判斷是否爲 VNode 對象?

// packages/runtime-core/src/vnode.ts
export function isVNode(value: any): value is VNode {
  return value ? value.__v_isVNode === true : false
}

VNode 對象中含有一個 __v_isVNode 內部屬性,利用該屬性可以用來判斷當前對象是否爲 VNode 對象。

5.2 如何判斷兩個 VNode 對象的類型是否相同?

// packages/runtime-core/src/vnode.ts
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  // 省略__DEV__環境的處理邏輯
  return n1.type === n2.type && n1.key === n2.key
}

在 Vue 3 中,是通過比較 VNode 對象的 typekey 屬性,來判斷兩個 VNode 對象的類型是否相同。

5.3 如何快速創建某些類型的 VNode 對象?

在 Vue 3 內部提供了 createTextVNodecreateCommentVNodecreateStaticVNode 函數來快速的創建文本節點、註釋節點和靜態節點:

createTextVNode
export function createTextVNode(text: string = ' ', flag: number = 0): VNode {
  return createVNode(Text, null, text, flag)
}
createCommentVNode
export function createCommentVNode(
  text: string = '',
  asBlock: boolean = false
): VNode 
{
  return asBlock
    ? (openBlock(), createBlock(Comment, null, text))
    : createVNode(Comment, null, text)
}
createStaticVNode
export function createStaticVNode(
  content: string,
  numberOfNodes: number
): VNode 
{
  const vnode = createVNode(Static, null, content)
  vnode.staticCount = numberOfNodes
  return vnode
}

本文阿寶哥主要介紹了 VNode 對象是什麼、如何創建 VNode 對象及如何創建規範的 VNode 對象。爲了讓大家能夠更深入地理解 hcreateVNode 函數的相關知識,阿寶哥還從源碼的角度分析了 createVNode 函數 。

在後續的文章中,阿寶哥將會介紹 VNode 在 Vue 3 內部是如何被使用的,感興趣的小夥伴不要錯過喲。

六、參考資源

  • Vue 3 官網 - 渲染函數
聚焦全棧,專注分享 TypeScript、Web API、前端架構等技術乾貨。

本文分享自微信公衆號 - 前端自習課(FE-study)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章