虛擬Dom詳解 - (二)

第一篇文章中主要講解了虛擬DOM基本實現,簡單的回顧一下,虛擬DOM是使用json數據描述的一段虛擬Node節點樹,經過render函數生成其真實DOM節點。並添加到其對應的元素容器中。在建立真實DOM節點的同時併爲其註冊事件並添加一些附屬屬性。javascript

虛擬Dom詳解 - (一)html

在上篇文章中也曾經提到過,當狀態變動的時候用修改後的新渲染的的JavaScript對象和舊的虛擬DOMJavaScript對象做對比,記錄着兩棵樹的差別,把差異反映到真實的DOM結構上最後操做真正的DOM的時候只操做有差別的部分的更改。然而上篇文章中也只是簡簡單單的提到過一句卻沒有進行實質性的實現,這篇文章主要講述一下虛擬DOM是如何作出更新的。那就開始吧...O(∩_∩)Ovue

在虛擬DOM中實現更新的話是使用DIFF算法進行更新的,我想大多數小夥伴都應該據說過這個詞,DIFF是整個虛擬DOM部分最核心的部分,由於當虛擬DOM節點狀態發生改變之後不可能去替換整個DOM節點樹,如果這樣的話會出現打兩個DOM操做,無非是對性能的極大影響,真的如此的話還不如直接操做DOM來的實際一些。java

第一篇文章中是經過render對虛擬DOM節點樹進行渲染的,可是在render函數中只作了一件事情,只是對虛擬DOM進行了新建也就是初始化工做,其實回過頭來想一下,不管是新建操做仍是修改操做,都應該經過render函數來作,在react中全部的DOM渲染都是經過其中的render函數完成的,那麼也就得出了這個結論。node

//  渲染虛擬DOM
//    虛擬DOM節點樹
//    承載DOM節點的容器,父元素
function render(vnode,container) {
  //  首次渲染
  mount(vnode,container);
};

既然更新和建立操做都是經過render函數來作的,在方法中又應該如何區分當前的操做究竟是新建仍是更新呢?畢竟在react咱們並無給出明確的標識來告訴其方法,當前是進行的哪一個操做。在執行render函數的時候有兩個參數,一個是傳入的vnode節點樹,還有一個就是承載真實DOM節點的容器,其實咱們能夠把其虛擬DOM節點樹掛載在其容器中,若容器中存在其節點樹則是更新操做,反之則是新建操做。react

//  渲染虛擬DOM
//    虛擬DOM節點樹
//    承載DOM節點的容器,父元素
function render(vnode, container) {

  if (!container.vnode) {
    //  首次渲染
    mount(vnode, container);
  } else {
    //  舊的虛擬DOM節點
    //  新的DOM節點
    //  承載DOM節點的容器
    patch(container.vnode, vnode, container);
  }
  container.vnode = vnode;
};

既然已經肯定了如今的render函數所須要進行的操做了,那麼接下來就應該進行下一步操做了,若是想要作更新的話必需要知道以下幾個參數,原有的虛擬DOM節點是什麼樣的,新的虛擬DOM又是什麼樣的,上一步操做中咱們已經把原有的虛擬DOM節點已經保存在了父容器中,直接使用便可。web

//  更新函數
//    舊的虛擬DOM節點
//    新的DOM節點
//    承載DOM節點的容器
function patch(oldVNode, newVNode, container) {
  //  新節點的VNode類型
  let newVNodeFlag = newVNode.flag;
  //  舊節點的VNode類型
  let oldVNodeFlag = oldVNode.flag;
  //  若是新節點與舊節點的類型不一致
  //  若是不一致的狀況下,至關於其節點發生了變化
  //  直接進行替換操做便可
  //  這裏判斷的是若是一個是 TEXT 一個是 Element
  //  類型判斷
  if (newVNodeFlag !== oldVNodeFlag) {
    replaceVNode(oldVNode, newVNode, container);
  }
  //  因爲在新建時建立Element和Text的時候使用的是兩個函數進行操做的
  //  在更新的時候也是同理的
  //  也應該針對不一樣的修改進行不一樣的操做
  //  若是新節點與舊節點的HTML相同
  else if (newVNodeFlag == vnodeTypes.HTML) {
    //  替換元素操做
    patchMethos.patchElement(oldVNode, newVNode, container);
  }
  //  若是新節點與舊節點的TEXT相同
  else if (newVNodeFlag == vnodeTypes.TEXT) {
    //  替換文本操做
    patchMethos.patchText(oldVNode, newVNode, container);
  }
}
//  更新VNode方法集
const patchMethos = {
    //  替換文本操做
    //    舊的虛擬DOM節點
    //    新的DOM節點
    //    承載DOM節點的容器
    patchText(oldVNode,newVNode,container){
        //  獲取到el,並將 oldVNode 賦值給 newVNode
        let el = (newVNode.el = oldVNode.el);
        //  若是 newVNode.children 不等於 oldVNode.children
        //  其餘狀況就是相等則沒有任何操做,不須要更新
        if(newVNode.children !== oldVNode.children){
            //  直接進行替換操做
            el.nodeValue = newVNode.children;
        }
    }
};
//  替換虛擬DOM
function replaceVNode(oldVNode, newVNode, container) {
  //  在原有節點中刪除舊節點
  container.removeChild(oldVNode.el);
  //  從新渲染新節點
  mount(newVNode, container);
}

