Vue原始碼學習(十四):diff演演算法patch比對

2023-11-03 21:08:19

好傢伙,

本篇將會解釋要以下效果的實現

 

1.目標

我們要實現以下元素替換的效果

gif:

 

以上例子的程式碼:

    //建立vnode
    let vm1 = new Vue({data:{name:'張三'}})
    let render1 = compileToFunction(`<a>{{name}}</a>`)
    let vnode1 = render1.call(vm1)
     document.body.appendChild(createELm(vnode1))

   //資料更新
     let vm2 = new Vue({data:{name:'李四'}})
     let render2 = compileToFunction(`<div>{{name}}</div>`)
     let vnode2 = render2.call(vm2)
   //屬性新增
     let vm3 = new Vue({data:{name:'李四'}})
     let render3 = compileToFunction(`<div style="color:red">{{name}}</div>`)
     let vnode3 = render3.call(vm3)
    
     //patch 比對
      setTimeout(()=>{
        patch(vnode1,vnode2)
      },2000)

      setTimeout(()=>{
        patch(vnode2,vnode3)
      },3000)

 

以上例子中compileToFunction()方法的詳細解釋

Vue原始碼學習(四):<templete>渲染第三步,將ast語法樹轉換為渲染函數

一句話解釋,這是一個將模板變為render函數的方法

 

開搞:

思路非常簡單,依舊是對不同的情況分類處理

 

2.程式碼解釋

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>
       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,ewChildren,el){

}
//新增屬性
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
}

 

三個方法,我們一個個看

2.1.patch()

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>
       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))
             }
       }
 
    }
}

patch()方法用於根據新的虛擬節點更新舊的虛擬節點以及對應的真實 DOM 元素。

 

首先判斷舊的虛擬節點是否是一個真實 DOM 元素(即是否為屬性節點),

--1--如果是,則表示這是第一次渲染,需要使用 createELm 函數建立新的 DOM 元素,並將其插入到舊的元素之前,最後再刪除舊的元素,返回新建立的元素。

--2--如果不是第一次渲染,則進行 diff 操作,

  --2.1--首先判斷新老節點的標籤是否相同,如果不同,則直接使用新的節點替換舊的節點。

  --2.2--如果標籤相同,則需要判斷節點的文字內容和屬性是否發生了變化,如果發生了變化,則通過 updataRpors 函數更新 DOM 元素屬性或文字內容。

  --2.3--最後,需要 diff 子元素

    --2.3.1--如果舊節點和新節點均有子元素,則需要將新舊子元素進行比較,通過 updataChild 函數更新舊節點的子元素與新節點的子元素。

    --2.3.2--如果舊節點有子元素而新節點沒有,則直接將舊節點的內容清空;

    --2.3.3--如果新節點有子元素而舊節點沒有,則直接將新節點的子元素新增到舊節點中。

 

2.2.updataRpors()

//新增屬性
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])
      }
  }
}
updataRpors()是一個更新屬性的方法,其主要功能是更新虛擬節點的屬性,包括刪除不再存在的屬性、更新樣式和類名等。

 

2.3.createELm()

//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
}


 createELm()是一個用於建立和渲染虛擬DOM的函數.

函數名稱為`createELm`,它接收一個引數`vnode`,這個引數是一個虛擬DOM節點物件。

這段程式碼的主要作用是根據傳入的虛擬DOM節點資料結構(`vnode`)建立一個相應的實際DOM元素,並返回該元素。

如果虛擬DOM節點包含子節點,它會遞迴地為每個子節點建立相應的DOM元素並新增到父節點的DOM元素中。