Vue 3.0 進(jìn)階之 VNode 探秘
本文轉(zhuǎn)載自微信公眾號(hào)「全棧修仙之路」,作者阿寶哥 。轉(zhuǎn)載本文請(qǐng)聯(lián)系全棧修仙之路公眾號(hào)。
本文是 Vue 3.0 進(jìn)階系列 的第五篇文章,在這篇文章中,阿寶哥將介紹 Vue 3 中的核心對(duì)象 —— VNode,該對(duì)象用于描述節(jié)點(diǎn)的信息,它的全稱是虛擬節(jié)點(diǎn)(virtual node)。與 “虛擬節(jié)點(diǎn)” 相關(guān)聯(lián)的另一個(gè)概念是 “虛擬 DOM”,它是我們對(duì)由 Vue 組件樹建立起來的整個(gè) VNode 樹的稱呼。通常一個(gè) Vue 應(yīng)用會(huì)以一棵嵌套的組件樹的形式來組織:
(圖片來源:https://v3.cn.vuejs.org/)
所以 “虛擬 DOM” 對(duì) Vue 應(yīng)用來說,是至關(guān)重要的。而 “虛擬 DOM” 又是由 VNode 組成的,它是 Vue 底層的核心基石。接下來,阿寶哥將帶大家一起來探索 Vue 3 中與 VNode 相關(guān)的一些知識(shí)。
一、VNode 長(zhǎng)什么樣?
- // packages/runtime-core/src/vnode.ts
- export interface VNode<
- HostNode = RendererNode,
- HostElement = RendererElement,
- ExtraProps = { [key: string]: any }
- > {
- // 省略內(nèi)部的屬性
- }
在 runtime-core/src/vnode.ts 文件中,我們找到了 VNode 的類型定義。通過 VNode 的類型定義可知,VNode 本質(zhì)是一個(gè)對(duì)象,該對(duì)象中按照屬性的作用,分為 5 大類。這里阿寶哥只詳細(xì)介紹其中常見的兩大類型屬性 —— 內(nèi)部屬性 和 DOM 屬性:
1.1 內(nèi)部屬性
- __v_isVNode: true // 標(biāo)識(shí)是否為VNode
- [ReactiveFlags.SKIP]: true // 標(biāo)識(shí)VNode不是observable
- type: VNodeTypes // VNode 類型
- props: (VNodeProps & ExtraProps) | null // 屬性信息
- key: string | number | null // 特殊 attribute 主要用在 Vue 的虛擬 DOM 算法
- ref: VNodeNormalizedRef | null // 被用來給元素或子組件注冊(cè)引用信息。
- scopeId: string | null // SFC only
- children: VNodeNormalizedChildren // 保存子節(jié)點(diǎn)
- component: ComponentInternalInstance | null // 指向VNode對(duì)應(yīng)的組件實(shí)例
- dirs: DirectiveBinding[] | null // 保存應(yīng)用在VNode的指令信息
- transition: TransitionHooks<HostElement> | null // 存儲(chǔ)過渡效果信息
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 應(yīng)用上下文屬性
- appContext: AppContext | null
二、如何創(chuàng)建 VNode?
要?jiǎng)?chuàng)建 VNode 對(duì)象的話,我們可以使用 Vue 提供的 h 函數(shù)。也許可以更準(zhǔn)確地將其命名為 createVNode(),但由于頻繁使用和簡(jiǎn)潔,它被稱為 h() 。該函數(shù)接受三個(gè)參數(shù):
- // packages/runtime-core/src/h.ts
- export function h(type: any, 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(type, null, [propsOrChildren])
- }
- // 只包含屬性不含有子元素
- return createVNode(type, propsOrChildren) // h('div', { id: 'foo' })
- } else {
- // 忽略屬性
- return createVNode(type, null, propsOrChildren) // h('div', ['foo'])
- }
- } else {
- if (l > 3) {
- children = Array.prototype.slice.call(arguments, 2)
- } else if (l === 3 && isVNode(children)) {
- children = [children]
- }
- return createVNode(type, propsOrChildren, children)
- }
- }
觀察以上代碼可知, h 函數(shù)內(nèi)部的主要處理邏輯就是根據(jù)參數(shù)個(gè)數(shù)和參數(shù)類型,執(zhí)行相應(yīng)處理操作,但最終都是通過調(diào)用 createVNode 函數(shù)來創(chuàng)建 VNode 對(duì)象。在開始介紹 createVNode 函數(shù)前,阿寶哥先舉一些實(shí)際開發(fā)中的示例:
- const app = createApp({ // 示例一
- render: () => h('div', '我是阿寶哥')
- })
- const Comp = () => h("p", "我是阿寶哥"); // 示例二
- app.component('component-a', { // 示例三
- template: "<p>我是阿寶哥</p>"
- })
示例一和示例二很明顯都使用了 h 函數(shù),而示例三并未看到 h 或 createVNode 函數(shù)的身影。為了一探究竟,我們需要借助 Vue 3 Template Explorer 這個(gè)在線工具來編譯一下 "
<p>我是阿寶哥</p>" 模板,該模板編譯后的結(jié)果如下(函數(shù)模式):
- // 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, "我是阿寶哥"))
- }
- }
由以上編譯結(jié)果可知, "<p>我是阿寶哥</p>" 模板被編譯生成了一個(gè) render 函數(shù),調(diào)用該函數(shù)后會(huì)返回 createBlock 函數(shù)的調(diào)用結(jié)果。其中 createBlock 函數(shù)的實(shí)現(xiàn)如下所示:
- // packages/runtime-core/src/vnode.ts
- export function createBlock(
- type: VNodeTypes | ClassComponent,
- props?: Record<string, any> | 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 函數(shù)內(nèi)部,我們終于看到了 createVNode 函數(shù)的身影。顧名思義,該函數(shù)的作用就是用于創(chuàng)建 VNode,接下來我們來分析一下它。
三、createVNode 函數(shù)內(nèi)部做了啥?
下面我們將從參數(shù)說明和邏輯說明兩方面來介紹 createVNode 函數(shù):
3.1 參數(shù)說明
- // 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
- }
在分析該函數(shù)的具體代碼前,我們先來看一下它的參數(shù)。該函數(shù)可以接收 6 個(gè)參數(shù),這里阿寶哥用思維導(dǎo)圖來重點(diǎn)介紹前面 2 個(gè)參數(shù):
type 參數(shù)
- // packages/runtime-core/src/vnode.ts
- function _createVNode(
- type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
- // 省略其他參數(shù)
- ): VNode { ... }
由上圖可知,type 參數(shù)支持很多類型,比如常用的 string、VNode 和 Component 等。此外,也有一些陌生的面孔,比如 Text、Comment 、Static 和 Fragment 等類型,它們的定義如下:
- // 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' : undefined) as any) as {
- __isFragment: true
- new (): {
- $props: VNodeProps
- }
- }
那么定義那么多的類型有什么意義呢?這是因?yàn)樵?patch 階段,會(huì)根據(jù)不同的 VNode 類型來執(zhí)行不同的操作:
- // 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: // 處理文本節(jié)點(diǎn)
- processText(n1, n2, container, anchor)
- break
- case Comment: // 處理注釋節(jié)點(diǎn)
- processCommentNode(n1, n2, container, anchor)
- break
- case Static: // 處理靜態(tài)節(jié)點(diǎn)
- if (n1 == null) {
- mountStaticNode(n2, container, anchor, isSVG)
- } else if (__DEV__) {
- patchStaticNode(n1, n2, container, isSVG)
- }
- break
- case Fragment: // 處理Fragment節(jié)點(diǎn)
- processFragment(...)
- break
- default:
- if (shapeFlag & ShapeFlags.ELEMENT) { // 元素類型
- processElement(...)
- } else if (shapeFlag & ShapeFlags.COMPONENT) { // 組件類型
- processComponent(...)
- } else if (shapeFlag & ShapeFlags.TELEPORT) { // teleport內(nèi)置組件
- ;(type as typeof TeleportImpl).process(...)
- } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
- ;(type as typeof SuspenseImpl).process(...)
- }
- }
- }
- }
介紹完 type 參數(shù)后,接下來我們來看 props 參數(shù),具體如下圖所示:
props 參數(shù)
- function _createVNode(
- type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
- props: (Data & VNodeProps) | null = null,
- ): VNode { ... }
props 參數(shù)的類型是聯(lián)合類型,這里我們來分析 Data & VNodeProps 交叉類型:
其中 Data 類型是通過 TypeScript 內(nèi)置的工具類型 Record 來定義的:
- export type Data = Record<string, unknown>
- type Record<K extends keyof any, T> = {
- [P in K]: T;
- };
而 VNodeProps 類型是通過類型別名來定義的,除了含有 key 和 ref 屬性之外,其他的屬性主要是定義了與生命周期有關(guān)的鉤子:
- // 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 函數(shù)內(nèi)部涉及較多的處理邏輯,這里我們只分析主要的邏輯:
- // 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類型,比如處理動(dòng)態(tài)組件的場(chǎng)景:<component :is="vnode"/>
- if (isVNode(type)) {
- const cloned = cloneVNode(type, props, true /* mergeRef: true */)
- if (children) {
- normalizeChildren(cloned, children)
- }
- return cloned
- }
- // 類組件規(guī)范化處理
- if (isClassComponent(type)) {
- type = type.__vccOpts
- }
- // 類和樣式規(guī)范化處理
- if (props) {
- // 省略相關(guān)代碼
- }
- // 把vnode的類型信息轉(zhuǎn)換為位圖
- 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
- // 創(chuàng)建VNode對(duì)象
- const vnode: VNode = {
- __v_isVNode: true,
- [ReactiveFlags.SKIP]: true,
- type,
- props,
- // ...
- }
- // 子元素規(guī)范化處理
- normalizeChildren(vnode, children)
- return vnode
- }
介紹完 createVNode 函數(shù)之后,阿寶哥再來介紹另一個(gè)比較重要的函數(shù) —— normalizeVNode。
四、如何創(chuàng)建規(guī)范的 VNode 對(duì)象?
normalizeVNode 函數(shù)的作用,用于將傳入的 child 參數(shù)轉(zhuǎn)換為規(guī)范的 VNode 對(duì)象。
- // 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, null, String(child))
- }
- }
由以上代碼可知,normalizeVNode 函數(shù)內(nèi)部會(huì)根據(jù) child 參數(shù)的類型進(jìn)行不同的處理:
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 對(duì)象?
- // packages/runtime-core/src/vnode.ts
- export function isVNode(value: any): value is VNode {
- return value ? value.__v_isVNode === true : false
- }
在 VNode 對(duì)象中含有一個(gè) __v_isVNode 內(nèi)部屬性,利用該屬性可以用來判斷當(dāng)前對(duì)象是否為 VNode 對(duì)象。
5.2 如何判斷兩個(gè) VNode 對(duì)象的類型是否相同?
- // packages/runtime-core/src/vnode.ts
- export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
- // 省略__DEV__環(huán)境的處理邏輯
- return n1.type === n2.type && n1.key === n2.key
- }
在 Vue 3 中,是通過比較 VNode 對(duì)象的 type 和 key 屬性,來判斷兩個(gè) VNode 對(duì)象的類型是否相同。
5.3 如何快速創(chuàng)建某些類型的 VNode 對(duì)象?
在 Vue 3 內(nèi)部提供了 createTextVNode 、createCommentVNode 和 createStaticVNode 函數(shù)來快速的創(chuàng)建文本節(jié)點(diǎn)、注釋節(jié)點(diǎn)和靜態(tài)節(jié)點(diǎn):
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 對(duì)象是什么、如何創(chuàng)建 VNode 對(duì)象及如何創(chuàng)建規(guī)范的 VNode 對(duì)象。為了讓大家能夠更深入地理解 h 和 createVNode 函數(shù)的相關(guān)知識(shí),阿寶哥還從源碼的角度分析了 createVNode 函數(shù) 。
在后續(xù)的文章中,阿寶哥將會(huì)介紹 VNode 在 Vue 3 內(nèi)部是如何被使用的,感興趣的小伙伴不要錯(cuò)過喲。
六、參考資源
Vue 3 官網(wǎng) - 渲染函數(shù)