上述方法簡單的實現了對Text更新的一個替換操做,因爲Text替換操做比較簡單,因此這裏就先實現,僅僅完成了對Text的更新是遠遠不夠的,當Element進行操做的時也是須要更新的。相對來講Text的更新要比Element更新要簡單不少的,Element更新比較複雜因此放到了後面,由於比較重要嘛,哈哈~算法

首先想要進行Element替換以前要肯定哪些Data數據進行了變動,而後才能對其進行替換操做,這樣的話須要肯定要更改的數據,而後替換掉原有數據,才能進行下一步更新操做。json

//  更新VNode方法集
const patchMethos = {
    //  替換元素操做
    //    舊的虛擬DOM節點
    //    新的DOM節點
    //    承載DOM節點的容器
    patchElement(oldVNode,newVNode,container){
        //  若是 newVNode 的標籤名稱與 oldVNode 標籤名稱不同
        //  既然標籤都不同則直接替換就行了,不須要再進行其餘多餘的操做
        if(newVNode.tag !== oldVNode.tag){
            replaceVNode(oldVNode,newVNode,container);
            return;
        }
        //  更新el
        let el = (newVNode.el = oldVNode.el);
        //  獲取舊的Data數據
        let oldData = oldVNode.data;
        //  獲取新的Data數據
        let newData = newVNode.data;
        //  若是新的Data數據存在
        //  進行更新和新增
        if(newData){
            for(let attr in newData){
                let oldVal = oldData[attr];
                let newVal = newData[attr];
                domAttributeMethod.patchData(el,attr,oldVal,newVal);
            }
        }
        //  若是舊的Data存在
        //  檢測更新
        if(oldData){
            for(let attr in oldData){
                let oldVal = oldData[attr];
                let newVal = newData[attr];
                //  若是舊數據存在,新數據中不存在
                //  則表示已刪除,須要進行更新操做
                if(oldVal && !newVal.hasOwnProperty(attr)){
                    //  既然新數據中不存在,則新數據則傳入Null
                    domAttributeMethod.patchData(el,attr,oldVal,null);
                }
            }
        }
    }
};
//  dom添加屬性方法
const domAttributeMethod = {
  //  修改Data數據方法
  patchData (el,key,prv,next){
    switch(key){
      case "style":
        this.setStyle(el,key,prv,next);
        //  添加了這裏,看我看我  (●'◡'●)
        //  添加遍歷循環
        //  循環舊的data
        this.setOldVal(el,key,prv,next);
        break;
      case "class":
        this.setClass(el,key,prv,next);
        break;
      default :
        this.defaultAttr(el,key,prv,next);
        break;
    }
  },
  //  遍歷舊數據
  setOldVal(el,key,prv,next){
    //  遍歷舊數據
    for(let attr in prv){
        //  若是舊數據存在,新數據中不存在
        if(!next.hasOwnProperty(attr)){
            //  直接賦值爲字符串
            el.style[attr] = "";
        }
    }
  },
  //  修改事件註冊方法
  addEvent(el,key,prev,next){
    //  添加了這裏,看我看我  (●'◡'●)
    //  prev 存在刪除原有事件,從新綁定新的事件
    if(prev){
      el.removeEventListener(key.slice(1),prev);
    }
    if(next){
      el.addEventListener(key.slice(1),next);
    }
  }
}

