虚拟 DOM
虚拟DOM :virtual dom(以下简称vdom,是vue和react的核心),使用比较简单。
一,vdom是什么,为何会存在vdom
1,什么是vdom:用js模拟DOM结构,DOM操作非常‘昂贵’,DOM变化的对比,放在JS层来做(图灵完备语言),提高重绘性能
需求:根据给出的数据,将该数据展示成一个表格, 随便修改一个信息, 表格也跟着修改,下面使用jquery实现demo:
<div id="container"></div> <button id="btn-change">change</button> <script src="https://cdn.bootcss.com/jquery/3.3.1/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)
遇到的问题:DOM操作是昂贵的,改动后,整个container容器都重新渲染了一遍,相当于‘推倒重来’,如果项目复杂,非常影响性能
dom操作的属性是非常多的,非常复杂,操作很昂贵,所以,尽量用js代替操作,例:
var div = document.createElement('div'); var item, result = ''; for(item in div) { result += '|' + item; } console.log(result);
vdom可以解决这个问题
二,vdom如何应用,核心API是什么
1,介绍snabbdom
var vnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),
h('li.item', {}, 'Item 2')
])
{
tag: 'ul',
attrs: {
id: 'list'
},
children: [{
tag: 'li',
attrs: { className: 'item' },
children: ['Item 1']
}, {
tag: 'li',
attrs: { className: 'item' },
children: ['Item 2']
}]
}
<!DOCTYPE html> <html lang="en"> <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.1/snabbdom.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.min.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.js"></script> <script> var snabbdom = window.snabbdom; // 定义patch var patch = snabbdom.init([ snabbdom_class, snabbdom_props, snabbdom_style, snabbdom_eventlisteners ]) // 定义h var h = snabbdom.h; var container = document.getElementById('container'); // 生成vnode var vnode = h('ul#list', {}, [ h('li.item', {}, 'Item 1'), h('li.item', {}, 'Item 2'), ]); patch(container,vnode) // 模拟改变 var btnChange = document.getElementById('btn-change'); btnChange.addEventListener('click', function() { var newVnode = h('ul#list', {}, [ h('li.item', {}, 'Item 1'), h('li.item', {}, 'Item 222'), h('li.item', {}, 'Item 333'), ]); patch(vnode,newVnode); }) </script> </body> </html>
2,重做之前的demo
<!DOCTYPE html> <html lang="en"> <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.1/snabbdom.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.min.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.js"></script> <script> 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) { // re-render 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>
3,核心API
h('标签名',{...属性...},[...子元素...]) //多个子元素
h('标签名',{...属性...},'...') //只有一个子元素
patch(container,vnode) //初次渲染,会把外层容器替代掉
patch(vnode,newVnode) //re-render
三,介绍diff算法(vdom核心算法)
1,vdom为何用diff算法
diff 是linux的基础命令,可以比较两个文本文件的不同 git diff xxx; vdom中应用diff算法是为了找出需要更新的节点
比如新建两个文本文件,log1.txt log2.txt
diff log1.txt log2.txt
diff在线对比:http://tool.oschina.net/diff
使用vdom原因:DOM操作是昂贵的,因此尽量减少DOM操作
找出本次DOM必须更新的节点来更新,其他的不更新
这个找出的过程,就需要diff算法 找出前后两个vdom的差异
2,diff算法的实现流程
vdom核心函数:h生成dom节点,patch函数-进行对比和渲染的
patch(container,vnode) 初次渲染,会把外层容器替代掉
patch(vnode,newVnode) re-render
3,如何用vnode生成真是的dom节点
diff实现:
1,patch(container,vnode)
2,patch(vnode,newVnode)
核心逻辑:createElement 和 updateChildren
// patch(container,vnode) 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); // 属性 var attrName; for (attrName in attrs) { if (attrs.hasOwnProperty(attrName)) { // 给elem添加属性 elem.setAttribute(attrName, attrs[attrName]); } } // 子元素 children.forEach(function(childNode) { // 递归调用 createElement 给elem添加子元素 elem.appendChild(createElement(childVnode)); //递归 }) // 返回真实的DOM元素 return elem; } // patch(vnode,newVnode) 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) } }) } function replaceNode(vnode, newVnode) { var elem = vnode.elem; //真实的DOM节点 var newElem = createElement(newVnode); // 替换 }