背景
我們都知道頻繁的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……,