「孩子,你會唱diff算法嗎」javascript
「twinkle,twinkle,diff start」html
先介紹一下虛擬dom的數據結構,咱們都知道源碼裏面有createElement函數,經過他建立虛擬dom,而後調用render函數。還記得VUE腳手架住入口文件那句足夠裝逼的h=>h(App)
嗎,其實就是相似createElement(App)這樣子的過程。咱們看一下他簡單的結構:vue
createElement('ul',{class:'ul'},[
createElement('li',{class:'li'},['1']),
createElement('li',{class:'li'},['2'])
])
複製代碼
createElement (type, props, children)傳入三個參數,節點類型、屬性集合、子節點集合java
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)
}
複製代碼
複製代碼,本身造兩個節點打印一下,在控制檯觀察一下。node
這個就是把虛擬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)
,看見新節點沒數組
都虛擬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)
}
複製代碼
咱們要的最終結果,大概是舊節點根據patches來變成新節點,最終結果的基本雛形:app
let el = render(vnode)//老的虛擬dom樹生成老html節點
document.body.appendChild(el) //掛載dom節點
let patches = diff(vnode,newvnode) //對虛擬dom進行diff獲得差別表
update (el, patches) //老節點根據差別表更新,這個函數包括了dom操做
複製代碼
咱們如今要開始完善dfs內部的邏輯dom
考慮幾種狀況:
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)
})
}
複製代碼
前面咱們已經大概構思了一個最終雛形: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
}
}
複製代碼
已經完成了,咱們試一下吧:
//隨便命名的,就別計較了
//建立虛擬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,別吐槽了,我懂,之後會更新的