本文主要介紹了vue的diff的簡(jiǎn)易實(shí)現(xiàn)過(guò)程丐怯,也就是兩個(gè)虛擬父節(jié)點(diǎn)都是同層級(jí)的喷好,且都不包含key屬性,當(dāng)然文章后面也會(huì)介紹帶有key屬性時(shí)读跷,diff的計(jì)算過(guò)程梗搅。
如果對(duì)簡(jiǎn)易實(shí)現(xiàn)代碼的h函數(shù)以及mount函數(shù)不懂的,可以去看我的另一篇文章:http://www.reibang.com/p/0cfca7d005cf
vue的diff的簡(jiǎn)易實(shí)現(xiàn)過(guò)程(patch函數(shù)):
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.clickDiv {
width: 20px;
height: 20px;
border: 1px solid #000;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="./render.js"></script>
<script>
let counter = 1
// compiler編譯template后的結(jié)果
const vnode = h("div", {class: 'black'}, [
h("button", {onclick: function() {counter++; console.log(counter)}} , '+1'),
h("h2", null, counter)
])
mount(vnode, document.querySelector('#app'))
setTimeout(() => {
const vnode1 = h("div", {class: 'black'}, [
h("div", {onclick: function() {counter--; console.log(counter)}, class: 'clickDiv'} , '-1'),
h("h2", null, '呵呵呵'),
])
patch(vnode, vnode1)
}, 2000)
</script>
</body>
</html>
// h函數(shù)的作用就是將compiler編譯后的模板轉(zhuǎn)為vnode(也就是js對(duì)象)
function h(tag, property, children) {
return {
tag,
property,
children
}
}
// 虛擬DOM轉(zhuǎn)為真實(shí)DOM
function mount(vnode, container) {
// 1. 將tag轉(zhuǎn)為標(biāo)簽
const el = vnode.el = document.createElement(vnode.tag)
// 2. 給標(biāo)簽設(shè)置對(duì)應(yīng)的屬性
if (vnode.property) {
for (const key in vnode.property) {
const value = vnode.property[key]
// 點(diǎn)擊事件
if (key.startsWith("on")) {
el.addEventListener(key.slice(2), value)
console.log(el.click)
} else {
el.setAttribute(key, value)
}
}
}
// 3. 處理children
if (vnode.children) {
if (typeof vnode.children === 'string' || typeof vnode.children === 'number') {
el.textContent = vnode.children
} else {
vnode.children.forEach(item => {
mount(item, el)
});
}
}
// 4. 將節(jié)點(diǎn)掛載到父節(jié)點(diǎn)上
container.appendChild(el)
}
// diff算法(最簡(jiǎn)易實(shí)現(xiàn)效览,同層級(jí)无切,不包含key屬性的節(jié)點(diǎn))
// vnode1是oldVNode, vnode2是newVNode
const patch = (vnode1, vnode2) => {
// 判斷是否是同一種標(biāo)簽
if (vnode1.tag !== vnode2.tag) {
// 移除oldVNode,添加newVNode
const elParentEl = vnode1.el.parentElement
elParentEl.removeChild(vnode1.el)
mount(vnode2, elParentEl)
} else {
// el是引用钦铺,在修改el時(shí),同時(shí)修改oldVNode,newVNode(目的是:在oldVNode上直接實(shí)現(xiàn)DOM更新)
// el就是最終需要的結(jié)果
const el = vnode2.el = vnode1.el
// 處理newVNode property(標(biāo)簽的屬性)订雾,給el添加newVNode的屬性
for (const key in vnode2.property) {
const newValue = vnode2.property[key]
const oldValue = vnode1.property[key]
if (newValue !== oldValue) {
// 對(duì)事件屬性做單獨(dú)處理
if (key.startsWith("on")) {
el.addEventListener(key.slice(2), vnode2.property[key])
} else {
el.setAttribute(key, newValue)
}
}
}
// 處理oldVNode property(標(biāo)簽的屬性),給el移除oldVNode的屬性
for (const key in vnode1.property) {
// 對(duì)事件屬性做單獨(dú)處理
if (key.startsWith("on")) {
el.removeEventListener(key.slice(2), vnode2.property[key])
}
if (!(key in vnode2.property)) {
el.removeAttribute(key)
}
}
// 處理children
// 如果newVNode的children是string或者number類(lèi)型
if (typeof vnode2.children === 'string' || typeof vnode1.children === 'number') {
el.innerHTML = vnode2.children
} else {
// 如果newVNode的children是array類(lèi)型
/**
* 這兒就實(shí)現(xiàn)一種最最簡(jiǎn)單的情況:
* vnode不帶有key屬性
*/
// newVNode.length = oldVNode.length
// 對(duì)子節(jié)點(diǎn)進(jìn)行diff
const commonLength = Math.min(vnode1.children.length, vnode2.children.length)
for (let i = 0; i < commonLength; i++) {
patch(vnode1.children[i], vnode2.children[i])
}
// newVNode.length > oldVNode.length
// 將newVNode多出來(lái)的節(jié)點(diǎn)掛載到el上
if (vnode2.children.length > vnode1.children.length) {
const newChildren = vnode2.children.slice(commonLength)
newChildren.forEach(item => {
mount(item, el)
})
}
// newVNode.length < oldVNode.length
// 將oldVNode多出來(lái)的節(jié)點(diǎn)從el上移除
if (vnode2.children.length < vnode1.children.length) {
const oldChildren = vnode1.children.slice(commonLength)
oldChildren.forEach(item => {
el.remove(item.el)
})
}
}
}
}
當(dāng)節(jié)點(diǎn)帶有key屬性時(shí)的diff:
新的VNodes和舊的VNodes對(duì)比使用diff算法:diff算法中有一個(gè)patch函數(shù)矛洞,用來(lái)對(duì)比新舊VNode洼哎,比較只會(huì)在同層級(jí)進(jìn)行, 不會(huì)跨層級(jí)比較讶请。
A B C D 新
A B F C D 舊
對(duì)比過(guò)程:
先對(duì)新舊VNode長(zhǎng)度進(jìn)行比較备徐,選擇較短的VNode進(jìn)行遍歷(while)桩砰,在遍歷過(guò)程中祭衩,先正序遍歷负懦,對(duì)相同的節(jié)點(diǎn)(patchFlag標(biāo)記(在編譯時(shí)加上標(biāo)記)和有key的情況下)進(jìn)行比較登疗,然后決定哪些內(nèi)容進(jìn)行替換蹂安,新增凳干,刪除等辫红; 當(dāng)遇到節(jié)點(diǎn)不同時(shí)(比如C F)凭涂,break跳出循環(huán);再進(jìn)行倒序遍歷贴妻,內(nèi)容同正序一樣切油。然后,如果是舊節(jié)點(diǎn)多出了VNode就進(jìn)行unmount(刪除)操作名惩,如果是新Vnode多出了就進(jìn)行mount(新增掛載)操作澎胡。如果中間是亂序,則盡可能地在舊的VNode中找到對(duì)應(yīng)的新的VNode娩鹉,再建立一個(gè)數(shù)組攻谁,然后將舊的Vnode放在與新的Vnode對(duì)應(yīng)的位置上。然后舊的Vnode多余的就進(jìn)行unmount操作弯予,新的VNode多余的就進(jìn)行mount操作戚宦。
在編譯時(shí),會(huì)對(duì)節(jié)點(diǎn)進(jìn)行加上標(biāo)記锈嫩,對(duì)于靜態(tài)節(jié)點(diǎn)(簡(jiǎn)單來(lái)說(shuō)阁苞,就是沒(méi)有變量困檩,不會(huì)動(dòng)態(tài)變化的節(jié)點(diǎn))會(huì)直接跳過(guò)。
如有錯(cuò)誤那槽,歡迎指正悼沿!