什么是虚拟Dom
虚拟 DOM 基于虚拟节点 VNode,VNode 本质上是一个对象,VDOM 就是VNode 组成的
废话,js 中所有的东西都是对象
虚拟DOM 为什么快,做了哪些优化
- 批量更新
- 多个DOM合并更新
- 减少浏览器的重排和重绘
- 局部更新
- 通过新VDOM对比,diff 算法
- 只更新需要重新渲染的真实 DOM
- 减少开销
- 跨平台支持
- node、浏览器、移动端、小程序都可以支持
虚拟DOM一定更快吗,不一定
- 额外的内存占用
- 虚拟DOM需要维护一个表示整个组件树的数据结构,这可能会占用额外的内存。
- VDOM 生成、对比,渲染都有额外的开销
- VDOM 适合中大型项目
- 简单的程序不适合VDOM,直接操作真实DOM更好
- 有些框架不用VDOM也很快
- 使用异步渲染技术
- requestAnimationFrame
- MutationObserver
- 使用异步渲染技术
实现VDOM
vue3 源码 runtime-core/src/vnode.ts 有关于 VNode 的定义
export interface VNode<
HostNode = RendererNode,
HostElement = RendererElement,
ExtraProps = { [key: string]: any },
> {
/**
* @internal
*/
__v_isVNode: true
/**
* @internal
*/
[ReactiveFlags.SKIP]: true
type: VNodeTypes
props: (VNodeProps & ExtraProps) | null
key: string | number | symbol | null
ref: VNodeNormalizedRef | null
/**
* SFC only. This is assigned on vnode creation using currentScopeId
* which is set alongside currentRenderingInstance.
*/
scopeId: string | null
/**
* SFC only. This is assigned to:
* - Slot fragment vnodes with :slotted SFC styles.
* - Component vnodes (during patch/hydration) so that its root node can
* inherit the component's slotScopeIds
* @internal
*/
slotScopeIds: string[] | null
children: VNodeNormalizedChildren
component: ComponentInternalInstance | null
dirs: DirectiveBinding[] | null
transition: TransitionHooks<HostElement> | null
// DOM
el: HostNode | null
anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target
targetAnchor: HostNode | null // teleport target anchor
/**
* number of elements contained in a static vnode
* @internal
*/
staticCount: number
// suspense
suspense: SuspenseBoundary | null
/**
* @internal
*/
ssContent: VNode | null
/**
* @internal
*/
ssFallback: VNode | null
// optimization only
shapeFlag: number
patchFlag: number
/**
* @internal
*/
dynamicProps: string[] | null
/**
* @internal
*/
dynamicChildren: VNode[] | null
// application root node only
appContext: AppContext | null
/**
* @internal lexical scope owner instance
*/
ctx: ComponentInternalInstance | null
/**
* @internal attached by v-memo
*/
memo?: any[]
/**
* @internal __COMPAT__ only
*/
isCompatRoot?: true
/**
* @internal custom element interception hook
*/
ce?: (instance: ComponentInternalInstance) => void
}
使用 createVNode 创建虚拟节点
虚拟 dom 就是虚拟 node 节点的结合,每个 Vnode 都有一个 children 属性,children 的每个元素也是一个 VNode,他们有共同的根节点,就形成了一个虚拟的树结构。
自己实现虚拟 DOM 的重点步骤
- 定义一个 VNode 数据结构【这个如果是用 js,没有接口类型定义,就不用在代码中直接体现】
- 类中有 children 属性【用来存储子节点】
- 有 tag 代表标签【用来存储真实 html 的标签, div, p, span 等】
- 有 props 节点的属性【用来存储 html 元素的各种属性,style, class 等】
- 定义一个创建 VNode 的函数或类【createVNode】
- 定义一个渲染函数,将 VDOM 转成真实节点【render】
下面是我根据上面的步骤自己实现的:
// 创建虚拟节点函数
function createVNode(tag, props, children) {
// 虚拟节点必须包含的三个属性
return {
isVnode: true, // 用来判断是否是虚拟节点,也不可不用这个
tag,
props,
children // 数组
}
}
function render(VNode) {
const { tag, props, children } = VNode
// 创建真实 Dom 元素
const element = document.createElement(tag)
// 给 dom 元素增加属性
for(let key in props) {
element.setAttribute(key, props[key])
}
for(let i =0; i < children.length; i ++) {
let child = children[i]
// 如果子节点还是虚拟节点,就递归调用渲染函数
if (child.isVnode ) {
element.appendChild(render(child))
} else {
// 最终的真实节点
element.appendChild(document.createTextNode(child))
}
}
return element
}
const vDom = createVNode('div', {style: 'color:red'}, [
createVNode('h1', { style: 'color:blue'}, ['你好']),
createVNode('p', {}, ['再见'])
])
const realDom = render(vDom)
console.log(realDom)
复制上面代码到浏览器开发者工具中可以直接运行
下面 chatgpt 给的答案,使用了一个 vnode类,看起来更好一些:
// 定义虚拟DOM节点的数据结构
class VNode {
constructor(tag, props, children) {
this.tag = tag;
this.props = props;
this.children = children;
}
// 渲染虚拟DOM为真实DOM
render() {
const element = document.createElement(this.tag);
// 设置属性
for (const key in this.props) {
element.setAttribute(key, this.props[key]);
}
// 渲染子节点
this.children.forEach(child => {
if (child instanceof VNode) {
element.appendChild(child.render());
} else {
element.appendChild(document.createTextNode(child));
}
});
return element;
}
}
// 创建虚拟DOM
const virtualDOM = new VNode('div', { class: 'container' }, [
new VNode('h1', {}, ['Hello, Virtual DOM!']),
new VNode('p', {}, ['This is a paragraph.']),
]);
// 将虚拟DOM渲染到页面中
const root = document.getElementById('root');
root.appendChild(virtualDOM.render());