虚拟DOM (virtual dom)
- vdom 是 vue 和 react 的核心,学习他们绕不开vdom
- vdom比较独立,使用也比较简单
- 如果面试问到了vue和react的实现,免不了问vdom
问题:
- 什么是vdom?为何会存在vdom?
- vdom如何应用,核心API是什么?
- 介绍一下diff算法
什么是vdom?为何会存在vdom
- virtual dom , 虚拟dom
- 用js模拟dom结构
- dom的变化对比,放在js层来做(图灵完备语言)
- 提高重绘性能
图灵完备的语言:简单来说就是能实现任何数学逻辑的语言。
对浏览器来说,dom操作比执行js更加耗费性能.所以使用vdom来模拟dom结构,再用diff算法来比较出dom的变化,最后只渲染变动的部分。要比把大块的dom树结构整个重新渲染,高效的多。所以vdom就有了实际价值和意义。
vdom之前,我们直接使用jQuery操作dom,但有很多问题:
- dom操作很昂贵,相比之下js的执行效率很高
- 所以应该尽量减少dom操作,而不是替换整块dom树
- 项目越复杂,影响越严重
- vdom就可以解决以上问题
vdom如何应用?核心API是什么?
我们说的vdom不是特指,是统称一类技术实现,有很多不同的库来实现。
- 介绍snabbdom(开源的vdom库,vue2.0用了)
- 核心api
<!--html结构-->
<ul id="list">
<li class="item">item1</li>
<li class="item">item2</li>
</ul>
// html对应的vdom结构
{
tag: 'ul',
attrs: { id: 'list' },
children: [
{
tag: 'li',
attrs: { className: 'item' },
children: ['item1']
},
{
tag: 'li',
attrs: { className: 'item' },
children: ['item2']
}
]
}
// 使用 snabbdom 的 h 函数来生成上边这个vdom
let vnode = h('ul#list', {}, [
h('li.item', {}, ['item1']),
h('li.item', {}, ['item2']),
]);
// 由此可以看出: h 函数用来生成虚拟节点vnode,他的参数为
h('选择器字符串', '描述对象', '子元素数组或文本字符串')
// patch 函数
// 第一次渲染时,把vnode塞入container
// 参数:空的容器节点, 要在其中渲染的虚拟dom
patch(container, vnode)
// 第二次渲染时,对比两个vnode,只更新必要的部分
// 参数:旧的vnode,新的vnode
patch(vnode, newVnode)
介绍一下diff算法
- 什么是diff算法
- diff算法很复杂,这里删繁就简,只提及核心主干
- vdom为何用diff算法
- diff算法的实现流程
Linux中就有一个基本的diff命令
diff 1.txt 2.txt
来比较两个文件的内容。git中也有diff,来比较改动。前端vue,react中的diff并非独创。
diff算法复杂,实现难度大,源码量大。这里我们去繁就简,讲明白核心流程,不关心细节。但即便如此,依然不简单。
vdom为何使用diff算法:dom操作昂贵,尽量减少;用diff算法找出不同,只更新必要的节点
diff的实现过程
上节讲了snabbdom库的patch函数,他有两种用法
- patch(container, vnode)
- patch(vnode, newVnode)
patch(container, vnode)
这种用法,我们把重点放在如何把表示vdom的js对象变成html;
// 将vdom转化为真实的dom
// 此处只用伪代码写明思路
function createElement(vnode) {
var tag = vnode.tag;
var attrs = vnode.attrs || {};
var children = vnode.children || [];
if(!tag) return null
// 创建真实的dom元素
var elem = document.createElement(tag);
// 设置属性
for(var attrName in attrs) {
if(attrs.hasOwnProperty(attrName)) {
elem.setAttribute(attrName, attrs[attrName])
}
}
// 递归生成并插入子元素
children.forEach(function(child) {
elem.appendChild(createElement(child));
});
// 返回真实的dom元素
return elem
}
patch(vnode, newVnode)
这种用法,我们把重点放在如何比较两个vdom的不同
// 比较两个vdom的区别
// 此处只用伪代码写明思路
// 实际上根元素是不变的 如vue,react中的: <div id="app"></div>
// 所以只要比较子元素
function updateChildren(vnode, newVnode) {
var children = vnode.children || [];
var newChildren = newVnode.children || [];
// 遍历子元素
children.forEach(function(childVnode, index) {
var newChildVnode = newChildren[index];
// 新旧比较tag是否相同
if(childVnode.tag === newChildVnode.tag) {
// tag相同则递归对比下一层子元素
updateChildren(childVnode, newChildVnode);
} else {
// tag不同则直接替换该tag
replaceNode(childVnode, newChildVnode)
}
})
}
// 替换节点
function replaceNode(vnode, newVnode) {
var elem = vnode.elem; // 通过现有vnode对应找到真实的dom节点
var newElem = createElement(newVnode);
// 替换
}
上边只是大概讲了核心思路,还有其他更多更复杂的内容