首先,我们需要用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是否是普通标签。
除此之外,还有两个抽象内容:Fragment
和Portal
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=box
的div
标签下,这就会导致蒙层的层级失效甚至布局都可能受到影响。
使用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
一个标签的子节点有哪些种类?
- 没有子节点
- 只有一个子节点
- 有多个子节点(有key,无key)
- 不知道子节点的情况
我们可以用一个叫做ChildrenFlag
来枚举以上这些情况,作为一个VNode的子节点的类型:
const ChildrenFlag = {
UNKNOW_CHILDREN: 0,
NO_CHILDREN: 1,
SINGLE_VNODE: 1 << 1,
KEYED_VNODE: 1 << 2,
NONE_KEYED_VNODE: 1 << 3
}
由于KEYED_VNODE
和NONE_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。
网友评论