大家应该都听说过一句非常有哲理的话:在get中收集依赖,在set中触发依赖 。。。这就是vue的响应式原理。相对于vue,React中的状态管理更加显式,通过useState Hook或类组件中 this.setState方法来通知React组件需要重新渲染。他们都借助了虚拟dom和diff算法来实现。
一、虚拟DOM是什么?为什么不直接操作dom来更新视图,而借助于虚拟dom?
•虚拟DOM (Virtual DOM) 就是JS对象,它模拟了真实的DOM结构。例如在Vue应用中,每当数据发生变化时,Vue并不会立即操作真实的DOM,而是首先更新虚拟DOM,最后再转换成真实DOM来更新页面。毕竟dom只是为了服务于页面,框架中的虚拟dom可以让开发者不用过度关注dom操作,并且它的本质是一种数据格式,有更好的跨端能力,扩展能力更强。
这个虚拟dom和diff算法饶了一圈,最后还是要操作真实的dom来更新视图。它的操作性能不一定比普通dom好,而是在多个平衡中的一个最优解。主要优点有:
1. 减少dom操作:通过数据驱动试图,开发者不必过多的关心dom操作,能更好的专注业务逻辑。虚拟DOM是在内存中构建一颗反映组件状态的轻量级JavaScript对象树(即虚拟DOM树),在虚拟DOM层面进行所有的修改和计算,而不是直接操作实际的DOM树。这样,Vue能够一次性计算出所有需要更新的地方,然后一次性地将这些变更同步到真实的DOM中,显著减少不必要的DOM操作,也就优化了性能。
2. 批量处理:Vue内部的数据绑定系统允许异步队列更新,当多个状态变化连续发生时,Vue会将这些变更积累起来,在下一个事件循环 tick 中一次性更新虚拟DOM并计算差异。这样就能确保只有最后一次的状态改变会影响DOM更新,而不是每次都立即触发更新。毕竟每次DOM变动后进行必要的回流(layout reflow)和重绘(repaint),这是一个相对昂贵的过程。
4. 跨平台能力:虚拟DOM是一种抽象层,使得像Vue这样的框架可以方便地移植到不同的环境,例如服务器端渲染、Web Workers或者其他非浏览器环境,因为它们不再直接依赖于浏览器提供的DOM API。
二、虚拟dom示例:
Vue中的模板会在运行时转化为渲染函数,生成虚拟DOM节点。
<template>
<div>
...
</div>
</template>
// 虚拟DOM节点示例
const vnode = {
// 标签名(元素类型)
tag: 'div',
// 属性对象
props: {
id: 'myDiv',
class: 'container'
},
// 组件的props(如果有)
componentProps: {...},
// 插槽内容(Slot)
slots: {
default: [...], // 子VNode数组或其他值
namedSlots: {
header: [...],
footer: [...]
}
},
// 子节点列表,可以是VNode对象的数组
children: [
{
tag: 'p',
props: { class: 'content' },
children: ['这是段落内容']
},
{
tag: 'button',
props: { type: 'button', onclick: handleClick },
children: ['点击我']
}
],
// 元素上的事件监听器
on: {
click: handleClick,
mouseover: handleMouseOver
},
// 其他Vue特有属性如ref、key等
key: 'uniqueKey',
ref: 'myRef',
patchFlag: ..., // Vue 3中用于标记节点更新类型的标志位
dynamicProps: [], // Vue 3中动态属性的列表
shapeFlag: ... // Vue 3中表示VNode形状的标志位
}
虚拟DOM(Virtual DOM)编译过程包括:模板解析、静态分析与优化、转换为渲染函数、创建VNode,挂载与更新:
1. 模板解析:
•Vue3 通常接收一个模板字符串或者setup()函数返回的渲染函数作为输入。
•模板字符串首先被Vue的编译器解析为抽象语法树(AST)。
2. 静态分析与优化:
•在编译阶段,Vue3 进行了一系列静态分析,包括静态属性/指令收集、静态根节点判断、静态提升等优化手段,这有助于减少运行时的开销。
•对于不变的静态内容,Vue3 会尽可能地将其提升到外部,以避免不必要的虚拟DOM diff。
3. 转换为渲染函数:
•编译器会将AST转换为可执行的JavaScript函数,也就是渲染函数(render function)。
•渲染函数的作用是在运行时根据组件的状态动态生成VNode。
4. 创建VNode:
•当渲染函数被执行时,它会根据组件的数据模型和计算属性等生成对应的VNode对象。
•VNode是对DOM节点的一种轻量级的JavaScript对象表示,包含了元素类型、属性、子节点、插槽内容、指令等所有必要的信息。
5. 挂载与更新:
•当VNode树生成后,Vue3会使用Renderer(渲染器)将VNode树转换为真实的DOM并挂载到页面上。
•当组件状态变化时,Vue3会再次执行渲染函数生成新的VNode树,并通过高效的Diff算法对比新旧两棵VNode树的不同,只针对有差异的地方执行最小化的真实DOM操作。
三、虚拟dom的更新:Diff算法的作用与实现
Diff算法的主要任务就是在新旧两棵虚拟DOM树之间找到最小化DOM操作集。
Vue的Diff算法实现基于以下几个原则:
•同层节点比较:Vue会对同层级的虚拟DOM节点进行深度优先遍历,查找变更。
•基于键值(key)的优化:在循环渲染列表时,Vue利用:key属性作为唯一标识符,使得Diff算法能够准确判断元素是否移动或新增/删除,而不是简单粗暴地重新渲染整个列表。
•最小变更集合:Vue尽量保持最少的DOM变动次数,通过合理的算法策略降低更新开销。通过虚拟DOM与Diff算法的结合,Vue能够确保即使在复杂的应用场景下也能保证高性能的视图更新,同时简化了开发者的编程体验。
•两端向中间比较(Two-Ends Patching):Vue3的Diff算法采用了“双端比较”的策略,即从头尾同时开始比较新旧虚拟DOM树。这样做的目的是在遍历过程中尽快找到可以移动的节点,从而极大地提高了列表节点移动的效率。
Vue3对其Diff算法进行了大量的优化,例如使用动态编程求解最长递增子序列(LIS)以最小化DOM移动操作,并且在编译阶段引入了静态提升和块级作用域跟踪等策略来进一步提升性能。
简化版diff算法示例 用于理解:
// 假设我们有之前和现在的虚拟DOM节点列表
let oldVNodes = [a, b, c, d, e];
let newVNodes = [a, c, f, d, g];
// 简化版diff算法
function patch(oldVNodes, newVNodes) {
let moved = []; // 记录移动过的节点
let patches = []; // 存放需要更新DOM的操作
// 从两端开始比较,寻找可复用节点
for (let i = 0, j = 0; i < oldVNodes.length || j < newVNodes.length;) {
if (j >= newVNodes.length || (i < oldVNodes.length && sameVNodeType(oldVNodes[i], newVNodes[j]))) {
// 类型相同,比较并更新属性或内容
patchVNode(oldVNodes[i], newVNodes[j]);
i++;
j++;
} else if (i >= oldVNodes.length || sameVNodeType(oldVNodes[i], newVNodes[j])) {
// 类型不同或老节点找不到匹配的新节点,移除老节点
removeVNode(oldVNodes[i]);
i++;
} else {
// 新节点找不到匹配的老节点,插入新节点
insertVNode(newVNodes[j]);
moved.push(...findMovedVNodesInRange(oldVNodes.slice(i), oldVNodes.slice(0, i)));
j++;
}
}
// 处理移动节点
for (let move of moved) {
moveVNode(move.oldIndex, move.newIndex);
}
}
// 辅助函数示例
function sameVNodeType(vnode1, vnode2) {
// 检查节点类型、key等是否一致
}
function patchVNode(oldVNode, newVNode) {
// 更新属性、文本内容或子节点
}
function removeVNode(vNode) {
// 从DOM中移除对应的节点
}
function insertVNode(vNode) {
// 将虚拟节点渲染到DOM中
}
function findMovedVNodesInRange(range, beforeRange) {
// 查找指定范围内被移动的节点信息
}
function moveVNode(oldIndex, newIndex) {
// 在DOM中移动节点,以匹配新位置
}
总结:
1、虚拟DOM:
•是用JS对象模拟了真实DOM树的结构和状态。
•当应用状态改变时,框架不会直接操作浏览器的DOM,而是先在内存中创建新的虚拟DOM树。
•虚拟DOM树能够更快地构造和销毁,避免了直接操作DOM带来的昂贵性能开销。
2、Diff算法:
•Diff算法主要用于比较两个虚拟DOM树的差异。
•当组件状态改变导致新的虚拟DOM树被创建后,框架使用Diff算法找出老的和新的虚拟DOM树之间的最小差异集。
•Diff算法通常不是全量比较所有节点,而是采用了优化策略,比如同层比较、仅检查特定属性变化以及利用key属性来进行高效的节点增删和移动操作。
•结果是一个差异描述符(patch),它指出了哪些部分需要在真实DOM上进行更新。
3、两者的优势:
•虚拟DOM和Diff算法协同工作,确保只有必要的DOM改动会被反映到实际页面上,大大减少了不必要的DOM操作次数,它可以避免频繁重绘和回流,从而提升了网页应用程序的性能表现和用户体验。