一、virtual dom是什么?它为什么会出现?
1、是什么?
- virtual dom即 虚拟dom
- 用js模拟DOM结构
- DOM变化的对比,放在JS层来做
- 提高重绘性能
// 真实的HTML DOM结构
- Item 1
- Item 2
// 用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操作是非常‘昂贵’的,看似更复杂JS的virtual dom实则效率更高
// 用jquery实现修改DOM
当点击change按钮后,看似只有彭二的age和彭三的height发生改变,但实则整个table表单又重新渲染了一次
2、遇到的问题
- DOM操作是‘昂贵’的,JS运行效率高
- 尽量减少DOM操作,而不是‘推到重来’
- 项目越复杂,影响越严重
- Virtual DOM即可解决这个问题
3、virtual dom存在的必要
- 用JS模拟DOM结构,效率更高
- DOM操作‘昂贵’
- 将DOM对比操作放在JS层,提高效率
4、virtual dom实现的三步
- 通过 JS 来模拟创建 DOM 对象
- 判断两个对象的差异
- 渲染差异
二、virtual dom如何应用,核心API是什么?
1、如何用?
能实现virtual dom的库很多,如: snabbdom
var container = document.getElementById('container');
var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
' and this is just normal text',
h('a', {props: {href: '/foo'}}, 'I\'ll take you places!')
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);
var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
' and this is still just normal text',
h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
]);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
上面一段是snabbdom给出的一个示例,其中 h方法 是创建一个vnode,即虚拟节点,定义一个div,有一个id名container
,两个类名two
和classes
,绑定一个click事件someFn方法,后面跟着一个数组,数组中有3个元素:
- 第一个用h方法返回的,一个span,有
font-weight
样式,和文本内容This is bold
- 第二个元素就是一个文本字符串:
and this is just normal text
- 第三个元素是一个a元素,有一个属性href链接,后面是a标签文本
第一个patch
方法是将vnode放入到空的container中
newVnode
是返回一个新的node,然后第二个patch
是将前后两个node进行一个对比,找出区别,只更新需要改动的内容,其他不更新的内容不更新,这样做到尽可能少的操作DOM。
h方法抽离出来如下:
用h方法去具体实现文章开头的那个简单dom节点
// 真实的HTML DOM结构
- Item 1
- Item 2
// JS模拟这个DOM
{
tag: 'ul',
attrs: {
id: 'list'
},
children: [
{
tag: 'li',
attrs: {className: 'item'},
children: ['Item 1']
},
{
tag: 'li',
attrs: {className: 'item'},
children: ['Item 2']
}
]
}
// 用h方法表示这个dom节点:
var vnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),
h('li.item', {}, 'Item 2'),
])
用virtual dom写法改写jquery的那个demo:
2、核心API
- h('<标签名>', {属性}, [子元素])
- h('<标签名>', {属性}, '文本字符串')
- 初次渲染:patch(container, vnode)
- 再次修改后DOM渲染:patch(vnode, newVnode)
三、diff算法
1、什么是diff算法
日常开发中都会用到diff,最普通的linux基础命令
diff
两个文件,找出不同,还有就是git命令比对前后修改内容
// 两个对象分别放在两个json中
// data1.json
{
"name": "pengxiaohua",
"age": 18,
"height": 184
}
// data2.json
{
"name": "xiaohua",
"age": 18,
"height": 183
}
// 控制台输入 diff data1.json data2.json,得出:
2c2
< "name": "pengxiaohua",
---
> "name": "xiaohua",
4c4
< "height": 184
---
> "height": 183
同时在git命令中的git diff XXXX
也可以用来比对文件修改前后的差别
virtual dom为何用diff算法?
- DOM操作是昂贵的,应该尽可能减少DOM操作
- 找出本次必须更新的节点,其他的不用更新
- 这个“找出”的过程,就需要diff算法
一句话,virtual dom中应用diff算法是为了找出需要更新的节点
2、diff算法实现流程
diff算法分为了两步:
- 1、首先从上至下,从左往右遍历对象,也就是树的深度遍历,这一步中会给每个节点添加索引,便于最后渲染差异。
- 2、一旦节点有子元素,就去判断子元素是否有不同。
diff的实现过程就是 patch(container, vnode)
和 - patch(vnode, newVnode)
diff的实现的核心就是 createElement
和 updateChildren
-
patch(container, vnode)
初始化加载,直接将vnode
节点打包渲染到一个空的容器container
中
文章开头可以看到,用JS去模拟一个简单的DOM节点
// 真实的HTML DOM结构
- Item 1
- Item 2
// JS模拟这个DOM
{
tag: 'ul',
attrs: {
id: 'list'
},
children: [
{
tag: 'li',
attrs: {className: 'item'},
children: ['Item 1']
},
{
tag: 'li',
attrs: {className: 'item'},
children: ['Item 2']
}
]
}
那么模拟完了之后,怎么将模拟的JS进行转化为真实的DOM的呢?这个转化过程可以用这样一个 createElement
函数来描述:
function createElement (vnode) {
var tag = vnode.tag
var attrs = vnode.attrs || {}
var children = vnode.children || []
if(!tag) {
return null
}
// 创建真实的 DOM 元素
var ele = document.createElement(tag)
// 属性
var attrName
for (attrName in attrs) {
if (attrs.hasOwnProperty(attrName)) {
elem.setAttribute(attrName, attrs[attrName])
}
}
// 子元素
children.forEach(function (childVnode) {
// 递归调用 createElement 创建子元素
elem.appendChild(createElement(childVnode))
})
// 返回真实的 DOM 元素
return elem
}
当我们修改子节点,如下操作后,就要用到 patch(vnode, newVnode)
-
patch(vnode, newVnode)
数据改变后,patch对比老数据vnode
和新数据newVnode
// 真实的HTML DOM结构
- Item 1
- Item 22
- Item 3
// JS模拟这个DOM
{
tag: 'ul',
attrs: {
id: 'list'
},
children: [
{
tag: 'li',
attrs: {className: 'item'},
children: ['Item 1']
},
{
tag: 'li',
attrs: {className: 'item'},
children: ['Item 22']
},
{
tag: 'li',
attrs: {className: 'item'},
children: ['Item 3']
}
]
}
这个转化过程,其实就是遍历子节点,然后找出区别,如下面的方法 updateChildren
:
function updateChildren (vnode, newVnode) {
var children = vnode.children || []
var newChildren = newVnode.children || []
// 遍历现有的 children
children.forEach(function (child, index) {
var newChild = newChildren[index]
if (newChild == null) {
return
}
if (child.tag === newChildren.tag) {
// 两者 tag 一样
updateChildren(child, newChild)
} else {
// 两者 tag 不一样
replaceNode(child, newChild)
}
})
}
function replaceNode (vnode, newVnode) {
// 真实的DOM节点
var elem = vnode.elem
var newElem = createElement(newVnode)
// 替换(此处代码太过复杂,略省无数字)
... ...
}
3、diff算法做了哪些事:
- 节点的新增和删除
- 节点重新排序
- 节点属性、样式、事件绑定
- 如何极致压榨性能
- ... ...