简单的看一下vnode的设计

2021-04-19  本文已影响0人  HelenYin

首先,我们需要用vnode来描述一个真实DOM标签
一个真实DOM标签有:名称,属性,事件,样式,子节点等

const elementVnode = {
  tag: 'div',
  data: {
    style: {
      width: '100px',
      height: '100px',
      backgroundColor: 'red'
    }
  }
}

使用tag属性来存储标签名称,data属性用来存出标签的附加信息,这里我们把VNode对象的data属性,称为VNodeData。
为了描述子节点,我们需要给VNode对象添加children属性

const elementVNode = {
  tag: 'div',
  data: null,
  children: {
    tag: 'span',
    data: null,
  }
}

除了标签之外,DOM中还有文本节点

const textVNode = {
  tag: null,
  data: null,
  children: '文本内容'
}

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

用VNode来描述抽象内容

所谓抽象内容就是组件,例如:

<div>
  <MyComponent />
</div>

我们的意图并不是要在页面中渲染一个名为<MyComponent />的标签元素,而是要渲染<MyComponent />组件所产出的内容。
我们仍然要用VNode来描述`<MyComponent />,并且给此类组件的VNode添加一个标识,一遍在挂在的时候有办法区分一个VNode是普通标签还是组件

const elementVnode = {
  tag: 'div',
  data: null,
  children: {
    tag: MyComponent,
    data: null,
  }
}

这样我们可以通过tag属性是否是字符串来确定一个VNode是否是普通标签。

除此之外,还有两个抽象内容:FragmentPortal

Fragment

Fragment是指渲染一个片段
比如有如下模板:

<template>
  <table>
    <tr>
      <Colums />
    </tr>
  </table>
</template>

组件Colums会返回多个<td>

<template>
  <td></td>
  <td></td>
  <td></td>
</template>

假设模板中只有一个td标签,只有一个根元素,这很容易表示

const elementVNode = {
  tag: 'td',
  data: null
}

如果有多个根元素,我们就需要引入一个抽象元素Fragment

const Fragment = Symbol()
const fragmentVNode = {
  // tag属性值是一个唯一标识
  tag: Fragment,
  data: null,
  children: [
    { tag: 'td', data: null },
    { tag: 'td', data: null },
    { tag: 'td', data: null },
  ]
}

这样我们把所有td标签作为fragmantVnode的子节点,根元素并不是一个实实在在的真实DOM,而是一个抽象的标识,即Fragment
当渲染器在渲染VNode时候,如果发现该Vnode的类型是Fragment,就只需要把该Vnode的子节点渲染到页面上

Portal

它允许你把内容渲染到任何地方。
使用场景:你需要渲染一个蒙层<Overlay />,要求该组件的z-index的层级最高,无论在哪里使用都希望遮住全部内容,用户可能会将其用在任何你需要蒙层的地方。

<template>
  <div id="box" style="z-index: -1">
    <Overlay />
  </div>
</template>

如果没有Portal的情况下,上面的<Overlay>组件内容只能渲染到id=boxdiv标签下,这就会导致蒙层的层级失效甚至布局都可能受到影响。
使用Portal就可以这样来编写

<template>
  <Portal target="#app-root">
    <div class="overlay"></div>
  </Portal>
</template>

其最终的效果是,无论你在何处使用<Overlay />组件,他都会把内容渲染到id=app-root的元素下。由此可知,所谓Portal就是把子节点渲染到给定的目标,我们可以使用如下的VNode对象来描述

const Portal = Symbol()
const portalVNode = {
  tag: Portal,
  data: {
    target: '#app-root',
  },
  children: {
    tag: 'div',
    data: {
      class: 'overlay'
    }
  }
}

VNode的种类

当VNode描述不同的事物时,其属性的值也各不相同,比如一个VNode对象时html标签的描述,那么其tag属性就是一个字符串,即标签名;如果是组件的描述,那么tag属性值则引用组件类本身;如果是文本节点的描述,那么tag属性值为nul
最终我们发现,不同类型的VNode拥有不同的设计,这些差异积少成多,所以我们完全可以将他们分门别类。

我们可以把VNode分为五类:html/svg元素,组件,纯文本,Fragment,Portal

使用flags作为VNode的标识

既然VNode 有类表之分,我们就需要一个为一个的标识,来表明某一个VNode属于哪一类,给VNode添加flags,这是vnode算法的优化手段之一

if (flags & VNodeFlag.ELEMENT) {
  mountElement(/*...*/)
} else if (flags & VNodeFlags.COMPONENT) {
  mountComponent/*...*/)
} else if (flags & VNodeFlags.TEXT) {
  mountText(/*...*/)
}

使用位运算,性能提升

// VNode对象
{
  flags: ...
}

枚举值VNodeFlags

每一个VNode种类我们都为其分配一个flags值,我们把它设计成一个枚举值,名称为VNodeFlags,在js中用一个对象来表示即可

const VNodeFlags = {
  // html标签
  ELEMENT_HTML: 1,  // 0000 0000 0000 0001
  // SVG
  ELEMENT_SVG: 1 << 1,  // 0000 0000 0000 0010
  // 普通有状态的组件
  COMPONENT_STATEFUL_NORMAL: 1 << 2,  // 0000 0000 0000 0100
  // 需要被keepAlive的有状态的组件
  COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE: 1 << 3,  // 0000 0000 0000 1000
  // 已经被keepAlive的有状态组件
  COMPONENT_STATEFUL_KEPT_ALIVE: 1 << 4,  // 0000 0000 0001 0000
  // 函数式组件
  COMPONENT_FUNCTIONAL: 1 << 5,  // 0000 0000 0010 0000
  // 纯文本
  TEXT: 1 << 6,  // 0000 0000 0100 0000
  // Fragment
  FRAGMENT: 1 << 7,  // 0000 0000 1000 0000
  // Portal
  PORTAL: 1 << 8,  // 0000 0001 0000 0000
}

可以派生出来额外的三个标识:
html和svg

// 0000 0000 0000 0011
VNodeFlag.ELEMENT = VNodeFlag. ELEMENT_HTML | VNodeFlag. ELEMENT_SVG 

普通有状态组件,需要被keepAlive的有状态组件、已经被keepAlice的有状态组件 都是“有状态组件”,统一用 COMPONENT_STATEFUL 表示

// 0000 0000 0001 1100
VNodeFlags.COMPONENT_STATEFUL =
  VNodeFlags.COMPONENT_STATEFUL_NORMAL |
  VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE |
  VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE

有状态的组件和函数式组件都是组件,用COMPONENT表示

// 0000 0000 0011 1100
VNodeFlags.COMPONENT = VNodeFlags.COMPONENT_STATEFUL | VNodeFlags.COMPONENT_FUNCTIONAL

这样我们在创建vnode的时候就可以预先为其加上flags,表明VNode的类型

// html 元素节点
const htmlVnode = {
  flags: VNodeFlags.Element_HTML,
  tag: 'div',
  data: null,
}
// svg 元素节点
const svgVnode = {
  flags: VnodeFlags.ELEMENT_SVG,
  tag: 'svg',
  data: null
}
// 函数式组件
const functionalComponentVnode = {
  flags: VNodeFlags. COMPONENT_FUNCTIONAL
}
// 普通有状态组件
const normalComponentVnode = {
  flags: VNodeFlags.COMPONENT_STATEFUL_NORMAL,
  tag: MyStatefulComponent
}
// Fragment
const fragmentVnode = {
  flags: VNodeFlags.FRAGMENT,
  // 由于有flags的存在,我们已经不需要使用tag属性来存储唯一标识
  tag: null
}
// Portal
const portalVnode = {
  flags: VNodeFlags. PORTAL,
  tag: target
}

如下是利用VNodeFlags判断vnode类型的例子,

functionalComponentVnode.flags & VNodeFlags.COMPONENT  // 真 (非0)
normalComponentVnode.flags & VNodeFlags.COMPONENT // 真(非0)
htmlVnode.flags & VNodeFlags.COMPONENT // 假(为0)

children 和 childrenFlag

一个标签的子节点有哪些种类?

我们可以用一个叫做ChildrenFlag来枚举以上这些情况,作为一个VNode的子节点的类型:

const ChildrenFlag = {
  UNKNOW_CHILDREN: 0,
  NO_CHILDREN: 1,
  SINGLE_VNODE: 1 << 1,
  KEYED_VNODE: 1 << 2,
  NONE_KEYED_VNODE: 1 << 3
}

由于KEYED_VNODENONE_KEYED_VNODE都属于多个children,所以我们可以派生出来一个多children的类型,以方便程序判断

ChildrenFlags. MULTIPLE_VNODES = ChildrenFlag. KEYED_VNODE | ChildrenFlag. NONE_KEYED_VNODE;

这样我们判断一个VNode的子节点是否是多个子节点

someVnode.childrenFlag & ChildrenFlag.MULTIPLE_VNODES;

在一个VNode对象中,我们使用flags来存储vnode的类型,用childrenFlags来存储子节点的类型。

// 没有子节点的div标签
const elementVNode = {
  flags: VNodeFlags.ELEMENT_HTML,
  tag: 'div',
  data: null,
  children: null,
  childFlags: ChildrenFlags.NO_CHILDREN
}
// 文本节点的 childrenFlags 始终都是 NO_CHILDREN
const textVNode = {
  tag: null,
  data: null,
  children: '我是文本',
  childFlags: ChildrenFlags.NO_CHILDREN
}
// 拥有多个使用了key的 li 标签作为子节点的 ul 标签
const elementVNode = {
  flags: VNodeFlags.ELEMENT_HTML,
  tag: 'ul',
  data: null,
  childFlags: ChildrenFlags.KEYED_VNODES,
  children: [
    {
      tag: 'li',
      data: null,
      key: 0
    },
    {
      tag: 'li',
      data: null,
      key: 1
    }
  ]
}
// 只有一个子节点的 Fragment
const elementVNode = {
  flags: VNodeFlags.FRAGMENT,
  tag: null,
  data: null,
  childFlags: ChildrenFlags.SINGLE_VNODE,
  children: {
    tag: 'p',
    data: null
  }
}

VNodeData

VNodeData是对VNode进行描述的数据,任何对VNode进行描述的数据都应该放在data里。

{
  flags: VNodeFlags.ELEMENT_HTML,
  tag: 'div',
  data: {
    class: ['class-a', 'active'],
    style: {
      background: 'red',
      color: 'green'
    },
    // 其他数据...
  }
}

当vnode是组件时

<MyComponent @some-event="handler" prop-a="1" />

{
  flags: VNodeFlags.COMPONENT_STATEFUL,
  tag: 'div',
  data: {
    on: {
      'some-event': handler
    },
    propA: '1'
    // 其他数据...
  }
}

到目前为止,还差一个el属性,el属性值在vnode被渲染为真实DOM之前一直都是null。当vnode被渲染为真实dom之后,el的值指向该真实DOM。

上一篇下一篇

猜你喜欢

热点阅读