Skip to content

Latest commit

 

History

History
224 lines (195 loc) · 7.65 KB

File metadata and controls

224 lines (195 loc) · 7.65 KB

设计 VNode

render 函数返回结果就是 h 函数执行的结果,因此 h 函数的输出为 VNode

所以需要先设计一下我们的 VNode

用 VNode 描述 HTML

一个 html 标签有它的标签名、属性、事件、样式、子节点等诸多信息,这些内容都需要在 VNode 中体现。

<div id="div">
  div text
  <p>p text</p>
</div>
const elementVNode = {
  tag: 'div',
  props: {
    id: 'div'
  },
  text: 'div text',
  children: [{
    tag: 'p',
    props: null,
    text: 'p text'
  }]
}

上面的代码显示了 DOM 变成 VNode 的表现形式,VNode 各属性解释:

  • tag :表示 DOM 元素的标签名,如 divspan
  • props:表示 DOM 元素上的属性,如idclass
  • children:表示 DOM 元素的子节点
  • text:表示 DOM 元素的文本节点

这样设计 VNode 完全没有问题(实际上 Vue 2 就是这样设计的),但是 Vue 3 设计的 VNode 并不包含 text 属性,而是直接用 children 代替,因为 text 本质也是 DOM 的子节点。

在保证语义讲得通的情况下尽可能复用属性,可以使 VNode 对象更加轻量

基于此我们把刚才的 VNode 修改成如下形式:

const elementVNode = {
  tag: 'div',
  props: {
    id: 'div'
  },
  children: [{
    tag: null,
    props: null,
    children: 'div text'
  }, {
    tag: 'p',
    props: null,
    children: 'p text'
  }]
}

用 VNode 描述抽象内容

什么是抽象内容呢?组件就属于抽象内容,比如下面这一段模板内容:

<div>
  <MyComponent></MyComponent>
</div>

MyComponent 是一个组件,我们预期渲染出 MyComponent 组件所有的内容,而不是一个 MyComponent 标签,这用 VNode 如何表示呢?

上一段内容我们其实已经通过 tag 是否为 null 来区分元素节点和文本节点了,那这里我们可以通过 tag 是否是字符串判断是标签还是组件呢?

const elementVNode = {
  tag: 'div',
  props: null,
  children: [{
    tag: MyComponent,
    props: null
  }]
}

理论上是可以的,Vue 2 中就是通过 tag 来判断的,具体过程如下,可以在这里看源码

  1. VNode.tag 如果不是字符串,则创建组件类型的 VNode
  2. VNode.tag 是字符串
    1. 若是内置的 htmlsvg 标签,则创建正常的 VNode
    2. 若是属于某个组件的 id,则创建组件类型的 VNode
    3. 未知或没有命名空间的组件,直接创建 VNode

以上这些判断都是在挂载(或 patch)阶段进行的,换句话说,一个 VNode 表示的内容需要在代码运行阶段才知道。这就带来了两个难题:无法从 AOT 的层面优化、开发者无法手动优化。

如果可以提前知道 VNode 类型,那么就可以对其进行优化,所以这里我们可以定义好一套用来判断 VNode 类型的规则,随便是用 FLAG = 1 这样的数字表示还是其它方法。

区分 VNode 类型

这里我们给 VNode 增加一个字段 shapeFlag(这是为了和 Vue 3 保持一致),它是一个枚举类型变量,具体如下:

export const enum ShapeFlags {
  // html 或 svg 标签
  ELEMENT = 1,
  // 函数式组件
  FUNCTIONAL_COMPONENT = 1 << 1,
  // 普通有状态组件
  STATEFUL_COMPONENT = 1 << 2,
  // 子节点是纯文本
  TEXT_CHILDREN = 1 << 3,
  // 子节点是数组
  ARRAY_CHILDREN = 1 << 4,
  // 子节点是 slots
  SLOTS_CHILDREN = 1 << 5,
  // Portal
  PORTAL = 1 << 6,
  // Suspense
  SUSPENSE = 1 << 7,
  // 需要被keepAlive的有状态组件
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  // 已经被keepAlive的有状态组件
  COMPONENT_KEPT_ALIVE = 1 << 9,
  // 有状态组件和函数式组件都是“组件”,用 COMPONENT 表示
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

现在我们可以修改我们的 VNode 如下:

const elementVNode = {
  shapeFlag: ShapeFlags.ELEMENT,
  tag: 'div',
  props: null,
  children: [{
    shapeFlag: ShapeFlags.COMPONENT,
    tag: MyComponent,
    props: null
  }]
}

shapeFlag 如何用来判断 VNode 类型呢?按位运算即可。

const isComponent = vnode.shapeFlag & ShapeFlags.COMPONENT

熟悉一下按位运算。

  • a & b:对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0。
  • a | b:对于每一个比特位,当两个操作数相应的比特位至少有一个1时,结果为1,否则为0。

我们把 ShapeFlags 对应的值列出来,如下:

ShapeFlags 操作 bitmap
ELEMENT 0000000001
FUNCTIONAL_COMPONENT 1 << 1 0000000010
STATEFUL_COMPONENT 1 << 2 0000000100
TEXT_CHILDREN 1 << 3 0000001000
ARRAY_CHILDREN 1 << 4 0000010000
SLOTS_CHILDREN 1 << 5 0000100000
PORTAL 1 << 6 0001000000
SUSPENSE 1 << 7 0010000000
COMPONENT_SHOULD_KEEP_ALIVE 1 << 8 0100000000
COMPONENT_KEPT_ALIVE 1 << 9 1000000000
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT

根据上表展示的基本 flags 值可以很容易地得出下表:

ShapeFlags bitmap
COMPONENT 00000001 10

区分 children 的类型

上面我们已经看到了 children 可以是数组或纯文本,但真实场景可能是:

  • null
  • 纯文本
  • 数组

这里我们可以增加一个 ChildrenShapeFlags 的变量表示 children 的类型,但是基于之前的设计原则,我们完全可以用 ShapeFlags 来表示,那么同一个 ShapeFlags 如何既用来表示 VNode 的类型,又用来表示其 children 的类型呢?

仍然是按位运算,我们通过 JavaScript 代码判断 children 类型,然后和当前 VNode 进行按位或运算即可。

我们增加如下函数用来专门处理子节点类型,这和 Vue 3 中的处理一致:

function normalizeChildren(vnode, children) {
  let type = 0
  if (children == null) {
    children = null
  } else if (Array.isArray(children)) {
    type = ShapeFlags.ARRAY_CHILDREN
  } else if (typeof children === 'string') {
    children = String(children)
    type = ShapeFlags.TEXT_CHILDREN
  }
  vnode.shapeFlag |= type
}

这样我们就可以直接通过 shapeFlag 同时判断 VNode 及其 children 类型了。

为什么 children 也需要标识呢?原因只有一个:为了 patch 过程的优化

定义 VNode

至此,我们可以定义 VNode 结构如下:

export interface VNodeProps {
  [key: string]: any
}
export interface VNode {
  // _isVNode 是 VNode 对象
  _isVNode: true
  // el VNode 对应的真实 DOM
  el: Element | null
  shapeFlag: ShapeFlags.ELEMENT,
  tag: | string | Component | null,
  props: VNodeProps | null,
  children: string | Array<VNode>
}

实际上,Vue 3 中对 VNode 的定义要复杂的多,这里就不去细看了。

消化一下,下回继续~

下一课 - 生成 VNode 的 h 函数