虚拟DOM
一、什么是虚拟DOM
虚拟DOM(Virtual DOM)本质上是一个能描述DOM结构的 JavaScript 对象。
虚拟 DOM 是对真实DOM 的抽象,这个对象是更加轻量级的对 DOM 的描述。
在代码渲染到页面之前,vue 或者 rect 会把代码转换成一个对象(虚拟DOM)。以对象的形式来描述真实的 DOM 结构,最终渲染到页面,在每次数据发生变化前,虚拟 DOM 都会缓存一份,变化之时现在的虚拟 DOM 会和缓存的虚拟 DOM 进行比较。
当视图更新时,虚拟DOM会进行Diff比较,在内存中计算好要渲染的DOM,从而更新虚拟DOM树,然后再加载到真实DOM中。
另外,现代前端框架的一个基本要求就是无需手动操作 DOM ,一方面是因为手动操作 DOM 无法保证程序性能,多人协作的项目中如果 review 不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动操作 DOM 可以大大提高开发效率。
二、为什么要使用虚拟DOM
1.保证性能下限
页面渲染过程:解析 HTML => 生成 DOM => 生成 CSSOM => Layout(布局) => Paint(绘制) => Compiler
修改 DOM 时:
真实 DOM :生成 HTML 字符串 + 重建所有的 DOM 元素
虚拟 DOM :生成 Virtual Node + DOMDiff + 必要的 DOM 更新
优点:
- 更新DOM时,虚拟DOM在JS中的准备阶段上耗费更多的时间,相比于直接操作真实DOM它的代价是很低的。
- 首次渲染大量的 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。
- 在后续大量、频繁的DOM操作时,使用虚拟DOM性能会更快。
2.跨平台
虚拟DOM 本质上是 JS 对象,他可以很方便的跨平台操作。比如 Node.js 就没有 DOM ,如果想实现 SSR(服务端渲染),那么,一个方式就是借助虚拟 DOM ,因为虚拟 DOM 本身就是 JS 对象。
三、虚拟DOM的diff算法
当DOM更新操作发生时,虚拟DOM不会立即操作DOM,而是将操作中更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性地渲染到DOM树上,节省了真实DOM的操作。
vue采用的是深度优先,同层比较的策略。
平层Diff,只有以下4种情况:
- 节点类型改变。直接将旧节点卸载并装载新节点。旧节点包括下面的子节点都将被卸载,如果新节点和旧节点仅仅是类型不同,但下面的所有子节点都一样时,这样做效率不高。开发时应避免节点类型的变化。
- 节点类型不变,仅有属性或属性值改变。此时不会卸载节点,而是执行节点更新的操作。
- 文本内容改变。直接修改文字内容。
- 移动、增加、删除 子节点,根据key属性,找到具体位置,再执行卸载、加载操作。
四、vue中如何实现虚拟DOM
实现简易的创建虚拟DOM并渲染
<!-- 创建并插入以下html元素 -->
<div id="wrapper">
<span style="color: red">hello</span>
yjh
</div>
// index.js
import {h} from './h.js';
import {render} from './patch.js';
// 导出模块
export default {
mounted() {
// 创建虚拟节点
let vnode = h('div', {id: 'wrapper', a: 1, key: 'xx'},
h('span', {style: {color: 'red'}}, 'hello'),
'yjh');
// 将虚拟节点渲染至页面
let yyy = document.getElementById('yyy')
render(vnode, yyy);
}
};
h函数,创建虚拟节点
/**
* 创建虚拟节点
* @param type 标签
* @param props 属性
* @param children 子节点
* @return {{children: *, text: *, type: *, key: *, props: *}}
*/
function h(type, props, ...children) {
let key;
if (props.key) {
key = props.key;
delete props.key;
}
// 将不是虚拟节点的子节点 变成虚拟节点
children = children.map(child => {
if (typeof child === 'string') {
return vnode(undefined, undefined, undefined, undefined, child);
} else {
return child;
}
});
return vnode(type, key, props, children);
}
function vnode(type, key, props, children, text) {
return {
type, key, props, children, text
};
}
export {h};
patch算法,将虚拟节点渲染到页面的容器中
/**
* 将虚拟节点渲染到页面的容器中
* @param vnode 虚拟节点
* @param container 要渲染到的容器
*/
function render(vnode, container) {
// 将虚拟节点转化成真实节点
let ele = createDomElementFromVnode(vnode);
console.log(ele)
container.appendChild(ele);
}
function createDomElementFromVnode(vnode) {
let {type, key, props, children, text} = vnode;
if (type) {
// 标签
vnode.domElement = document.createElement(type);
// 根据虚拟节点属性,去更新真实DOM
updateProperties(vnode);
// children中放的也是虚拟节点,递归渲染
children.forEach(childVnode => render(childVnode, vnode.domElement))
} else {
// 文本
vnode.domElement = document.createTextNode(text);
}
return vnode.domElement;
}
function updateProperties(newVnode, oldProps={}) {
// 真实的DOM元素
let domElemnt = newVnode.domElement;
// 当前虚拟节点中的属性
let newProps = newVnode.props;
// 移除属性
for (let oldPropName in oldProps) {
if (!newProps[oldPropName]) {
delete domElemnt[oldPropName];
}
}
// 新节点属性直接覆盖
for (let newPropsName in newProps) {
if (newPropsName === 'style') {
let styleObj = newProps.style;
for (let s in styleObj) {
domElemnt.style[s] = styleObj[s];
}
} else {
domElemnt[newPropsName] = newProps[newPropsName];
}
}
}
export {render};