JavaScript從初級往高階走系列————Virtual Dom
阿新 • • 發佈:2019-02-19
什麼是虛擬DOM
- 用JS模擬DOM結構
- DOM變化的對比,放在JS層來做(圖靈完備語言)
- 提高重繪效能
重繪和迴流
頁面渲染過程:
- 當render tree中的一部分(或全部)因為元素的規模尺寸,佈局,隱藏等改變而需要重新構建。這就稱為迴流(reflow)。
- 當render tree中的一些元素需要更新屬性,而這些屬性只是影響元素的外觀,風格,而不會影響佈局的,比如background-color。則就叫稱為重繪。
模擬虛擬DOM
<ul id="list"> <li class="item">Item 1</li> <li class="item">Item 2</li> </ul>
// js模擬虛擬DOM
{
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> <script src="https://cdn.bootcss.com/jquery/2.2.0/jquery.min.js"></script> </head> <body> <div id="container"></div> <button id="btn-change">change</button> <script> var data = [ {name: '張三',age: '20',address: '北京'}, {name: '王五',age: '22',address: '成都'}, {name: '李四',age: '21',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> </body> </html>
雖然只改變了兩個資料,但是整個table都閃爍了(迴流&重繪)
- DOM操作是‘昂貴’的,js執行效率高
- 儘量減少DOM操作,儘量減少迴流重繪
虛擬DOM如何應用,核心API是什麼
介紹 snabbdom
官網例子:
var snabbdom = require('snabbdom'); var patch = snabbdom.init([ // Init patch function with chosen modules require('snabbdom/modules/class').default, // makes it easy to toggle classes require('snabbdom/modules/props').default, // for setting properties on DOM elements require('snabbdom/modules/style').default, // handles styling on elements with support for animations require('snabbdom/modules/eventlisteners').default, // attaches event listeners ]); var h = require('snabbdom/h').default; // helper function for creating vnodes var container = document.getElementById('container'); // h函式生成一個虛擬節點 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); // 把vnode加入到container中 // 資料改變,重新生成一個newVnode 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 // 將newVnode更新到之前的vnode中,從而更新檢視 patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
snabbdom h 函式
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']
}
]
}
snabbdom patch 函式
var vnode = h('ul#list',{},[
h('li.item',{},'Item 1'),
h('li.item',{},'Item 2')
])
var container = document.getElementById('container');
patch(container, vnode);
// 模擬改變
var btnChange = document.getElementById('btn-change');
btnChange.addEventListener('click',function(){
var newVnode = h('ul#list',{},[
h('li.item',{},'Item 111'),
h('li.item',{},'Item 222'),
h('li.item',{},'Item 333')
])
patch(vnode, newVnode);
})
snabbdom例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<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.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/h.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
</head>
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<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>
看圖,只有修改了的資料才進行了重新整理,減少了DOM操作,這其實就是vnode與newVnode對比,找出改變了的地方,然後只重新渲染改變的
重做之前的demo
<!DOCTYPE html>
<html>
<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.0/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/h.js"></script>
<script type="text/javascript">
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: '22',address: '成都'},
{name: '李四',age: '21',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>
核心API
- h('<標籤名>',{...屬性...},[...子元素...])
- h('<標籤名>',{...屬性...},'...')
- patch(container,vnode)
- patch(vnode,newVnode)
簡單介紹 diff 演算法
什麼是 diff 演算法
這裡有兩個文字檔案:
借用git bash
中 diff
命令可以比較兩個檔案的區別:
虛擬DOM ---> DOM
// 一個實現流程,實際情況還很複雜
function createElement(vnode) {
var tag = vnode.tag // 'ul'
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 (childVnode) {
// 給 elem 新增子元素
elem.appendChild(createElement(childVnode)) // 遞迴
})
// 返回真實的 DOM 元素
return elem
}
vnode ---> newVnode
function updateChildren(vnode, newVnode) {
var children = vnode.children || [];
var newChildren = newVnode.children || [];
children.forEach(function (childVnode, index) {
var newChildVnode = newChildren[index];
if (childVnode.tag === newChildVnode.tag) {
// 深層次對比,遞迴
updateChildren(childVnode, newChildVnode);
} else {
// 替換
replaceNode(childVnode, newChildVnode);
}
})
}
function replaceNode(vnode, newVnode) {
var elem = vnode.elem; // 真實的 DOM 節點
var newElem = createElement(newVnode);
// 替換
}
最後
建立了一個前端學習交流群,感興趣的朋友,一起來嗨呀!