上面的操做其實只是替換Data部分,可是其子元素沒有進行替換,因此還須要對子元素進行替換處理。替換子元素有共分爲6種狀況:性能優化

  1. 舊元素只有一個
  2. 舊元素爲空
  3. 舊元素爲多個
  4. 新元素只有一個
  5. 新元素爲空
  6. 新元素爲多個
//  更新VNode方法集
const patchMethos = {
    //  替換元素操做
    //    舊的虛擬DOM節點
    //    新的DOM節點
    //    承載DOM節點的容器
    patchElement(oldVNode,newVNode,container){
        //  若是 newVNode 的標籤名稱與 oldVNode 標籤名稱不同
        //  既然標籤都不同則直接替換就行了,不須要再進行其餘多餘的操做
        if(newVNode.tag !== oldVNode.tag){
            replaceVNode(oldVNode,newVNode,container);
            return;
        }
        //  更新el
        let el = (newVNode.el = oldVNode.el);
        //  獲取舊的Data數據
        let oldData = oldVNode.data;
        //  獲取新的Data數據
        let newData = newVNode.data;
        //  若是新的Data數據存在
        //  進行更新和新增
        if(newData){
            for(let attr in newData){
                let oldVal = oldData[attr];
                let newVal = newData[attr];
                domAttributeMethod.patchData(el,attr,oldVal,newVal);
            }
        }
        //  若是舊的Data存在
        //  檢測更新
        if(oldData){
            for(let attr in oldData){
                let oldVal = oldData[attr];
                let newVal = newData[attr];
                //  若是舊數據存在,新數據中不存在
                //  則表示已刪除,須要進行更新操做
                if(oldVal && !newVal.hasOwnProperty(attr)){
                    //  既然新數據中不存在,則新數據則傳入Null
                    domAttributeMethod.patchData(el,attr,oldVal,null);
                }
            }
        }
        //  添加了這裏
        //  更新子元素
        //      舊子元素類型
        //      新子元素類型
        //      舊子元素的children
        //      新子元素的children
        //      el元素,容器
        this.patchChildren(
            oldVNode.childrenFlag,
            newVNode.childrenFlag,
            oldVNode.children,
            newVNode.children,
            el,
        );

    },
    //  更新子元素
    //      舊子元素類型
    //      新子元素類型
    //      舊子元素的children
    //      新子元素的children
    //      el元素,容器
    patchChildren(...arg){
        let [oldChildrenFlag,newChildrenFlag,oldChildren,newChildren,container] = arg;
        switch(oldChildrenFlag){
            //  若是舊元素的子元素爲一個
            case childTeyps.SINGLE:
                this.upChildSingle(...arg);
                break;
            //  若是舊元素的子元素爲空
            case childTeyps.EMPTY:
                this.upChildEmpty(...arg);
                break;
            //  若是舊元素的子元素爲多個
            case childTeyps.MULTIPLE:
                this.upChildMultiple(...arg);
                break;
        }
    },

    upChildSingle(...arg){
        let [oldChildrenFlag,newChildrenFlag,oldChildren,newChildren,container] = arg;
        //  循環新的子元素
        switch(newChildrenFlag){
            //  若是新元素的子元素爲一個
            case childTeyps.SINGLE:
                patch(oldChildren,newChildren,container);
                break;
            //  若是新元素的子元素爲空
            case childTeyps.EMPTY:
                container.removeChild(oldChildren.el);
                break;
            //  若是新元素的子元素多個
            case childTeyps.MULTIPLE:
                container.removeChild(oldChildren.el);
                for(let i = 0;i<newChildren.length;i++){
                    mount(newChildren[i],container);
                }
                break;
        }
    },

    upChildEmpty(...arg){
        let [oldChildrenFlag,newChildrenFlag,oldChildren,newChildren,container] = arg;
        //  循環新的子元素
        switch(newChildrenFlag){
            //  若是新元素的子元素爲一個
            case childTeyps.SINGLE:
                mount(newChildren,container);
                break;
            //  若是新元素的子元素爲空
            case childTeyps.EMPTY:
                break;
            //  若是新元素的子元素多個
            case childTeyps.MULTIPLE:
                container.removeChild(oldChildren.el);
                for(let i = 0;i<newChildren.length;i++){
                    mount(newChildren[i],container);
                }
                break;
        }
    },

    upChildMultiple(...arg){
        let [oldChildrenFlag,newChildrenFlag,oldChildren,newChildren,container] = arg;
        //  循環新的子元素
        switch(newChildrenFlag){
            //  若是新元素的子元素爲一個
            case childTeyps.SINGLE:
                for(let i = 0;i<oldChildren.length;i++){
                    container.removeChild(oldChildren[i].el);
                }
                mount(newChildren,container);
                break;
            //  若是新元素的子元素爲空
            case childTeyps.EMPTY:
                for(let i = 0;i<oldChildren.length;i++){
                    container.removeChild(oldChildren[i].el);
                }
                break;
            //  若是新元素的子元素多個
            case childTeyps.MULTIPLE:
                //  **
                //  暫時擱置 這裏是全部節點的對比
                //  **
                break;
        }
    }

};

