小白的diff算法試試水之旅0.前言1. 主角1:Element構造函數2. 主角2:render函數3. 大主角: diff函數4. 更新5. 完成

0.前言

“孩子,你會唱diff算法嗎”

“twinkle,twinkle,diff start”

1. 主角1:Element構造函數

先介紹一下虛擬dom的數據結構,我們都知道源碼裏面有createElement函數,通過他創建虛擬dom,然後調用render函數。還記得VUE腳手架住入口文件那句足夠裝逼的h=>h(App)嗎,其實就是類似createElement(App)這樣子的過程。我們看一下他簡單的結構:

createElement('ul',{class:'ul'},[
		createElement('li',{class:'li'},['1']),
		createElement('li',{class:'li'},['2'])
	])
複製代碼

createElement (type, props, children)傳入三個參數,節點類型、屬性集合、子節點集合

function Element(type, props, children) {
		this.type = type
		this.props = props
		this.children = children || []
}

function createElement (type, props, children) {
	return new Element(type, props, children)
}
複製代碼

複製代碼,自己造兩個節點打印一下,在控制檯觀察一下。

2. 主角2:render函數

這個就是把虛擬dom轉化爲真正的dom的函數。vue裏面把虛擬節點叫做vnode,那我們翻版,也要翻版得像一點才行:

function render (vnode) {
	let el = document.createElement(vnode.type)//創建html元素
	for(let key in vnode.props){//遍歷虛擬dom的屬性集合,給新建的html元素加上
		el.setAttribute(key, vnode.props[key])
	}
	vnode.children&&vnode.children.forEach(child=>{//遞歸子節點,如果是文本節點則直接插入
			child = (child instanceof Element) ? 
			render(child)://不是文本節點,則遞歸render
			document.createTextNode(child)
			el.appendChild(child)
		})
	return el
}
複製代碼

這個是真正的dom喔,是不是飢渴難耐了,那好,可以試一下document.body.appendChild(el),看見新節點沒

3. 大主角: diff函數

都虛擬dom了,還不diff幹啥呢。

function  diff (oldTree, newTree) {
	const patches = {}//差異表記錄差異,這個記錄一個樹的所有差異
	let index = 0//記錄開始索引,我們給節點編號用的
	dfswalk(oldTree, newTree, index, patches)//先序深度優先遍歷,涉及到樹的遍歷,這是必須的
	return patches
}

//老節點、新節點、第幾個節點、差異表
function dfswalk (oldNode, newNode, index, patches) {
	const currentPatch = []
	//...一系列寫入差異的過程
	//最後將當前差異數組寫入差異表
	currentPatch.length && (patches[index] = currentPatch)
}
複製代碼

3.1 結果預想

我們要的最終結果,大概是舊節點根據patches來變成新節點,最終結果的基本雛形:

let el =  render(vnode)//老的虛擬dom樹生成老html節點
document.body.appendChild(el) //掛載dom節點
let patches =  diff(vnode,newvnode) //對虛擬dom進行diff得到差異表
update (el, patches) //老節點根據差異表更新,這個函數包括了dom操作
複製代碼

3.2 深度優先搜索

我們現在要開始完善dfs內部的邏輯

考慮幾種情況:

  1. 兩個節點類型一樣,那我們應該對比他的屬性和子節點(ATTR)
  2. 兩個節點類型不一樣,我們把他視爲被替換(REPLACE)
  3. 兩個節點都是文本節點,直接用等號比吧(TEXT)
  4. 節點被刪除(DELETE)
function dfswalk (oldNode, newNode, index, patches) {
	const currentPatch = []
	if(!newNode){//判斷節點是否被刪除,記錄被刪的index
		currentPatch.push({type: 'REMOVE',index})
	}else if(typeof oldNode === 'string' && typeof newNode === 'string'){//處理文本節點
		if(oldNode !== newNode){
			currentPatch.push({type: 'TEXT',text:newNode})
		}
	}else if(oldNode.type === newNode.type){//如果節點類型相同
		//對比屬性
		let patch = props_diff(oldNode.props, newNode.props)
		//如果屬性有差異則寫入當前的差異數組
		Object.keys(patch).length && (currentPatch.push({type: 'ATTR',patch}))
		//對比子節點
		children_diff(oldNode.children, newNode.children, index, patches)
	}else{//節點類型不同
		currentPatch.push({type: 'REPLACE',newNode})
	}
	//將當前差異數組寫入差異表
	currentPatch.length && (patches[index] = currentPatch)
}
複製代碼

