結論
vue
中vdom渲染頁面的過程:將<template>
模板,通過render
渲染函數(createElement())得到虛擬的DOM樹,通過diff
算法進行新舊虛擬節點的比較,再通過patch
更新到真實的dom上實現視圖的更新。
1.什麼是Virtal DOM?
vdom指的是用JS模擬的DOM結構,將DOM的變化對比放在JS層。
<ul id="list">
<li class="item">Item1</li>
</ul>
變成vdom就是:
{
tag:'ul',
attrs:{
id:'list'
},
children:[{
tag:'li',
attrs:{
className:'item',
},
children:['Item1']
}]
}
2.爲什麼要使用vdom?
問題
在《高性能javascript》中提到,操作dom的代價很‘昂貴’,會導致頁面的重排和重繪等問題,影響js的性能。所以應該減少dom的訪問次數,將操作放在js中。
解決
使用vdom,只需要改變更新的DOM,不需要改動的地方不動,對dom的一些頻繁操作都在vdom樹上,減少重排重繪帶來的性能消耗,提高渲染的效率。
- 將真實的DOM編譯成vnode
- diff,比較oldVnode和newVnode之間的變化
- patch,將這些變化用打補丁的方式更新到真實的dom上去。
3.借用snabbdom的思想實現vdom
3.1介紹snabbdom
snabbdom
是一個簡易的實現vdom
功能的庫,vdom
裏面有兩個核心的API,一個是h
函數,一個是patch
函數。前者是用來生成vdom對象(vue
中使用render
函數,將真實的節點轉換成vnode
),後者是用做vdom
之間的對比以及將vdom掛載到真實的dom上。vue就是因爲其使用了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
//h:創建一個虛擬的節點,定義id=container
var container = document.getElementById('container');
//h:container有兩個類名class和two 綁定一個click事件,跟着一個數組
var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
//h: <h style='font-weight:bold'>This is bold </h>
h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
' and this is just normal text',
h('a', {props: {href: '/foo'}}, 'I\'ll take you places!')
]);
//pacth:首次渲染時將vnode放入到空的container中
patch(container, vnode);
//一個新的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!')
]);
//patch: dom變化時,新舊vnode比較,只更新需要改動的內容
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
3.2用h方法實現創建vdom
<ul id="list">
<li class="item">Item1</li>
</ul>
var vnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),,
])
3.3patch源碼
return function patch(oldVnode, vnode) {
var i, elm, parent;
//記錄被插入的vnode隊列,用於批觸發insert
var insertedVnodeQueue = [];
//調用全局pre鉤子
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
//如果oldvnode是dom節點,轉化爲oldvnode
if (isUndef(oldVnode.sel)) {
oldVnode = emptyNodeAt(oldVnode);
}
//如果oldvnode與vnode相似,進行更新
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);//patchnode裏有diff算法的核心updatechildren
} else {
//否則,將vnode插入,並將oldvnode從其父節點上直接刪除
elm = oldVnode.elm;
parent = api.parentNode(elm);
//將vnode生成真實的dom
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
//插入完後,調用被插入的vnode的insert鉤子
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
}
//然後調用全局下的post鉤子
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
//返回vnode用作下次patch的oldvnode
return vnode;
};
patch
方法中實現了snabbdom作爲高效vdom庫的法寶——diff
算法,diff
爲了找出需要更新的節點,核心邏輯在updateChildren
函數中。
patch中的重要函數sameVnode實現了只能同級訪問。
並且對於vdom中的children比較,因爲同層和有可能移動,順序比較無法最大化的複用已有的DOM,所以通過給每個vnode加上key來跟蹤這種順序的變動,形成爲一標識,高效更新虛擬DOM。
3.4diff算法
爲什麼要是有diff算法
找出新舊虛擬dom的差別,來更新一些節點。怎麼找出,就是通過diff算法。
如果要比較vdom樹的差異理論上的時間複雜度高達O(n^3),但是由於我們在實際開發中很少出現跨級的DOM變更,所以在vdom庫中,之比較同級的,此時的複雜度爲O(n).
diff的算法實現的流程
- 實現的過程patch(vnode,container)初始化加載,將vnode打包渲染到空的容器中和patch(vnode,newVnode)。新舊vnode的比較
- 核心就是createElement和updateChildren
vnode通過updateChildren找出區別:
//簡單的實現更新的操作
function updateChildren(vnode, newVnode) {
let children = vnode.children || [];
let newChildren = newVnode.children || [];
children.forEach((childVnode, index) => {
let newChildVNode = newChildren[index];
//新舊的標籤名是否相等
if(childVnode.tag === newChildVNode.tag) {
//深層次對比, 遞歸過程
updateChildren(childVnode, newChildVNode);
} else {
//替換
replaceNode(childVnode, newChildVNode);
}
})
}
通過createElement將模擬的JS轉化成真實的DOM
const createElement = (vnode) => {
let tag = vnode.tag;
let attrs = vnode.attrs || {};
let children = vnode.children || [];
if(!tag) {
return null;
}
//創建元素
let elem = document.createElement(tag);
//屬性
let attrName;
for (attrName in attrs) {
if(attrs.hasOwnProperty(attrName)) {
elem.setAttribute(attrName, attrs[attrName]);
}
}
//子元素
children.forEach(childVnode => {
//給elem添加子元素
elem.appendChild(createElement(childVnode));
})
//返回真實的dom元素
return elem;
}
https://www.cnblogs.com/chrislinlin/p/12585851.html
https://www.cnblogs.com/tomatoto/p/10002343.html