本文是 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(type: any, propsOrChildren?: any, children?: any): VNode {const l = arguments.lengthif (l === 2) { if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { // single vnode without propsif (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 函数内部的主要处理逻辑就是根据参数个数和参数类型,执行相应处理操作,但最终都是通过调用 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 } = _Vuereturn (_openBlock(), _createBlock("p", null, "我是阿宝哥"))}
}

由以上编译结果可知, "<p>我是阿宝哥</p>" 模板被编译生成了一个 render 函数,调用该函数后会返回 createBlock 函数的调用结果。其中 createBlock 函数的实现如下所示:

// 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 函数内部,我们终于看到了 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 _createVNodefunction _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' : undefined) as any) as {__isFragment: truenew (): {$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 } = n2switch (type) {case Text: // 处理文本节点processText(n1, n2, container, anchor)breakcase Comment: // 处理注释节点processCommentNode(n1, n2, container, anchor)breakcase Static: // 处理静态节点if (n1 == null) {mountStaticNode(n2, container, anchor, isSVG)} else if (__DEV__) {patchStaticNode(n1, n2, container, isSVG)}breakcase Fragment: // 处理Fragment节点processFragment(...)breakdefault: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 | numberref?: VNodeRef// vnode hooksonVnodeBeforeMount?: 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 -> Commentreturn createVNode(Comment)} else if (isArray(child)) { // array -> Fragmentreturn createVNode(Fragment, null, child)} else if (typeof child === 'object') { // VNode -> VNode or mounted VNode -> cloned VNodereturn child.el === null ? child : cloneVNode(child)} else { // primitive types:'foo' or 1return createVNode(Text, null, String(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 = numberOfNodesreturn vnode
}

本文阿宝哥主要介绍了 VNode 对象是什么、如何创建 VNode 对象及如何创建规范的 VNode 对象。为了让大家能够更深入地理解 hcreateVNode 函数的相关知识,阿宝哥还从源码的角度分析了 createVNode 函数 。

在后续的文章中,阿宝哥将会介绍 VNode 在 Vue 3 内部是如何被使用的,感兴趣的小伙伴不要错过哟。

六、参考资源

  • Vue 3 官网 - 渲染函数

聚焦全栈,专注分享 TypeScript、Web API、前端架构等技术干货。

【Vue.js】900- Vue 3.0 进阶之 VNode 探秘相关推荐

  1. 【Vue.js】892- Vue 3.0 进阶之动态组件探秘

    本文是 Vue 3.0 进阶系列 的第四篇文章,在这篇文章中,阿宝哥将介绍 Vue 3 中的内置组件 -- component,该组件的作用是渲染一个 "元组件" 为动态组件.如果 ...

  2. Vue.js 框架源码与进阶 - Vue.js 源码剖析 - 响应式原理

    文章目录 一.准备工作 1.1 Vue 源码的获取 1.2 源目录结构 1.3 了解 Flow 1.4 调试设置 1.5 Vue 的不同构建版本 1.6 寻找入口文件 1.7 从入口开始 二.Vue ...

  3. Vue.js(一) Vue.js + element-ui 扫盲

    Vue.js(一) Vue.js + element-ui 扫盲 2018年12月09日 20:32:59 vbirdbest 阅读数 7043更多 分类专栏: Vue.js + ElementUI ...

  4. vue.js:597 [Vue warn]: Error in callback for watcher dat: TypeError: Cannot read property 'call'

    vue.js:597 [Vue warn]: Error in callback for watcher "dat": "TypeError: Cannot read p ...

  5. vue.js:590 [Vue tip]: Event “removeitem“ is emitted in component <TodoItems> but the handler is regi

    报错信息 vue.js:590 [Vue tip]: Event "removeitem" is emitted in component <TodoItems> bu ...

  6. vue.js报错 vue.js:597 [Vue warn]: Cannot find element: #app

    刚开始使用vue的时候发现vue报vue.js:597 [Vue warn]: Cannot find element: #app的错误,初始以为是写的代码有错误导致,于是认真的对照了几遍发现代码和官 ...

  7. Vue.js教程-Vue项目的目录结构和.vue文件的构成

    Vue.js教程-Vue项目的目录结构和.vue文件的构成 前言 Vue项目的目录结构(Vue-cli3/4版本) .vue文件的构成 Html区域(template) script区域 export ...

  8. vue在html中执行js代码,Vue.js 和 Vue.runtime.js

    Vue官方中文文档: Vue有两个版本: 完整版:vue.js.vue.min.js(运行时版+编译器)(编译器:将模板字符串编译成为JS渲染函数的代码) 运行时版:vue.runtime.js.vu ...

  9. Vue.js 框架源码与进阶 - 搭建自己的SSR

    文章目录 一.Vue SSR 介绍 1.1 Vue SSR 是什么 1.2 使用场景 1.3 如何实现 Vue SSR 二.Vue SSR 基本使用 2.1 渲染一个 Vue 实例 2.2 与服务器集 ...

最新文章

  1. python大家都会吗_一篇告诉你为什么人人都应该学点Python?
  2. sphinx mysql存储引擎_基于Sphinx+MySQL的千万级数据全文检索(搜索引擎)架构设计...
  3. oracle11g ogg报价,Oracle11g GoldenGate配置错误OGG-00868 Attaching to ASM server
  4. Android心电数据分析,Android SurfaceView+Canvas画脉搏/心电数据图-Go语言中文社区
  5. Graphviz 入口
  6. Ubuntu18.04编译Android7.1.2源码(刷机Pixel)
  7. layui让当前页面刷新_layui点击按钮页面会自动刷新的解决方案
  8. Dubbo 没落了吗?
  9. 洛谷OJ P2356 弹珠游戏 维护前缀和
  10. 卡塔编程_量子卡塔教您如何在Q#中进行量子编程
  11. uniapp web设置ios safri浏览器 添加到屏幕 像是应用 但是不用证书
  12. 鸿蒙石boss 怎么杀,《仙侠世界》沧海岛副本介绍 沧海岛副本怎么玩
  13. android 辅助服务默认开启,Android 检测辅助功能是否开启,并调整设置页面
  14. oracle中字段类型为date存储数据精确到时分秒的问题
  15. 鸿蒙系统2.0崩溃了,集体失声?鸿蒙系统官宣后,鸿蒙系统的真实现状显现
  16. 程序员的自我修养_之二_曾国藩的“大悔大悟”
  17. 2天,我把MySQL索引、锁、事务、分库分表撸干净了!
  18. 一个软件公司需要多少前端_开发APP软件需要多少钱?
  19. Hackthebox:Arctic Walkthrough
  20. 违禁词检索chrom扩展插件

热门文章

  1. pythonif嵌套语句案例_Python系列07:if嵌套语句
  2. Mac 电脑安装 Android Studio 配置代理教程
  3. Pixhawk ulog飞行日志分析
  4. 八月十七日个人训练小结(补题)
  5. Kason研发出一套用于金属3D打印机的金属粉末回收系统
  6. 分数构造方法java,Java--构造方法
  7. B站开源了动漫画质修复模型!超分辨率无杂线无伪影!二次元狂喜!
  8. Linux 符号系列
  9. 天津天地伟业程序员怎么样_第一批市级制造业单项冠军培育企业名单公布 天津制造业 铸造创新驱动发展强引擎...
  10. 浏览器访问云服务器上图片的两种方法