VNode:虚拟DOM
大概结构如下:
export interface VNode {
_isVNode: true
el: Element | null
flags: VNodeFlags
tag: string | FunctionalComponent | ComponentClass | null
data: VNodeData | null
children: VNodeChildren
childFlags: ChildrenFlags
}
demo
一个简单的例子:
const elementVnode = {
tag: 'div'
}
// 把 elementVnode 渲染到 id 为 app 的元素下
render(elementVnode, document.getElementById('app'))
function render(vnode, container) {
mountElement(vnode, container)
}
function mountElement(vnode, container) {
// 创建元素
const el = document.createElement(vnode.tag)
// 将元素添加到容器
container.appendChild(el)
}
组件用VNode表示:
// MyComponent 组件
class MyComponent {
render() {
// render 函数产出 VNode
return {
tag: 'div'
}
}
}
// VNode
const componentVnode = {
tag: MyComponent
}
// 渲染
render(componentVnode, document.getElementById('app'))
function render(vnode, container) {
if (typeof vnode.tag === 'string') {
// html 标签
mountElement(vnode, container)
} else {
// 组件
mountComponent(vnode, container)
}
}
function mountComponent(vnode, container) {
// 创建组件实例
const instance = new vnode.tag()
// 渲染
instance.$vnode = instance.render()
// 挂载
mountElement(instance.$vnode, container)
}
function mountElement(vnode, container) {
// 创建元素
const el = document.createElement(vnode.tag)
// 将元素添加到容器
container.appendChild(el)
}
用VNode描述抽象内容
<div>
<MyComponent />
</div>
const elementVNode = {
tag: 'div',
data: null,
children: {
tag: MyComponent,
data: null
}
}
可以通过检查 tag 属性值是否是字符串来确定一个 VNode 是否是普通标签。
tag
特殊的tag
- Fragment:抽象的,不是真实存在的
```javascript
const fragmentVNode = { // tag 属性值是一个唯一标识 tag: Fragment, data: null, children: [ { tag: ‘td’, data: null }, { tag: ‘td’, data: null }, { tag: ‘td’, data: null } ] }
- Portal
```javascript
<template>
<Portal target="#app-root">
<div class="overlay"></div>
</Portal>
</template>
//无论你在何处使用该组件,它都会把内容渲染到 id="app-root" 的元素下
VNode的种类:
flag
在 VNode 创建的时候就把该 VNode 的类型通过 flags 标明,这样在挂载或 patch 阶段通过 flags 可以直接避免掉很多消耗性能的判断,我们先提前感受一下渲染器的代码:
if (flags & VNodeFlags.ELEMENT) {
// VNode 是普通标签
mountElement(/* ... */)
} else if (flags & VNodeFlags.COMPONENT) {
// VNode 是组件
mountComponent(/* ... */)
} else if (flags & VNodeFlags.TEXT) {
// VNode 是纯文本
mountText(/* ... */)
}
const VNodeFlags = {
// html 标签
ELEMENT_HTML: 1,
// SVG 标签
ELEMENT_SVG: 1 << 1,
// 普通有状态组件
COMPONENT_STATEFUL_NORMAL: 1 << 2,
// 需要被keepAlive的有状态组件
COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE: 1 << 3,
// 已经被keepAlive的有状态组件
COMPONENT_STATEFUL_KEPT_ALIVE: 1 << 4,
// 函数式组件
COMPONENT_FUNCTIONAL: 1 << 5,
// 纯文本
TEXT: 1 << 6,
// Fragment
FRAGMENT: 1 << 7,
// Portal
PORTAL: 1 << 8
}
// html 和 svg 都是标签元素,可以用 ELEMENT 表示
VNodeFlags.ELEMENT = VNodeFlags.ELEMENT_HTML | VNodeFlags.ELEMENT_SVG
// 普通有状态组件、需要被keepAlive的有状态组件、已经被keepAlice的有状态组件 都是“有状态组件”,统一用 COMPONENT_STATEFUL 表示
VNodeFlags.COMPONENT_STATEFUL = VNodeFlags.COMPONENT_STATEFUL_NORMAL
| VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE
| VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE
// 有状态组件 和 函数式组件都是“组件”,用 COMPONENT 表示
VNodeFlags.COMPONENT = VNodeFlags.COMPONENT_STATEFUL | VNodeFlags.COMPONENT_FUNCTIONAL
VNode可以加上flag表示:
// html 元素节点
const htmlVnode = {
flags: VNodeFlags.ELEMENT_HTML,
tag: 'div',
data: null
}
// 函数式组件
const functionalComponentVnode = {
flags: VNodeFlags.COMPONENT_FUNCTIONAL,
tag: MyFunctionalComponent
}
VNodeFlags | 左移运算 | 32 位的 bit 序列(出于简略,只用 9 位表示) |
---|---|---|
ELEMENT_HTML | 无 | 000000001 |
ELEMENT_SVG | 1 << 1 | 000000010 |
COMPONENT_STATEFUL_NORMAL | 1 << 2 | 000000100 |
COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE | 1 << 3 | 000001000 |
COMPONENT_STATEFUL_KEPT_ALIVE | 1 << 4 | 000010000 |
COMPONENT_FUNCTIONAL | 1 << 5 | 000100000 |
TEXT | 1 << 6 | 001000000 |
FRAGMENT | 1 << 7 | 010000000 |
PORTAL | 1 << 8 | 100000000 |
根据上表展示的基本 flags 值可以很容易地得出下表:
VNodeFlags | 32 位的比特序列(出于简略,只用 9 位表示) |
---|---|
ELEMENT | 00000001 1 |
COMPONENT_STATEFUL | 00001 1 100 |
COMPONENT | 0001 1 1 100 |
一个标签的子节点会有以下几种情况:
- 没有子节点
- 只有一个子节点
- 多个子节点
- 有 key
- 无 key
不知道子节点的情况 ```javascript const ChildrenFlags = { // 未知的 children 类型 UNKNOWN_CHILDREN: 0, // 没有 children NO_CHILDREN: 1, // children 是单个 VNode SINGLE_VNODE: 1 << 1,
// children 是多个拥有 key 的 VNode KEYED_VNODES: 1 << 2, // children 是多个没有 key 的 VNode NONE_KEYED_VNODES: 1 << 3 }
//children是多个 VNode ChildrenFlags.MULTIPLE_VNODES = ChildrenFlags.KEYED_VNODES | ChildrenFlags.NONE_KEYED_VNODES
VNode表示:
```javascript
// 没有子节点的 div 标签
const elementVNode = {
flags: VNodeFlags.ELEMENT_HTML,
tag: 'div',
data: null,
children: null,
childFlags: ChildrenFlags.NO_CHILDREN
}
// 文本节点的 childFlags 始终都是 NO_CHILDREN
const textVNode = {
tag: null,
data: null,
children: '我是文本',
childFlags: ChildrenFlags.NO_CHILDREN
}
// 只有一个子节点的 Fragment
const elementVNode = {
flags: VNodeFlags.FRAGMENT,
tag: null,
data: null,
childFlags: ChildrenFlags.SINGLE_VNODE,
children: {
tag: 'p',
data: null
}
}
data
{
flags: VNodeFlags.ELEMENT_HTML,
tag: 'div',
data: {
class: ['class-a', 'active'],
style: {
background: 'red',
color: 'green'
},
// 其他数据...
}
}
<MyComponent @some-event="handler" prop-a="1" />
{
flags: VNodeFlags.COMPONENT_STATEFUL,
tag: 'div',
data: {
on: {
'some-event': handler
},
propA: '1'
// 其他数据...
}
}
那么当前的VNode对象为:
export interface VNode {
// _isVNode 属性在上文中没有提到,它是一个始终为 true 的值,有了它,我们就可以判断一个对象是否是 VNode 对象
_isVNode: true
// el 属性在上文中也没有提到,当一个 VNode 被渲染为真实 DOM 之后,el 属性的值会引用该真实DOM
el: Element | null
flags: VNodeFlags
tag: string | FunctionalComponent | ComponentClass | null
data: VNodeData | null
children: VNodeChildren
childFlags: ChildrenFlags
}