對比屬性:

我們傳入新節點和老節點的屬性集合,進行遍歷

function props_diff(oldProp, newProp){
	const patch = {}
	//判斷新老屬性的差別
	for(let k in oldProp){
		//如果屬性不同,寫入patch,老屬性有,新屬性沒有或者不同,寫入差異表
		oldProp[k] !== newProp[k] && (patch[k] = newProp[k])
	}
	//新節點新屬性
	for(let k in newProp){
		//判斷老節點的屬性在新節點裏面是否存在,沒有就寫入patch
		!oldProp.hasOwnProperty(k) && (patch[k] = newProp[k])
	}	
	return patch
}
複製代碼

對比子節點:

let allIndex = 0
function children_diff (oldChildren, newChildren, index, patches) {
	//對每一個子節點深度優先遍歷
	oldChildren&&oldChildren.forEach((child,i)=>{
		//allIndex在每一次進dfs的時候要加一,作爲唯一key。注意這個是全局的、共有的allIndex,表示節點樹的哪一個節點,0是根節點,子節點再走一遍dfs
		dfswalk(child, newChildren[i], ++allIndex, patches)
	})
}
複製代碼

4. 更新

前面我們已經大概構思了一個最終雛形:update (el, patches),我們順着這條路開始吧

let allPatches //全局存放差異表
//這裏是真的html元素喔,接下來是dom操作了
function update (HTMLNode, patches) {//根據差異表更新html元素,vnode轉換爲真正的節點
	allPatches = patches
	htmlwalk(HTMLNode)//遍歷節點,最開始從第一個節點遍歷
}
複製代碼
let Index = 0//索引從第一個節點開始,同上面的allIndex一樣的道理,全局標記
function htmlwalk (HTMLNode) {
	const currentPatch = allPatches[Index++]//遍歷一個節點,就下一個節點
	const childNodes = HTMLNode.childNodes
	//有子節點就後序深度優先遍歷
	childNodes && childNodes.forEach(node=>{
		htmlwalk (node)
	})
	//對當前的差異數組進行遍歷,根據差異還原元素
	currentPatch && currentPatch.length && currentPatch.forEach(patch=>{
		doPatch(HTMLNode, patch)//根據差異還原
	})
}
複製代碼

差異還原:

function doPatch (node, patch) {//還原過程,其實就是dom操作
	switch (patch.type) {
		case 'REMOVE' ://熟悉的刪除節點操作
		node.parentNode.removeChild(node)
		break
		case 'TEXT' ://熟悉的textContent
		node.textContent = patch.text
		break
		case 'ATTR' :
		for(let k in patch.patch){//熟悉的setAttribute
			const v = patch.patch[k]
			if(v){
				node.setAttribute(k, v)
			}else{
				node.removeAttribute(k)
			}
		}
		break
		case 'REPLACE' ://如果是元素節點,用render渲染出來替換掉。如果是文本,自己新建一個
		const newNode = (patch.newNode instanceof Element) ?
		render(patch.newNode) : document.createTextNode(patch.newNode)
		node.parentNode.replaceChild(newNode, node)
		break						
	}
}
複製代碼

5. 完成

已經完成了,我們試一下吧:

//隨便命名的,就別計較了
//創建虛擬dom
var v = createElement('ul',{class:'ul'},[
		createElement('li',{class:'li'},['a']),
		createElement('li',{class:'li1'},['b']),
		createElement('li',{class:'a'},['c'])
	])
//dom diff
var d = diff(v,createElement('ul',{class:'ul'},[
		createElement('li',{class:'li'},['aaaaaaaaaaa']),
		createElement('div',{class:'li'},['b']),
                createElement('li',{class:'li'},['b'])
	]))
//vnode渲染成真正的dom
var el =  render(v)
//掛載dom
document.body.appendChild(el)
//diff後更新dom
update (el, d)
複製代碼
全部代碼:(希望大家別來這裏複製,一步步看下來自己做一遍是最好的)
function Element(type, props, children) {
		this.type = type
		this.props = props
		this.children = children || []
}

