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

2023-11-07 18:00:39

好傢伙,

 

本節來解決我們上一章留下來的問題,

新舊節點同時有兒子的情況本章繼續解決

 

1.要做什麼?

本章將解決,

1.在相同tag下子元素的替換問題

2.使用雙指標進行元素替換,

實現效果如下:

 

    let vm1 = new Vue({data:{name:'張三'}})
    let render1 = compileToFunction(`<ul>
    <li style="background:yellow" key="c">我是黃色</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:blue" key="c">我是藍色</li>
    </ul>`)
     let vnode2 = render2.call(vm2)

     //patch 比對
      setTimeout(()=>{
        patch(vnode1,vnode2)
      },2000)

 

2.思路

let vm1 = new Vue({
  data: {
    name: '張三'
  }
})
let render1 = compileToFunction(`<ul>
    <li style="background:red" key="a">a</li>
     <li style="background:pink" key="b">b</li>
     <li style="background:blue" key="c">c</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="a">a</li>
     <li style="background:pink" key="b">b</li>
     <li style="background:blue" key="c">c</li>
     <li style="background:yellow" key="d">d</li>
    </ul>`)
let vnode2 = render2.call(vm2)

setTimeout(() => {
  patch(vnode1, vnode2)
}, 2000)

我們用這個例子來舉例

 

1.正序(從頭開始)

 

 

 

找到不同(原先沒有的)的項,再將它新增上去

大概的思路就是如此.

 

但同時,根據不同的情況

我們還有多種比對方法

2.2.逆序

 

2.3.交叉對比(從頭)

 

2.4.交叉對比(從尾)

 

 

 

3.程式碼實現

3.1.雙指標

//雙指標 遍歷
    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]

雙指標的寫法非常粗暴,但是好用

 

3.2.迴圈

(照著上面的圖看)

while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        //比對子元素
        console.log(666)
        if (isSomeVnode(oldStartVnode, newStartVnode)) {
            //遞迴
            debugger;
            //1 從頭部開始
            patch(oldStartVnode, newStartVnode);
            //移動指標
            oldStartVnode = oldChildren[++oldStartIndex];
            newStartVnode = oldChildren[++newStartIndex];
        }//2 從尾部開始
        else if(isSomeVnode(oldEndVnode, newEndVnode)){
            //
            patch(oldEndVnode, newEndVnode);
            oldEndVnode = oldChildren[--oldEndIndex]
            newEndVnode = newChildren[--newEndIndex]
        }//3 交叉比對 從頭
        else if(isSomeVnode(oldStartVnode,newEndVnode)){
            patch(oldStartVnode, newEndVnode);
            oldStartVnode =oldChildren[++oldStartIndex]
            newEndVnode = newChildren[--newEndIndex];
        }//4 交叉比對 從尾
        else if(isSomeVnode(oldEndVnode,newStartVnode)){
            patch(oldEndVnode, newStartVnode);
            oldEndVnode =oldChildren[--oldStartIndex]
            newStartVnode = newChildren[++newStartIndex];
        }
    }

 

3.3.isSomeVnode()

isSomeVnode()方法用於判斷兩個節點是否相同

function isSomeVnode(oldContext, newContext) {
    // return true
    return (oldContext.tag == newContext.tag) && (oldContext.key === newContext.key);
}  

 

3.4.新增多餘的子兒子

//判斷完畢,新增多餘的子兒子  例子:舊的a b c  新的 a b c d  將d新增到parent
    if (newStartIndex <= newEndIndex) {
        for (let i = newStartIndex; i <= newEndIndex; i++) {
            
            parent.appendChild(createELm(newChildren[i]))
        }
    }

 

搞定

 

4.patch.js完整程式碼

如下:

export function patch(oldVnode, Vnode) {
    debugger;

    //原則  將虛擬節點轉換成真實的節點
    // 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>
        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中操作元素 常用的 思想 尾部新增 頭部新增 倒敘和正序的方式

    //雙指標 遍歷
    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)
    
    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        //比對子元素
        console.log(666)
        if (isSomeVnode(oldStartVnode, newStartVnode)) {
            //遞迴
            debugger;
            //1 從頭部開始
            patch(oldStartVnode, newStartVnode);
            //移動指標
            oldStartVnode = oldChildren[++oldStartIndex];
            newStartVnode = oldChildren[++newStartIndex];
        }//2 從尾部開始
        else if(isSomeVnode(oldEndVnode, newEndVnode)){
            //
            patch(oldEndVnode, newEndVnode);
            oldEndVnode = oldChildren[--oldEndIndex]
            newEndVnode = newChildren[--newEndIndex]
        }//3 交叉比對 從頭
        else if(isSomeVnode(oldStartVnode,newEndVnode)){
            patch(oldStartVnode, newEndVnode);
            oldStartVnode =oldChildren[++oldStartIndex]
            newEndVnode = newChildren[--newEndIndex];
        }//4 交叉比對 從尾
        else if(isSomeVnode(oldEndVnode,newStartVnode)){
            patch(oldEndVnode, newStartVnode);
            oldEndVnode =oldChildren[--oldStartIndex]
            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]))
        }
    }
}
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
}