虚拟DOM是现代化前端开发框架基本必备的功能,所以到底什么是虚拟DOM,为什么我们要使用虚拟DOM呢?
下面我们将从VUE创建应用实例APP的vnode来学习vue中的虚拟DOM。
二.vue中的虚拟dom--vnode
1. 什么是虚拟DOM
Vue 中的虚拟 DOM(Virtual DOM)是 Vue 框架的一个核心概念。虚拟 DOM 是一个轻量级的、内存中的 DOM 表示形式,它可以帮助 Vue 快速地更新和渲染 DOM。
为什么需要虚拟 DOM?
在传统的 DOM 操作中,每次更新 DOM 都需要手动操作 DOM 元素,这会导致浏览器重新渲染整个页面,这个过程非常耗时。尤其是在大型应用中,这种情况会变得更加严重。
虚拟 DOM 解决了这个问题。它在内存中维护一个 DOM 树的副本,每次更新时,只需要更新虚拟 DOM 树,然后再将虚拟 DOM 树与真实 DOM 树进行对比,找出需要更新的部分,然后才进行真实 DOM 的更新。
实际上虚拟Dom即为用对象的形式来模拟真实的Dom的结构,在更新时只需首先更新虚拟Dom树,之后与真实Dom树对比再进行真实Dom树结构的更新 -- 减少真实Dom操作
2. vue中的虚拟Dom组成
控制台输出的虚拟Dom对象。
- VNode:虚拟 DOM 树中的一个节点,每个 VNode 都包含了当前节点的信息,如标签名、属性、子节点等。
- createVNode:创建一个新的 VNode 实例。
- patch:比较两个 VNode 实例,找出需要更新的部分,然后更新真实 DOM。
- render:将虚拟 DOM 树渲染成真实 DOM 树。
3. createVnode
回到在上一节中,我们通过createApp创建了应用实例app,之后我们使用app上的mount方法进行挂载。
javascript
const app = {
_component: rootComponent,
mount(rootContainer) {
const vnode = createVNode(rootComponent);
render(vnode, rootContainer);
},
};
javascript
const createVNode = function (type, props, children) {
const vnode = {
el: null,
component: null,
key: props === null || props === void 0 ? void 0 : props.key,
type,
props: props || {},
children,
shapeFlag: getShapeFlag(type),
};
if (Array.isArray(children)) {
vnode.shapeFlag |= 16;
}
else if (typeof children === "string") {
vnode.shapeFlag |= 8;
}
normalizeChildren(vnode, children);
return vnode;
};
3.1 getShapeFlag
判断传入的type是否为字符串,是则为元素节点否则为组件节点。显然我们传入的type为rootComponent --> App为对象,所以这里代表我们创建的是有状态组件节点。
javascript
function getShapeFlag(type) {
return typeof type === "string"
? 1
: 4;
}
3.1.1 shapeFlag是什么?
shapeFlag 是一个数字值,它用来描述一个 VNode 的类型和特征。在 Vue 中,每个 VNode 都有一个唯一的 shapeFlag 值,这个值可以用来快速判断 VNode 的类型和特征。
为什么需要shapeFlag?
shapeFlag 值可以用来快速判断 VNode 的类型和特征,这可以帮助 Vue 在渲染和更新过程中做出更好的优化和决策。例如,在渲染过程中,Vue 可以根据 shapeFlag 值来决定是否需要创建一个新的 DOM 元素或更新一个现有的 DOM 元素。
vue中的shapeFlag代表的节点
- 0x1(1):
ELEMENT
- 表示 vnode 是一个普通的 HTML 元素 - 0x2(2):
FUNCTIONAL_COMPONENT
- 表示 vnode 是一个函数式组件 - 0x4(4):
STATEFUL_COMPONENT
- 表示 vnode 是一个有状态组件 - 0x8(8):
TEXT_CHILDREN
- 表示 vnode 的子节点是文本类型 - 0x10(16):
ARRAY_CHILDREN
- 表示 vnode 的子节点是数组类型 - 0x20(32):
SLOTS_CHILDREN
- 表示 vnode 的子节点是插槽类型 - 0x40(64):
TELEPORT
- 表示 vnode 是一个 teleport 组件 - 0x80(128):
SUSPENSE
- 表示 vnode 是一个 suspense 组件 - 0x100(256):
COMPONENT_SHOULD_KEEP_ALIVE
- 表示 vnode 是一个需要被 keep-live 的有状态组件 - 0x200(512):
COMPONENT_KEPT_ALIVE
- 表示 vnode 是一个已经被 keep-live 的有状态组件
3.1.2 按位或
按位或(bitwise OR)操作是指将两个二进制数进行逐位或运算。也就是说,对于每个对应的位,如果任意一个数的该位为 1,则结果的该位为 1。
在这里,我们使用按位或操作来组合 shapeFlag
的值。这是因为每个值代表一个特定的特征,我们可以通过组合这些值来描述一个 VNode 的多个特征。
让我们以 1 | 8
为例:
1
代表元素节点(Element)8
代表vnode 的子节点是文本类型(TEXT_CHILDREN)
当我们进行按位或操作时,我们得到:
yaml
0001
| 1000
------
1001 //9
结果是 9
。这个值同时代表元素节点和子节点是文本类型的节点。
这样,我们就可以使用一个单独的值来描述一个 VNode 的多个特征。在 Vue 中,这个值可以被用来优化渲染和更新过程。
3.2 normalizeChildren
判断vnode的子组件类型
如果 children
是一个对象,并且 vnode
不是一个普通的 HTML 元素(即不是 ELEMENT
),则表示 vnode
的子节点是插槽类型的。
因此,设置 vnode.shapeFlag
中的 SLOTS_CHILDREN
标志(32)是为了标识 vnode
的子节点是插槽类型的。
javascript
function normalizeChildren(vnode, children) {
if (typeof children === "object") {
if (vnode.shapeFlag & 1) ;
else {
vnode.shapeFlag |= 32;
}
}
}
3.3 vnode
通过createVnode创建了vnode节点,而我们创建app组件的shapeFlag很明显为4,代表为组件节点,同时这里的type属性为调用函数传入的rootComponent即为App组件。
javascript
const vnode = {
el: null,
component: null,
key: props === null || props === void 0 ? void 0 : props.key,
type, //rootComponent
props: props || {},
children,
shapeFlag: getShapeFlag(type)
}
下图为输出的vnode对象,注意这里除了component和el均与目前创建的虚拟节点相同。
4. render
这里调用的render实际为虚拟dom组成成分中的patch而不是真实的render
render(vnode, rootContainer);
调用的两个参数分别为刚刚使用createVnode创建的vnode和调用mount时传入的root真实dom节点。
javascript
const render = (vnode, container) => {
console.log("调用 patch");
patch(null, vnode, container);
};
4.1 patch
注意我们调用patch时的参数,n1为null,n2为创建的app的vnode,container为root真实dom。
解构出的type为rootComponent,shapeFlag为4,所以执行了处理component的逻辑,进入了processComponent函数。
javascript
function patch(n1, n2, container = null, anchor = null, parentComponent = null) {
const { type, shapeFlag } = n2; // type为rootComponent,shapeFlag为4
console.log('patch',n2)
switch (type) {
case Text:
processText(n1, n2, container);
break;
case Fragment:
processFragment(n1, n2, container);
break;
default:
if (shapeFlag & 1) {
console.log("处理 element");
processElement(n1, n2, container, anchor, parentComponent);
}
else if (shapeFlag & 4) {
console.log("处理 component");
processComponent(n1, n2, container, parentComponent);
}
}
}
4.2 processComponent
判断是挂载组件还是更新组件,n1为null走mountComponent流程。
javascript
function processComponent(n1, n2, container, parentComponent) {
//n1 == null
if (!n1) {
mountComponent(n2, container, parentComponent);
}
else {
updateComponent(n1, n2);
}
}