【Vue.js】890- Vue 3.0 进阶之 VNode 探秘

发表于 3年以前  | 总阅读数:788 次

本文是 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.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 函数内部的主要处理逻辑就是根据参数个数和参数类型,执行相应处理操作,但最终都是通过调用 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 这个在线工具来编译一下 "我是阿宝哥" 模板,该模板编译后的结果如下(函数模式):

// 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, "我是阿宝哥"))
  }
}

由以上编译结果可知, "我是阿宝哥" 模板被编译生成了一个 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 _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' : undefined) as any) as {
  __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, 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 = numberOfNodes
  return vnode
}

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

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

六、参考资源

  • Vue 3 官网 - 渲染函数

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/4IXmux6Qlfltop95KF6w1g

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 目录