浅谈 Virtual DOM 的那些事
程序员文章站
2022-03-18 15:42:21
背景 我们都知道频繁的dom给我们带来的代价是昂贵的,例如我们有时候需要去更新Table 的部分数据,必须去重新重绘表格,这代价实在是太大了,相比于频繁的手动去操作dom而带来性能问题,vdom很好的将dom做了一层映射关系,进而将在我们本需要直接进行dom的一系列操作,映射到了操作vdom. 解决 ......
背景
我们都知道频繁的dom给我们带来的代价是昂贵的,例如我们有时候需要去更新Table 的部分数据,必须去重新重绘表格,这代价实在是太大了,相比于频繁的手动去操作dom而带来性能问题,vdom很好的将dom做了一层映射关系,进而将在我们本需要直接进行dom的一系列操作,映射到了操作vdom.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>virtualDom</title> </head> <body> <div id="container"></div> <button id="btn-change">修改</button> <script type="application/javascript" src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> <script type="application/javascript"> const dataSource = [{ key: '1', name: '胡彦斌', age: 32, address: '西湖区湖底公园1号' }, { key: '2', name: '胡彦祖', age: 42, address: '西湖区湖底公园1号' }]; const columns = [{ title: '姓名', dataIndex: 'name', key: 'name', }, { title: '年龄', dataIndex: 'age', key: 'age', }, { title: '住址', dataIndex: 'address', key: 'address', }]; function render(data) { var container = $('#container'); container.html(''); //清空容器 //添加表头 var $table =$('<table>') $table.append($('<tr>')) columns.map(function(item,index){ $table.append($('<td>'+item.title+'</td>')) }) $table.append($('</tr>')) //添加表体 dataSource.forEach(function(item){ $table.append($('<tr></tr><td>'+item.name+'</td>'+'<td>'+item.age+'</td>'+'<td>'+item.address+'</td></tr>')) }) //只渲染一遍dom,尽然如此,还是需要清空容器 container.append($table) } $('#btn-change').click(function(){ dataSource[0].name="胡军网"; dataSource[1].address='南山区沙河东路1号' //re——render render(dataSource) }) render() </script> </body> </html>
解决
- virtual dom,虚拟 DOM
- 用 JS 模拟 DOM
什么是vdom
HTML DOM 结构:
<ul id="ul-list"> <li class="item">Item 1</li> <li class="item">Item 2</li> <li class="item">Item 3</li> </ul>
针对于上面HTML DOM 结构,可以用JS表示为:
var ulE = { tagName: 'ul', // 标签名 props: { // 属性用对象存储键值对 id: 'ul-list' }, children: [ // 子节点 {tagName: 'li', props: {className: 'item'}, children: ["Item 1"]}, {tagName: 'li', props: {className: 'item'}, children: ["Item 2"]}, {tagName: 'li', props: {className: 'item'}, children: ["Item 3"]}, ] }
JS对象中抽取公共的部分属性,进一步封装:
export default Ele = (tagName, props, children) => { this.tagName = tagName this.props = props this.children = children }
import * as el from 'ele'; var ol = el('ul', {id: 'ul-list'}, [ el('li', {className: 'item'}, ['Item 1']), el('li', {className: 'item'}, ['Item 2']), el('li', {className: 'item'}, ['Item 3']) ]);
通过snabbdom进行virtual dom(核心API:h函数、patch函数)
案例一: 对比局部更新添加修改ul中的li
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>snabbdom</title> </head> <body> <div id="container"></div> <button id="btn-change">修改</button> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.min.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.min.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.min.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.min.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.min.js"></script> <script type="application/javascript"> 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'); //定义 virtual node var vnode = h('ul#ul-list',{},[ h('li.item',{},'item1'), h('li.item',{},'item2') ]) patch(container,vnode); document.getElementById('btn-change').addEventListener('click',function () { var newVnode = h('ul#ul-list',{},[ h('li.item',{},'item1'), h('li.item',{},'西湖区湖底公园1号'), h('li.item',{},'西湖区湖底公园2号'), h('li.item',{},'西湖区湖底公园3号') ]) patch(vnode,newVnode); }) </script> </body> </html>
item1 所在的li不会进行dom渲染,只有新增或者修改的node才会发生改变,执行结果如下所示:
案例二: 局部更新部分Table 数据(使用Vitual DOM 性能的提升)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>snabbdom</title> </head> <body> <div id="container"></div> <button id="btn-change">修改</button> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.min.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.min.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.min.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.min.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.min.js"></script> <script type="application/javascript"> 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'); const dataSource = [{ key: '1', name: '胡彦斌', age: 32, address: '西湖区湖底公园1号' }, { key: '2', name: '胡彦祖', age: 42, address: '西湖区湖底公园1号' }]; const columns = [{ title: '姓名', dataIndex: 'name', key: 'name', }, { title: '年龄', dataIndex: 'age', key: 'age', }, { title: '住址', dataIndex: 'address', key: 'address', }]; var vdom=null; function render(dataSource) { var titleTr= []; titleTr.push(h('td',{},' ')) columns.forEach(function(item){ if(item.hasOwnProperty('title')){ titleTr.push(h('td',{},item['title'])) } }) var vTitle = h('tr',{},titleTr); var vBody =dataSource.map(function(item){ const vp= [] for(var i in item) { if(item.hasOwnProperty(i)){ vp.push(h('td',{},item[i])) } } return h('tr',{},vp) }) vBody.unshift(vTitle); var vTable = Object.assign([],vBody); var newVnode = h('table',{},vTable) if(!vdom){ vdom = newVnode; patch(container,vdom); }else{ patch(vdom,newVnode); } } document.getElementById('btn-change').addEventListener('click',function () { dataSource[0].name="胡军网"; dataSource[1].address='南山区沙河东路1号' //re——render render(dataSource) }) render(dataSource) </script> </body> </html>
执行结果如下所示:
patch函数——patch(container,vDom)过程的简单实现
/** * * @param container 容器 * @param vDom 虚拟dom * @constructor */ var ulE = { tagName: 'ul', // 标签名 props: { // 属性用对象存储键值对 id: 'ul-list' }, children: [ // 子节点 {tagName: 'li', props: {className: 'item'}, children: ["Item 1"]}, {tagName: 'li', props: {className: 'item'}, children: ["Item 2"]}, {tagName: 'li', props: {className: 'item'}, children: ["Item 3"]}, ] } export default function VDomCreateElement(vDom){ var tagName=vDom.tagName || ''; var props =vDom.props || {}; var children =vDom.children || []; var tagNameEle =document.createElement(tagName); for(var prop in props){ if(props.hasOwnProperty(prop)){ tagNameEle.setAttribute(prop,props[prop]) } } if(!children){ return tagNameEle; }else{ children.forEach(function(item){ tagNameEle.appendChild(VDomCreateElement(item)) //不断递归生成child Node }) } return tagNameEle; }
patch函数——patch(vDom,newVDom)过程的简单模拟实现
/** * vDOM 简单diff 对比 新的dom渲染到旧的dom * @param vDom 老vDom * @param newVDom 新vDom */ export function vDomDiff(vDom,newVDom){ var vDomChilden = vDom.children || []; var newVDomChilden = newVDom.children || []; //假设 tagName 相同 vDomChilden.forEach(function(item,index){ if(!newVDomChilden[index]){ return; } if(item.tagName === newVDomChilden[index].tagName){ //两者tagName 一样 递归 VDomCreateElement(item,newVDomChilden[index]); }else { //两者tagName 不一样 替换 replaceNode(item,newVDomChilden[index]) } }) } /** * dom操作 替换 * @param vDom * @param newVDom */ function replaceNode(vDom,newVDom){ //dom操作 node替换 // .... }
Visual DOM 为何使用diff算法
Visual DOM找出DOM 中不同,进而更新DOM,diff算法同样也是找出文件中的不同进行对比,diff应用在linux,git……,
上一篇: Web应用三种部署方式的优缺点