先介紹一個概念Virtual Dom,我猜你們或多或少都應該知道什麼是Virtual Dom吧,簡單來講就是用js來模擬DOM中的結點。node
下面就是一個Virtual Dom的結構,包含了標籤名,擁有的屬性,孩子結點,render函數算法
class Element {
constructor(tagName, attrs, children) {
this.tagName = tagName;
this.attrs = attrs || {};
this.children = children || [];
}
render () {
//這個函數是用來生成真實DOM的,最後會把return的結果添加到頁面中去
}
}
複製代碼
/**
<ul id="list">
<li class="a">txt_a</li>
<li class="a">txt_b</li>
</ul>
**/
//根據上面結構能夠用一下方式建立一棵 Virtual Dom Tree
let ul = Element('ul', { id: 'list' }, [
Element('li', { class: 'a' }, ['txt_a']),
Element('li', { class: 'b' }, ['txt_b'])
]);//ul 就是一棵個Virtual Dom Tree
let ulDom = ul.render();//生成真實Dom
document.body.appendChild(ulDom);//添加到頁面中
複製代碼
以上就是Virtual Dom Tree如何被轉換成真實Dom並添加到網頁中的過程,再這個過程當中我把render函數給省略,只是爲了讓大家先了解原理,具體實現能夠之後再深究。我學一個東西的時候,習慣是先把總體原理弄清楚,再去深刻學習相關的知識。數組
在介紹Diff算法以前,再次聲明我只會列舉Diff算法中會用到的函數,並串聯它們之間的關係並不會給出具體實現的代碼bash
diff算法是進行虛擬節點Element的對比,並返回一個patchs對象,用來存儲兩個節點不一樣的地方,最後用patchs記錄的消息去局部更新Dom。app
兩個樹若是徹底比較的話須要時間複雜度爲O(n^3),若是對O(n^3)不太清楚的話建議去網上搜索資料。而在Diff算法中由於考慮效率的問題,只會對同層級元素比較,時間複雜度則爲O(n),說白了就是深度遍歷,並比較同層級的節點。dom
let patchs = diff(oldTree, newTree);//獲取兩棵Virtual Dom Tree 差別
patch(ulDom, patchs);//找到對應的真實dom,進行部分渲染
複製代碼
//深度遍歷樹,將須要作變動操做的取出來
//局部更新 DOM
function patch(node,patchs){
//代碼略
}
// diff 入口,比較新舊兩棵樹的差別
function diff (oldTree, newTree) {
let index = 0
let patches = {} // 記錄每一個節點差別的補丁
dfs(oldTree, newTree, index, patches)
return patches;
}
/**
* dfs 深度遍歷查找節點差別
* @param oldNode - 舊虛擬Dom樹
* @param newNode - 新虛擬Dom樹
* @param index - 當前所在樹的第幾層
* @param patches - 記錄節點差別
*/
function dfs (oldNode, newNode, index, patches){
let currentPatch = [];//當前層的差別對比
if (!newNode) {
//若是節點不存不用處理,listdiff函數會處理被刪除的節點
}else if (isTxt(oldNode) && isTxt(newNode)) {//isTxt用來判斷是不是文本,爲了簡便這邊並無聲明
if (newNode !== oldNode)
currentPatch.push({ type: "text", content: newNode })
//若是發現文本不一樣,currentPatch會記錄一個差別
}else if(oldNode.tagName === newNode.tagName && oldNode.key === newNode.key){
//若是發現兩個節點同樣 則去判斷節點是屬性是否同樣,並記錄下來
let attrsPatches = diffAttrs(oldNode, newNode)
if (attrsPatches) {//有屬性差別則把差別記錄下來
currentPatch.push({ type: "attrs", "attrs": attrsPatches })
}
// 遞歸遍歷子節點,並對子節點進行diff比較
diffChildren(oldNode.children, newNode.children, index, patches)
}else{
//最後一種狀況是,兩個節點徹底不同,這樣只須要把舊節點之間替換就行
//把當前差別記錄下來
currentPatch.push({ type: "replace", node: newNode})
}
//若是有差別則記錄到當前層去
if (currentPatch.length) {
if (patches[index]) {
patches[index] = patches[index].concat(currentPatch)
} else {
patches[index] = currentPatch
}
}
}
//判斷兩個節點的屬性差別
function diffAttrs(oldNode, newNode){
let attrsPatches = {};//記錄差別
let count = 0;//記錄差別的條數
/**
代碼略
判斷兩個節點的屬性差別的代碼就略了,
讓大家知道這裏的代碼就是判斷兩個節點的屬性有哪些差別,
若是有差別就記錄在attrsPatches這個對象中,並把它返回
**/
if(0 == count){
return null;
}else {
return attrsPatches;
}
}
//判斷孩子節點
function diffChildren(oldChild, newChild, index, patches){
let { changes, list } = listDiff(oldChild, newChild, index, patches);
if (changes.length) {//若是有差別則記錄到當前層去
if (patches[index]) {
patches[index] = patches[index].concat(changes);
} else {
patches[index] = changes;
}
}
// 代碼略
//遍歷當前數組
oldChild && oldChild.forEach((item, i) => {
// 代碼略
let node;// 通過判斷後node節點是同時存在於oldChild 和 newChild中
//則對節點進行遞歸遍歷 至關於 進入下一層 節點,
let curIndex;
dfs(item, node, curIndex, patches);
// 代碼略
})
}
//判斷oldNodeList, newNodeList 節點的位置差,主要是爲了判斷哪些節點被移動、刪除、新增。
function listDiff(oldNodeList, newNodeList, index){
let changes = [];//記錄 oldNodeList, newNodeList節點的位置差別,是被移動、刪除、新增
let list = [];//記錄 oldNodeList,newNodeList 同時存在的節點
/**
具體判斷邏輯的代碼就略了
**/
return {changes,list};
}
複製代碼
若是你們對函數之間的調用還不明白的話能夠看下面的圖函數
Virtual Dom 算法的實現也就是如下三步學習
上面省略了不少代碼,主要是爲了讓你們快速瞭解Dom diff 的基本原理和流程,若是想更深刻的瞭解,能夠在網上查閱相關資料。ui