好傢伙,這是diff的最後一節了
沒有可複用的節點:當新舊虛擬 DOM 的結構完全不同,或者某個節點不能被複用時,需要通過暴力比對來建立新的節點,並在真實 DOM 上進行相應的插入操作。
// 建立vnode
let vm1 = new Vue({
data: {
name: '張三'
}
})
let render1 = compileToFunction(`<ul>
<li style="background:red" key="c">c</li>
<li style="background:pink" key="b">b</li>
<li style="background:blue" key="a">a</li>
</ul>`)
let vnode1 = render1.call(vm1)
document.body.appendChild(createELm(vnode1))
//資料更新
let vm2 = new Vue({
data: {
name: '李四'
}
})
let render2 = compileToFunction(`<ul>
<li style="background:red" key="f">f</li>
<li style="background:pink" key="g">g</li>
<li style="background:blue" key="e">e</li>
</ul>`)
let vnode2 = render2.call(vm2)
setTimeout(() => {
patch(vnode1, vnode2)
}, 2000)
let vm1 = new Vue({
data: {
name: '張三'
}
})
let render1 = compileToFunction(`<ul>
<li style="background:red" key="c">c</li>
<li style="background:pink" key="b">b</li>
<li style="background:blue" key="a">a</li>
</ul>`)
let vnode1 = render1.call(vm1)
document.body.appendChild(createELm(vnode1))
//資料更新
let vm2 = new Vue({
data: {
name: '李四'
}
})
let render2 = compileToFunction(`<ul>
<li style="background:red" key="f">f</li>
<li style="background:pink" key="g">g</li>
<li style="background:pink" key="b">b</li>
<li style="background:blue" key="e">e</li>
</ul>`)
let vnode2 = render2.call(vm2)
setTimeout(() => {
patch(vnode1, vnode2)
}, 2000)
依舊是這個例子,但我們分開兩種情況討論
情況一:render1和render2中沒有相同的key值
情況二:render1和render2中只有一個節點的key值是相同的
以上兩種情況上一張的方法
無法處理
於是我們使用暴力比對
來看邏輯圖:
例子:c b a 與 f g e 比對
1.1.1比對新舊vnode中的首個節點
不匹配,將新vnode中的元素新增到舊vnode首個元素前
1.1.2.新vnode指標++
新增邏輯同上,依舊是新增到新增到舊vnode首個元素前
1.1.3.新vnode指標++
新增邏輯同上,依舊是新增到新增到舊vnode首個元素前
1.1.4.匹配完成,刪除舊vnode
例子: c b a 與 f g b e比對
前兩步一致
但,到相同的元素b時有些許的不同
此處我們會引入一箇舊vnode的關係對映表
1.2.2.新vnode節點中的每一個子節點都將與這個對映表進行匹配,尋找相同的元素
1.2.3.繼續
將舊vnode中的"相同節點"打上一個"刪去標記"
後續步驟同情況一
function makeIndexBykey(child){
let map = {}
child.forEach((item,index)=>{
if(item.key){
map[item.key] =index
}
})
return map
}
//建立對映表
let map =makeIndexBykey(oldChildren)
在暴力比對(也稱為全量更新)期間,舊 vnode
對映表的作用是儲存舊 vnode
子元素的鍵值對(key-value pairs)。
這個對映表的目的是在新舊 vnode
的比對中,可以通過鍵(key)快速查詢舊 vnode
中對應的索引位置。
在給定的程式碼中,makeIndexBykey
函數接收一個 child
陣列作為輸入引數,遍歷每個 child
元素,並且如果該元素存在 key
屬性,
則將其在 child
陣列中的索引值儲存到 map
物件中,以 item.key
作為鍵,index
作為值。
這樣做的目的是為了在後續的比對過程中,可以通過 key
值快速找到舊 vnode
中對應的索引值。
通過查詢 map
物件,可以在遇到新的 vnode
元素時,快速判斷是否存在對應的 key
值,並且獲取舊 vnode
中的索引值。
這對於減少比對時間和優化更新效能非常有幫助,尤其在大型應用程式或具有複雜資料結構的頁面中。
console.log(5)
//1 建立 舊元素對映表
//2 從舊的中尋找新的中有的元素
let moveIndex = map[newStartVnode.key]
//沒有相應key值的元素
if(moveIndex == undefined){
parent.insertBefore(createELm(newStartVnode),oldStartVnode.el)
}//有
else{
let moveVnode = oldChildren[moveIndex] //獲取到有的元素
oldChildren[moveIndex]=null
//a b f c 和 d f e
parent.insertBefore(moveVnode.el,oldStartVnode.el)
patch(moveVnode,newEndVnode)
}
newStartVnode = newChildren[++newStartIndex]
--1--如果舊 vnode
中不存在相同鍵(key)的元素,即 moveIndex
為 undefined
,則說明這是一個新元素,需要將新元素插入到舊 vnode
開始位置元素之前。
這裡呼叫 createELm(newStartVnode)
建立新元素的 DOM 節點,並通過 parent.insertBefore
方法將其插入到舊 vnode
開始位置元素之前。
--2--如果舊 vnode
中存在相同鍵(key)的元素,則說明這是一個相同元素(一個需要移動的元素,事實上,程式碼的邏輯為,將在舊vnode中該"相同元素"移動)
通過 let moveVnode = oldChildren[moveIndex]
將該元素賦值給 moveVnode
。然後將 oldChildren[moveIndex]
設為 null
,標記該元素已經被處理。
然後,通過 parent.insertBefore(moveVnode.el, oldStartVnode.el)
,將該元素的 DOM 節點插入到舊 vnode
開始位置元素之前。
//將老的多餘的元素刪去
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
//注意null
let child = oldChildren[i]
if(child !=null ){
parent.removeChild(child.el) //刪除元素
}
}
}
export function patch(oldVnode, Vnode) {
//原則 將虛擬節點轉換成真實的節點
console.log(oldVnode, Vnode)
// console.log(oldVnode.nodeType)
// console.log(Vnode.nodeType)
//第一次渲染 oldVnode 是一個真實的DOM
//判斷ldVnode.nodeType是否為一,意思就是判斷oldVnode是否為屬性節點
if (oldVnode.nodeType === 1) {
console.log(oldVnode, Vnode) //注意oldVnode 需要在載入 mount 新增上去 vm.$el= el
let el = createELm(Vnode) // 產生一個新的DOM
let parentElm = oldVnode.parentNode //獲取老元素(app) 父親 ,body
// console.log(oldVnode)
// console.log(parentElm)
parentElm.insertBefore(el, oldVnode.nextSibling) //當前真實的元素插入到app 的後面
parentElm.removeChild(oldVnode) //刪除老節點
//重新賦值
return el
} else { // diff
// console.log(oldVnode.nodeType)
// console.log(oldVnode, Vnode)
//1 元素不是一樣
if (oldVnode.tag !== Vnode.tag) {
//舊的元素 直接替換為新的元素
return oldVnode.el.parentNode.replaceChild(createELm(Vnode), oldVnode.el)
}
//2 標籤一樣 text 屬性 <div>1</div> <div>2</div> tag:undefined
if (!oldVnode.tag) {
if (oldVnode.text !== Vnode.text) {
return oldVnode.el.textContent = Vnode.text
}
}
//2.1屬性 (標籤一樣) <div id='a'>1</div> <div style>2</div>
//在updataRpors方法中處理
//方法 1直接複製
let el = Vnode.el = oldVnode.el
updataRpors(Vnode, oldVnode.data)
//diff子元素 <div>1</div> <div></div>
console.log(oldVnode,Vnode)
let oldChildren = oldVnode.children || []
let newChildren = Vnode.children || []
if (oldChildren.length > 0 && newChildren.length > 0) { //老的有兒子 新有兒子
//建立方法
updataChild(oldChildren, newChildren, el)
} else if (oldChildren.length > 0 && newChildren.length <= 0) { //老的元素 有兒子 新的沒有兒子
el.innerHTML = ''
} else if (newChildren.length > 0 && oldChildren.length <= 0) { //老沒有兒子 新的有兒子
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i]
//新增到真實DOM
el.appendChild(createELm(child))
}
}
}
}
function updataChild(oldChildren, newChildren, parent) {
//diff演演算法 做了很多優化 例子<div>11</div> 更新為 <div>22</div>
//dom中操作元素 常用的 思想 尾部新增 頭部新增 倒敘和正序的方式
//雙指標 遍歷
console.log(oldChildren, newChildren)
let oldStartIndex = 0 //老的開頭索引
let oldStartVnode = oldChildren[oldStartIndex];
let oldEndIndex = oldChildren.length - 1
let oldEndVnode = oldChildren[oldEndIndex]
let newStartIndex = 0 //新的開頭索引
let newStartVnode = newChildren[newStartIndex];
let newEndIndex = newChildren.length - 1
let newEndVnode = newChildren[newEndIndex]
// console.log(oldEndIndex,newEndIndex)
// console.log(oldEndVnode,newEndVnode)
function makeIndexBykey(child){
let map = {}
child.forEach((item,index)=>{
if(item.key){
map[item.key] =index
}
})
return map
}
//建立對映表
let map =makeIndexBykey(oldChildren)
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
//比對子元素
console.log(666)
if (isSomeVnode(oldStartVnode, newStartVnode)) {
//遞迴
//1 從頭部開始
console.log(1)
patch(oldStartVnode, newStartVnode);
//移動指標
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
console.log(oldStartVnode,newStartVnode)
}//2 從尾部開始
else if(isSomeVnode(oldEndVnode, newEndVnode)){
//
console.log(2)
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
}//3 交叉比對 從頭
else if(isSomeVnode(oldStartVnode,newEndVnode)){
console.log(3)
patch(oldStartVnode, newEndVnode);
oldStartVnode =oldChildren[++oldStartIndex]
newEndVnode = newChildren[--newEndIndex];
}//4 交叉比對 從尾
else if(isSomeVnode(oldEndVnode,newStartVnode)){
console.log(4)
patch(oldEndVnode, newStartVnode);
oldEndVnode =oldChildren[--oldStartIndex]
newStartVnode = newChildren[++newStartIndex];
}//5 暴力比對 兒子之間沒有任何關係
else{
console.log(5)
//1 建立 舊元素對映表
//2 從舊的中尋找新的中有的元素
let moveIndex = map[newStartVnode.key]
//沒有相應key值的元素
if(moveIndex == undefined){
parent.insertBefore(createELm(newStartVnode),oldStartVnode.el)
}//有
else{
let moveVnode = oldChildren[moveIndex] //獲取到有的元素
oldChildren[moveIndex]=null
//a b f c 和 d f e
parent.insertBefore(moveVnode.el,oldStartVnode.el)
patch(moveVnode,newEndVnode)
}
newStartVnode = newChildren[++newStartIndex]
}
}
//判斷完畢,新增多餘的子兒子 a b c 新的 a b c d
console.log(newEndIndex)
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
parent.appendChild(createELm(newChildren[i]))
}
}
//將老的多餘的元素刪去
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
//注意null
let child = oldChildren[i]
if(child !=null ){
parent.removeChild(child.el) //刪除元素
}
}
}
}
function isSomeVnode(oldContext, newContext) {
// return true
return (oldContext.tag == newContext.tag) && (oldContext.key === newContext.key);
}
//新增屬性
function updataRpors(vnode, oldProps = {}) { //第一次
let newProps = vnode.data || {} //獲取當前新節點 的屬性
let el = vnode.el //獲取當前真實節點 {}
//1老的有屬性,新沒有屬性
for (let key in oldProps) {
if (!newProps[key]) {
//刪除屬性
el.removeAttribute[key] //
}
}
//2演示 老的 style={color:red} 新的 style="{background:red}"
let newStyle = newProps.style || {} //獲取新的樣式
let oldStyle = oldProps.style || {} //老的
for (let key in oldStyle) {
if (!newStyle[key]) {
el.style = ''
}
}
//新的
for (let key in newProps) {
if (key === "style") {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName]
}
} else if (key === 'class') {
el.className = newProps.class
} else {
el.setAttribute(key, newProps[key])
}
}
}
//vnode 變成真實的Dom
export function createELm(vnode) {
let {
tag,
children,
key,
data,
text
} = vnode
//注意
if (typeof tag === 'string') { //建立元素 放到 vnode.el上
vnode.el = document.createElement(tag) //建立元素
updataRpors(vnode)
//有兒子
children.forEach(child => {
// 遞迴 兒子 將兒子渲染後的結果放到 父親中
vnode.el.appendChild(createELm(child))
})
} else { //文字
vnode.el = document.createTextNode(text)
}
return vnode.el //新的dom
}