上面代碼比較亂,由於嵌套了多層循環,大體邏輯就是使用上述六種狀況一一對接配對而且使用其對應的解決方案。

上述六中狀況,switch匹配邏輯:

新數據 舊數據
舊元素只有一個 新元素只有一個
舊元素只有一個 新元素爲空
舊元素只有一個 新元素爲多個
舊元素爲空 新元素只有一個
舊元素爲空 新元素爲空
舊元素爲空 新元素爲多個
舊元素爲多個 新元素只有一個
舊元素爲多個 新元素爲空
舊元素爲多個 新元素爲多個

最爲複雜的就是最後一種狀況,新舊元素各爲多個,然而對於這一部分reactvue的處理方式都是不同的。如下借鑑的是reactdiff算法。

在進行虛擬DOM替換時,當元素之間的順序沒有發生變化則原有元素是不須要進行任何改動的,也就是說,若原有順序是123456,新順序爲654321則他們之間的順序發生了變化這個時候須要對其進行變動處理,若其順序出現了插入狀況192939495969在每一個數字後面添加了一個9,其實這個時候也是不須要進行更新操做的,其實他們之間的順序仍是和原來一致,只是添加了一些元素值而已,若是變成了213456,這是時候只須要改變12就好,其餘的是不須要作任何改動的。 接下來須要添加最關鍵的邏輯了。