function createElement (type, props, children) {
	return new Element(type, props, children)
}
//將vnode轉化爲真正的dom
function render (vnode) {
	let el = document.createElement(vnode.type)
	for(let key in vnode.props){
		el.setAttribute(key, vnode.props[key])
	}
	vnode.children&&vnode.children.forEach(child=>{//遞歸節點,如果是文本節點則直接插入
			child = (child instanceof Element) ? 
			render(child):
			document.createTextNode(child)
			el.appendChild(child)
		})
	return el
}
let allIndex = 0

function  diff (oldTree, newTree) {
	const patches = {}//差異表記錄差異
	let index = 0//記錄開始索引
	dfswalk(oldTree, newTree, index, patches)//先序深度優先遍歷
	return patches
}

function dfswalk (oldNode, newNode, index, patches) {
	const currentPatch = []
	if(!newNode){//判斷節點是否被刪除,記錄被刪的index
		currentPatch.push({type: 'REMOVE',index})
	}else if(typeof oldNode === 'string' && typeof newNode === 'string'){//處理文本節點
		if(oldNode !== newNode){
			currentPatch.push({type: 'TEXT',text:newNode})
		}
	}else if(oldNode.type === newNode.type){//如果節點類型相同
		//對比屬性
		let patch = props_diff(oldNode.props, newNode.props)
		//如果屬性有差異則寫入當前的差異數組
		Object.keys(patch).length && (currentPatch.push({type: 'ATTR',patch}))
		//對比子節點
		children_diff(oldNode.children, newNode.children, index, patches)
	}else{//節點類型不同
		currentPatch.push({type: 'REPLACE',newNode})
	}
	//將當前差異數組寫入差異表
	currentPatch.length && (patches[index] = currentPatch)
}

function children_diff (oldChildren, newChildren, index, patches) {
	//對每一個子節點深度優先遍歷
	oldChildren.forEach((child,i)=>{
		//index在每一次進dfs的時候要加一,作爲唯一key
		dfswalk(child, newChildren[i], ++allIndex, patches)
	})
}

function props_diff(oldProp, newProp){
	const patch = {}
	//判斷新老屬性的差別
	for(let k in oldProp){
		//如果屬性不同,寫入patch
		oldProp[k] !== newProp[k] && (patch[k] = newProp[k])
	}
	//新節點新屬性
	for(let k in newProp){
		//判斷老節點的屬性在新節點裏面是否存在,沒有就寫入patch
		!oldProp.hasOwnProperty(k) && (patch[k] = newProp[k])
	}	
	return patch
}

let allPatches//根據差異還原dom,記錄差異表
let Index = 0//索引從第一個節點開始
function update (HTMLNode, patches) {//根據差異表更新html元素,vnode轉換爲真正的節點
	allPatches = patches
	htmlwalk(HTMLNode)//遍歷節點,最開始從第一個節點遍歷
}

function htmlwalk (HTMLNode) {
	const currentPatch = allPatches[Index++]//遍歷一個節點,就下一個節點
	const childNodes = HTMLNode.childNodes
	//有子節點就後序dfs
	childNodes && childNodes.forEach(node=>{
		htmlwalk (node)
	})
	//對當前的差異數組進行遍歷,根據差異還原元素
	currentPatch && currentPatch.length && currentPatch.forEach(patch=>{
		doPatch(HTMLNode, patch)
	})
}

function doPatch (node, patch) {
	switch (patch.type) {
		case 'REMOVE' :
		node.parentNode.removeChild(node)
		break
		case 'TEXT' :
		node.textContent = patch.text
		break
		case 'ATTR' :
		for(let k in patch.patch){
			const v = patch.patch[k]
			if(v){
				node.setAttribute(k, v)
			}else{
				node.removeAttribute(k)
			}
		}
		break
		case 'REPLACE' :
		const newNode = (patch.newNode instanceof Element) ?
		render(patch.newNode) : document.createTextNode(patch.newNode)
		node.parentNode.replaceChild(newNode, node)
		break						
	}
}
複製代碼

過程差不多是這樣子的。我寫的有很多bug,別吐槽了,我懂,以後會更新的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章