vdom 是什么,为何会存在 vdom
什么是 vdom
- virtual dom, 虚拟 DOM
- 用 JS 模拟 DOM 结构
- DOM 操作非常“昂贵”
- DOM 变化的对比,放在 JS 层来做(图灵完备语言)提高效率
- 提高重绘性能
// 真实的 DOM
<ul id="list">
<li class="item">Item1</li>
<li class="item">Item2</li>
</ul>
// 虚拟 DOM
{
tag: 'ul',
attrs: {
id: 'list'
},
children: [{
tag: 'li',
attrs: { calssName: 'item' },
children: ['Item1']
}, {
tag: 'li',
attrs: { calssName: 'item' },
children: ['Item2']
}]
}
设计一个需求场景
// 1、将该数据展示成一个表格。 2、随便修改一个信息,表格也跟着更改
[{
name: '张三',
age: '20',
address: '北京'
}, {
name: '李四',
age: '21',
address: '上海'
}, {
name: '王五',
age: '22',
address: '广州'
}]
用 jQuery 实现
<div id="container"></div>
<button id="btn-change">change</button>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
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>
遇到的问题
- DOM 操作是低效的,js 运行效率高
- 尽量减少 DOM 操作
- 项目越复杂,影响越严重
- vdom 可解决这个问题
var div = document.createElement('div')
var item, result = '';
for (item in div) {
result += '|' + item;
}
console.log(result)
打印的内容为:(证明操作DOM 是低效的 )
|align|title|lang|translate|dir|dataset|hidden|tabIndex|accessKey|draggable|spellcheck|autocapitalize|contentEditable|isContentEditable|inputMode|offsetParent|off...
vdom 如何应用,核心 API 是什么
解答思路
- 可用snabbdom 的用法来举例
- 核心 API: h 函数, patch 函数
介绍 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
重做之前的 demo
<div id="container"></div>
<button id="btn-change">change</button>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.1/snabbdom.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.1/snabbdom-class.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.1/snabbdom-props.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.1/snabbdom-style.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.1/h.js"></script>
<script>
var data = [{
name: '张三',
age: '20',
address: '北京'
}, {
name: '李四',
age: '21',
address: '上海'
}, {
name: '王五',
age: '22',
address: '广州'
}];
data.unshift({
name: '姓名',
age: '年龄',
address: '地址'
})
var snabbdom = window.snabbdom;
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
]);
var h = snabbdom.h;
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 = newVnode;
}
render(data);
var btnChange = document.getElementById('btn-change')
btnChange.addEventListener('click', function() {
data[1].age = 30;
data[2].address = '深圳';
render(data);
})
</script>
核心 API
- h('<标签名>', {...属性...}, [...子元素...])
- h('<标签名>', {...属性...}, '...')
- patch(container, vnode)
- patch(vnode, newVnode)
介绍一下 diff 算法
解答思路:
- 知道什么是 diff 算法,是 linux 的基础命令
- vdom 中应用 diff 算法是为了找出需要更新的节点
- diff 实现 patch(container, vnode) patch(vnode, newVnode)
- 核心逻辑 createElement 和 updateChildren
diff 算法非常复杂,实现难度大,源码量很大
去繁就简,讲明白核心流程,不关心细节
面试官也大部分不清楚细节,但是很关心核心流程
去繁就简后,依然有很大的挑战性,并不简单
vdom 为何使用 diff 算法
- DOM 操作是昂贵的,因此尽量减少 DOM 操作
- 找出本次 DOM 必须更新的节点来更新,其他的不更新
- 这个“找出”的过程,就需要 diff 算法
diff 实现过程
- patch(container, vnode)
- patch(vnode, newVnode)
// patch(container, vnode)
// 虚拟 DOM
{
tag: 'ul',
attrs: {
id: 'list'
},
children: [{
tag: 'li',
attrs: { calssName: 'item' },
children: ['Item1']
}, {
tag: 'li',
attrs: { calssName: 'item' },
children: ['Item2']
}]
}
// 模拟代码(将虚拟 dom 转化为真实 DOM)
function createElement(vnode) {
var tag = vnode.tag;
var attrs = vnode.attrs || {};
var children = vnode.children || [];
if (!tag) {
return null
}
// 创建元素
var elem = 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))
})
return elem;
}
// patch(vnode, newVnode) 对比两个 vnode
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 === newChild.tag) {
// 两者 tag 一样
updateChildren(child, newChild)
} else {
// 两者 tag 不一样
replaceNode(child, newChild)
}
})
}
另外还有 节点新增和删除、节点重新排序、节点属性、样式、事件绑定、如何极致压榨性能
网友评论