什么是虚拟dom
他是一个轻量级的JavaScript 框架 用来描述真实dom的层次结构
虚拟DOM是将状态映射成视图的众多解决方案中的一种,它的运作原理是使用状态生成虚拟节点,然后使用虚拟节点渲染视图。
之所以需要先使用状态生成虚拟节点,是因为如果直接用状态生成真实DOM,会有一定程度的性能浪费。而先创建虚拟节点再渲染视图,就可以将虚拟节点缓存,然后使用新创建的虚拟节点和上一次渲染时缓存的虚拟节点进行对比,然后根据对比结果只更新需要更新的真实DOM节点,从而避免不必要的DOM操作,节省一定的性能开销。
由于Vue.js的变化侦测粒度更细,所以当状态发生变化时,Vue.js知道的信息更多,一定程度上可以知道哪些位置使用了状态。因此,Vue.js可以通过细粒度的绑定来更新视图,Vue.js 1.0就是这样实现的。
但是这样做也有一定的代价。因为粒度太细,就会有很多watcher同时观察某些状态,会有一些内存开销以及一些依赖追踪的开销,所以Vue.js 2.0采取了一个中等粒度的解决方案,状态侦测不再细化到某个具体节点,而是某个组件,组件内部通过虚拟DOM来渲染视图,这可以大大缩减依赖数量和watcher数量。
因此,虚拟DOM在Vue.js中所做的事是提供虚拟节点vnode和对新旧两个vnode进行比对,并根据比对结果进行DOM操作来更新视图。
VNode
kotlin
/**
* @internal
* 虚拟节点类,Vue 虚拟DOM系统的核心类,用于描述DOM元素、组件或文本节点
*/
export default class VNode {
// 标签名,如 'div', 'span' 等,组件则为组件名
tag?: string
// 节点数据,包含属性、指令、事件等
data: VNodeData | undefined
// 子节点数组
children?: Array<VNode> | null
// 文本节点的内容
text?: string
// 真实DOM元素引用
elm: Node | undefined
// 命名空间,用于SVG等特殊元素
ns?: string
// 组件作用域上下文,该节点在哪个组件的作用域下渲染
context?: Component // rendered in this component's scope
// 节点的唯一标识,用于优化更新性能
key: string | number | undefined
// 组件选项,包含组件的各种配置
componentOptions?: VNodeComponentOptions
// 组件实例引用
componentInstance?: Component // component instance
// 父虚拟节点引用,组件占位符节点
parent: VNode | undefined | null // component placeholder node
// strictly internal
// 是否包含原始HTML(仅服务端渲染使用)
raw: boolean // contains raw HTML? (server only)
// 是否为静态节点(已被提升)
isStatic: boolean // hoisted static node
// 是否在根节点插入,用于进入过渡检查
isRootInsert: boolean // necessary for enter transition check
// 是否为空注释占位符
isComment: boolean // empty comment placeholder?
// 是否为克隆节点
isCloned: boolean // is a cloned node?
// 是否为v-once节点(只渲染一次)
isOnce: boolean // is a v-once node?
// 异步组件工厂函数
asyncFactory?: Function // async component factory function
// 异步组件元数据
asyncMeta: Object | void
// 是否为异步组件占位符
isAsyncPlaceholder: boolean
// 服务端渲染上下文
ssrContext?: Object | void
// 函数式组件的真实上下文vm
fnContext: Component | void // real context vm for functional nodes
// 用于服务端渲染缓存的函数式组件选项
fnOptions?: ComponentOptions | null // for SSR caching
// 用于存储函数式组件渲染上下文的开发工具元数据
devtoolsMeta?: Object | null // used to store functional render context for devtools
// 函数式作用域ID支持
fnScopeId?: string | null // functional scope id support
// 是否为组件根元素(用于服务端渲染指令)
isComponentRootElement?: boolean | null // for SSR directives
/**
* 构造函数,创建一个新的VNode实例
* @param tag 标签名
* @param data 节点数据
* @param children 子节点
* @param text 文本内容
* @param elm 真实DOM元素
* @param context 组件上下文
* @param componentOptions 组件选项
* @param asyncFactory 异步组件工厂函数
*/
constructor(
tag?: string,
data?: VNodeData,
children?: Array<VNode> | null,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag // 设置标签名
this.data = data // 设置节点数据
this.children = children // 设置子节点
this.text = text // 设置文本内容
this.elm = elm // 设置真实DOM元素引用
this.ns = undefined // 初始化命名空间为undefined
this.context = context // 设置组件上下文
this.fnContext = undefined // 初始化函数式组件上下文为undefined
this.fnOptions = undefined // 初始化函数式组件选项为undefined
this.fnScopeId = undefined // 初始化函数式作用域ID为undefined
this.key = data && data.key // 从节点数据中获取key
this.componentOptions = componentOptions // 设置组件选项
this.componentInstance = undefined // 初始化组件实例为undefined
this.parent = undefined // 初始化父节点为undefined
this.raw = false // 初始化raw为false
this.isStatic = false // 初始化isStatic为false
this.isRootInsert = true // 初始化isRootInsert为true
this.isComment = false // 初始化isComment为false
this.isCloned = false // 初始化isCloned为false
this.isOnce = false // 初始化isOnce为false
this.asyncFactory = asyncFactory // 设置异步组件工厂函数
this.asyncMeta = undefined // 初始化异步组件元数据为undefined
this.isAsyncPlaceholder = false // 初始化isAsyncPlaceholder为false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
/**
* 已废弃:componentInstance的别名,为了向后兼容
*/
get child(): Component | void {
return this.componentInstance
}
}
nodeData 数据结构
css
/**
* @internal
* VNode 数据接口,包含虚拟节点的所有相关属性和配置
*/
export interface VNodeData {
/**
* 节点的唯一标识符,用于 Vue 的 diff 算法,帮助识别相同节点以优化渲染性能
*/
key?: string | number
/**
* 插槽名称,用于标识组件的内容分发位置
*/
slot?: string
/**
* 引用标识符或函数,用于在组件实例中访问对应的 DOM 元素或子组件实例
* 可以是字符串、Ref 对象或接收元素作为参数的回调函数
*/
ref?: string | Ref | ((el: any) => void)
/**
* 用于动态组件的标签名,指定要渲染的实际组件
*/
is?: string
/**
* 是否跳过编译(Vue 1.x 遗留特性),标记内容为原始 HTML
*/
pre?: boolean
/**
* 元素标签名
*/
tag?: string
/**
* 静态 CSS 类名,不会随数据变化的类
*/
staticClass?: string
/**
* 动态 CSS 类名,可以是字符串、对象或数组
*/
class?: any
/**
* 静态样式对象,不会随数据变化的样式
*/
staticStyle?: { [key: string]: any }
/**
* 动态样式,可以是字符串、样式对象数组或样式对象
*/
style?: string | Array<Object> | Object
/**
* 标准化后的样式对象,在内部渲染过程中使用
*/
normalizedStyle?: Object
/**
* 组件的属性(props),会被传递给子组件
*/
props?: { [key: string]: any }
/**
* HTML 属性,会被设置到 DOM 元素上
*/
attrs?: { [key: string]: string }
/**
* DOM 属性,直接设置到 DOM 元素的属性上
*/
domProps?: { [key: string]: any }
/**
* 生命周期钩子函数集合,在 VNode 不同阶段被调用
*/
hook?: { [key: string]: Function }
/**
* 事件监听器,由 Vue 事件系统处理
*/
on?: { [key: string]: Function | Array<Function> }
/**
* 原生 DOM 事件监听器,直接绑定到 DOM 元素上
*/
nativeOn?: { [key: string]: Function | Array<Function> }
/**
* 过渡动画配置,用于元素进入/离开时的动画效果
*/
transition?: Object
/**
* v-show 指令的标记,控制元素的显示/隐藏(通过 CSS display 属性实现)
*/
show?: boolean // marker for v-show
/**
* 内联模板配置,包含渲染函数和静态渲染函数数组
*/
inlineTemplate?: {
render: Function
staticRenderFns: Array<Function>
}
/**
* 指令数组,包含应用在节点上的所有指令
*/
directives?: Array<VNodeDirective>
/**
* 是否使用 keep-alive 缓存该组件实例
*/
keepAlive?: boolean
/**
* 作用域插槽函数集合,每个函数返回一个 VNode
*/
scopedSlots?: { [key: string]: Function }
/**
* v-model 指令的配置,包含绑定的值和更新回调
*/
model?: {
value: any
callback: Function
}
/**
* 索引签名,允许 VNodeData 包含任何额外的属性
*/
[key: string]: any
}```
VNodeData 接口是 Vue 虚拟DOM系统中的核心数据结构,它定义了一个虚拟节点可以包含的所有属性和配置。这些属性涵盖了从基本的HTML标签信息(如tag、class、style)到Vue特有的功能(如指令、组件属性、事件处理等)。
这个接口的设计非常灵活,通过可选属性和索引签名,允许根据不同场景包含不同的属性组合。Vue的渲染系统会根据这些数据来创建和更新实际的DOM元素。
总结:
VNode是一个类,可以生成不同类型的vnode实例,而不同类型的vnode表示不同类型的真实DOM元素。
由于Vue.js对组件采用了虚拟DOM来更新视图,当属性发生变化时,整个组件都要进行重新渲染的操作,但组件内并不是所有DOM节点都需要更新,所以将vnode缓存并将当前新生成的vnode和上一次缓存的oldVnode进行对比,只对需要更新的部分进行DOM操作可以提升很多性能。
vnode有多种类型,它们本质上都是从VNode类实例化出的对象,其唯一区别只是属性不同。
patch
虚拟DOM最核心的部分是patch,它可以将vnode渲染成真实的DOM。
patch也可以叫作patching算法,通过它渲染真实DOM时,并不是暴力覆盖原有DOM,而是比对新旧两个vnode之间有哪些不同,然后根据对比结果找出需要更新的节点进行更新。这一点从名字就可以看出,patch本身就有补丁、修补等意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。
主要是因为DOM操作的执行速度远不如JavaScript的运算速度快
对现有dom 修改需要做3件事
- 创建新增的节点
- 删除已经废弃的节点
- 修改需要更新的节点


创建一个节点并将其渲染到视图的全过程

新虚拟节点有文本属性 简单来说,就是当新虚拟节点有文本属性,并且和旧虚拟节点的文本属性不一样时,我们可以直接把视图中的真实DOM节点的内容改成新虚拟节点的文本。 更新节点的逻辑
