Vue原始碼學習(十六):diff演算法(三)暴力比對

養肥胖虎發表於2023-11-10

好傢伙,這是diff的最後一節了

 

0.暴力比對的使用場景 

沒有可複用的節點:當新舊虛擬 DOM 的結構完全不同,或者某個節點不能被複用時,需要透過暴力比對來建立新的節點,並在真實 DOM 上進行相應的插入操作。

0.1.例子一:

// 建立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)

 

0.2.例子二:

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值是相同的

以上兩種情況上一張的方法

Vue原始碼學習(十五):diff演算法(二)交叉比對(雙指標)

無法處理

於是我們使用暴力比對

 

 

1.分析

來看邏輯圖:

1.1.情況一:

例子: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

 

1.2.情況二:

例子: c b a 與 f g b e比對

前兩步一致

但,到相同的元素b時有些許的不同

此處我們會引入一箇舊vnode的關係對映表

 

1.2.2.新vnode節點中的每一個子節點都將與這個對映表進行匹配,尋找相同的元素

 

 1.2.3.繼續

 

 

 

將舊vnode中的"相同節點"打上一個"刪去標記"

後續步驟同情況一

 

2.程式碼實現

2.1.建立對映表

    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 中的索引值。

這對於減少比對時間和最佳化更新效能非常有幫助,尤其在大型應用程式或具有複雜資料結構的頁面中。

 

2.2.暴力比對演算法

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 開始位置元素之前。

 

2.3.刪除舊vnode中節點

//將老的多餘的元素刪去
    if (oldStartIndex <= oldEndIndex) {
        for (let i = oldStartIndex; i <= oldEndIndex; i++) {
            //注意null
            let child = oldChildren[i]
            if(child !=null ){
                parent.removeChild(child.el) //刪除元素
            }
        }
    }

 

3.patch.js完整程式碼

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
}

 

相關文章