//  更新VNode方法集
//  添加 oldMoreAndNewMore 方法
const patchMethos = {
  upChildMultiple(...arg) {
    let [oldChildrenFlag, newChildrenFlag, oldChildren, newChildren, container] = arg;
    //  循環新的子元素
    switch (newChildrenFlag) {
      //  若是新元素的子元素爲一個
      case childTeyps.SINGLE:
        for (let i = 0; i < oldChildren.length; i++) {
          // 遍歷刪除舊元素
          container.removeChild(oldChildren[i].el);
        }
        // 添加新元素
        mount(newChildren, container);
        break;
      //  若是新元素的子元素爲空
      case childTeyps.EMPTY:
        for (let i = 0; i < oldChildren.length; i++) {
          // 刪除全部子元素  
          container.removeChild(oldChildren[i].el);
        }
        break;
      //  若是新元素的子元素多個
      case childTeyps.MULTIPLE:
        // 修改了這裏 (●'◡'●) 
        this.oldMoreAndNewMore(...arg);
        break;
  },
  oldMoreAndNewMore(...arg) {
    let [oldChildrenFlag, newChildrenFlag, oldChildren, newChildren, container] = arg;
    let lastIndex = 0;
    for (let i = 0; i < newChildren.length; i++) {
      let newVnode = newChildren[i];
      let j = 0;
      //  新的元素是否找到
      let find = false;
      for (; j < oldChildren.length; j++) {
        let oldVnode = oldChildren[j];
        //  key相同爲同一個元素
        if (oldVnode.key === newVnode.key) {
          find = true;
          patch(oldVnode, newVnode, container);
          if (j < lastIndex) {
            if(newChildren[i-1].el){
              //  須要移動
              let flagNode = newChildren[i-1].el.nextSibling;
              container.insertBefore(oldVnode.el, flagNode);
            }
            break;
          }
          else {
            lastIndex = j;
          }
        }
      }
      // 若是沒有找到舊元素,須要新增
      if (!find) {
        // 須要插入的標誌元素
        let flagNode = i === 0 ? oldChildren[0].el : newChildren[i-1].el;
        mount(newVnode, container, flagNode);
      }
      //  移除元素
      for (let i = 0; i < oldChildren.length; i++) {
        //  舊節點
        const oldVNode = oldChildren[i];
        //  新節點key是否在舊節點中存在
        const has = newChildren.find(next => next.key === oldVNode.key);
        if (!has) {
          // 若是不存在刪除
          container.removeChild(oldVNode.el)
        }
      }
    }
  }
};
//  修改mount函數
//      flagNode    標誌node 新元素須要插入到哪裏
function mount(vnode, container, flagNode) {
  //  所需渲染標籤類型
  let { flag } = vnode;
  //  若是是節點
  if (flag === vnodeTypes.HTML) {
    //  調用建立節點方法
    mountMethod.mountElement(vnode, container, flagNode);
  } //  若是是文本
  else if (flag === vnodeTypes.TEXT) {
    //  調用建立文本方法
    mountMethod.mountText(vnode, container);
  };
};
//  修改mountElement
const mountMethod = {
  //  建立HTML元素方法
  //    修改了這裏 (●'◡'●) 添加 flagNode 參數
  mountElement(vnode, container, flagNode) {
    //  屬性,標籤名,子元素,子元素類型
    let { data, tag, children, childrenFlag } = vnode;
    //  建立的真實節點
    let dom = document.createElement(tag);
    //  添加屬性
    data && domAttributeMethod.addData(dom, data);
    //  在VNode中保存真實DOM節點
    vnode.el = dom;
    //  若是不爲空,表示有子元素存在
    if (childrenFlag !== childTeyps.EMPTY) {
      //  若是爲單個元素
      if (childrenFlag === childTeyps.SINGLE) {
        //  把子元素傳入,並把當前建立的DOM節點以父元素傳入
        //  其實就是要把children掛載到 當前建立的元素中
        mount(children, dom);
      } //  若是爲多個元素
      else if (childrenFlag === childTeyps.MULTIPLE) {
        //  循環子節點,並建立
        children.forEach((el) => mount(el, dom));
      };
    };
    //  添加元素節點  修改了這裏 (●'◡'●)
    flagNode ? container.insertBefore(dom, flagNode) : container.appendChild(dom);
  }
}

最終使用:

const VNODEData = [
    "div",
    {id:"test",key:789},
    [
      createElement("p",{
        key:1,
        style:{
          color:"red",
          background:"pink"
        }
      },"節點一"),
      createElement("p",{
        key:2,
        "@click":() => console.log("click me!!!")
      },"節點二"),
      createElement("p",{
        key:3,
        class:"active"
      },"節點三"),
      createElement("p",{key:4},"節點四"),
      createElement("p",{key:5},"節點五")
    ]
];
let VNODE = createElement(...VNODEData);
render(VNODE,document.getElementById("app"));

const VNODEData1 = [
    "div",
    {id:"test",key:789},
    [
      createElement("p",{
        key:6
      },"節點六"),
      createElement("p",{
        key:1,
        style:{
          color:"red",
          background:"pink"
        }
      },"節點一"),
      createElement("p",{
        key:5
      },"節點五"),
      createElement("p",{
        key:2
      },"節點二"),
      createElement("p",{
        key:4
      },"節點四"),
      createElement("p",{
        key:3,
        class:"active"
      },"節點三")
    ]
];

setTimeout(() => {
  let VNODE = createElement(...VNODEData1);
  render(VNODE,document.getElementById("app"));
},1000)

上面代碼用了大量的邏輯來處理其中使用大量計算,會比較兩棵樹之間的同級節點。這樣就完全的下降了複雜度,而且不會帶來什麼損失。由於在web應用中不太可能把一個組件在DOM樹中跨層級地去移動。

在計算中會盡量的引用以前的元素,進行位置替換,其實不管是React仍是Vue在渲染列表的時候須要給其元素賦值一個key屬性,由於在進行diff算法時,會優先使用其原有元素,進行位置調整,也是對性能優化的一大亮點。

結語

本文也只是對diff算法的簡單實現,也許不能知足全部要求,React的基本實現原理則是如此,但願這篇文章能對你們理解diff算法有所幫助。

很是感謝你們用這麼長時間來閱讀本文章,文章中代碼篇幅過長,如有錯誤請在評論區指出,我會及時作出改正。

相關文章
相關標籤/搜索