什么是virtual Dom?
Virtual Dom 是虚拟DOM,我们用JS来模拟DOM结构,结构类似下面的代码:
{
tag:'ul',
attrs:{
id:'list'
}
children:[
{
tag:'li',
attrs:{className:'item'},
children:['item 1']
},
{ tag:'li',
attrs:{className:'item'},
children:['item 2']
}
]
}复制代码
以上代码模拟的就是这样的DOM结构
<ul>
<li class='item'>item 1</li>
<li class='item'>item 2</li></ul>复制代码
那么为什么会有VDOM(virtual dom简称)这样的结构呢?
为什么会有Virtual DOM?
我们来模拟这样的一个场景需求。
1.有一堆数据,需要将数据渲染成表格
2.随便修改一个信息,表格也会跟着变化
如果没有VDOM,我们会用这样的代码来完成需求
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title></head><body>
<div id="container"></div>
<button id="btn-change">change</button>
<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
<script type="text/javascript">
var data = [
{
name: '张三',
age: '20',
address: '北京'
},
{
name: '李四',
age: '21',
address: '上海'
},
{
name: '王五',
age: '22',
address: '广州'
}
]
// 渲染函数
function render(data) {
var $container = $('#container')
// 清空容器,重要!!!
$container.html('')
// 拼接 table
var $table = $('<table>')
$table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>'))
data.forEach(function (item) {
$table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td>/tr>'))
})
// 渲染到页面
$container.append($table)
}
$('#btn-change').click(function () {
data[1].age = 30
data[2].address = '深圳'
// re-render 再次渲染
render(data)
})
// 页面加载完立刻执行(初次渲染)
render(data)
</script>
</body>
</html>
复制代码
上面的代码虽然完成了需求,但是,遗憾的是,如果我只是修改一部分数据,整个table
都需要全部渲染。对于浏览器而言,渲染DOM是一个非常“昂贵“的过程。那么,有没有什么办法,修改部分数据的时候,只是渲染我修改的DOM呢?
使用VDOM实现只渲染修改的DOM
我们首先来使用一下snabbdom这个库,它会利用VDOM来实现局部渲染。一起来感受一下
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title></head><body>
<div id="container"></div>
<button id="btn-change">change</button>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/h.js"></script>
<script type="text/javascript">
var snabbdom = window.snabbdom
// 定义关键函数 patch
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])
// 定义关键函数 h
var h = snabbdom.h
// 原始数据
var data = [
{
name: '张三',
age: '20',
address: '北京'
},
{
name: '李四',
age: '21',
address: '上海'
},
{
name: '王五',
age: '22',
address: '广州'
}
]
// 把表头也放在 data 中
data.unshift({
name: '姓名',
age: '年龄',
address: '地址'
})
var container = document.getElementById('container')
// 渲染函数
var vnode
function render(data) {
var newVnode = h('table', {}, data.map(function (item) {
var tds = []
var i
for (i in item) {
if (item.hasOwnProperty(i)) {
tds.push(h('td', {}, item[i] + ''))
}
}
return h('tr', {}, tds)
}))
if (vnode) {
// 如果已经渲染了
patch(vnode, newVnode)
} else {
// 初次渲染
patch(container, newVnode)
}
// 存储当前的 vnode 结果
vnode = newVnode
}
// 初次渲染
render(data)
var btnChange = document.getElementById('btn-change')
btnChange.addEventListener('click', function () {
data[1].age = 30
data[2].address = '深圳'
// re-render
render(data)
})
</script>
</body>
</html>复制代码
以上代码,则实现了当你修改了部分数据时,只渲染一部分数据。
那么,以上代码的核心就是两个函数,我们需要对此来做探讨。一个是函数h,一个是函数patch
关键函数h和关键函数patch
关键函数h
函数h返回的值是一个vnode,也就是虚拟DOM节点,如图所示
也就是说使用h函数可以生成类似于右边的vnode结构。
关键函数patch
那么关键函数patch的作用,则是将vnode渲染成真实的DOM节点,然后塞入到容器里面。
如果容器里面已经有生成好的vnode,那么,则会将新生成的newVnode和之前的vnode相比较,然后将不同的节点找出来,然后代替旧的节点。
到现在为止,已经基本上了解了VDOM的含义以及为什么会用VDOM,我们来做一个简单的总结
- 如果只有部分数据变动,却要因此渲染整个DOM
- DOM是非常“昂贵”的操作,因此我们需要减少DOM操作
- 找出必须要更新的节点,其他的则可以不更新
那么,接下来又出现了一个问题,我们如何知道哪个节点需要更新呢?这就是diff算法的作用
diff算法找出需要更新的DOM
因为diff算法本身太过于复杂,所以只需要理解一下核心的思想即可。
那么我们只需要关注在渲染的时候,发生了什么事情,理解下面这两个事件的核心流程即可。
- patch(container, newVnode)
- patch(vnode, newVnode)
也就是说,我们需要理解的是:
- 初次渲染的时候,将VDOM渲染成真正的DOM然后插入到容器里面。
- 再次渲染的时候,将新的vnode和旧的vnode相对比,然后如何进行局部渲染的过程。
1.patch(container, newVnode)
我们要实现的是这样的过程:
我们来模拟一下上面的创建过程,只是伪代码,我们了解大致的流程
function createElement(vnode) {
var tag = vnode.tag // 'ul'
var attrs = vnode.attrs || {}
var children = vnode.children || []
if (!tag) {
return null
}
// 创建真实的 DOM 元素
var elem = document.createElement(tag)
// 属性
var attrName
for (attrName in attrs) {
if (attrs.hasOwnProperty(attrName)) {
// 给 elem 添加属性
elem.setAttribute(attrName, attrs[attrName])
}
}
// 子元素
children.forEach(function (childVnode) {
// 给 elem 添加子元素,如果还有子节点,则递归的生成子节点。
elem.appendChild(createElement(childVnode)) // 递归
}) // 返回真实的 DOM 元素
return elem
}复制代码
那么,通过上面的模拟代码,已经可以很好的了解最开始将vdom渲染到容器的过程。
2.patch(vnode, newVnode)
这个过程就是将newVnode和vnode对比,将差异进行渲染的部分。
那么伪代码流程如下:
function updateChildren(vnode, newVnode) {
var children = vnode.children || []
var newChildren = newVnode.children || []
children.forEach(function (childVnode, index) {
var newChildVnode = newChildren[index]
if (childVnode.tag === newChildVnode.tag) {
// 深层次对比,递归
updateChildren(childVnode, newChildVnode)
} else {
// 替换
replaceNode(childVnode, newChildVnode)
}
}
)}
function replaceNode(vnode, newVnode) {
var elem = vnode.elem // 取得旧的 真实的 DOM 节点
var newElem = createElement(newVnode)//生成新的真实的dom节点
// 替换
}复制代码
那么真正的替换过程有哪些呢?简单的总结一下:
- 找到对应的真实dom,称为
elem
- 判断
newVnode
和oldVnode
是否指向同一个对象,如果是,那么直接return
- 如果他们都有文本节点并且不相等,那么将
el
的文本节点设置为Vnode
的文本节点。 - 如果
oldVnode
有子节点而newVnode
没有,则删除el
的子节点 - 如果
oldVnode
没有子节点而newVnode
有,则将Vnode
的子节点真实化之后添加到elem
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点,这一步很重要,请参考这篇文章
以上只是简单的理解了diff算法的流程,关于更多的diff算法的详细过程,可以阅读参考文章。
参考文章
转载于:https://juejin.im/post/5d0228436fb9a07f0357354d