摘要:第一篇文章中主要講解了虛擬基本實現,簡單的回顧一下,虛擬是使用數據描述的一段虛擬節點樹,通過函數生成其真實節點。并添加到其對應的元素容器中。在創建真實節點的同時并為其注冊事件并添加一些附屬屬性。
第一篇文章中主要講解了虛擬DOM基本實現,簡單的回顧一下,虛擬DOM是使用json數據描述的一段虛擬Node節點樹,通過render函數生成其真實DOM節點。并添加到其對應的元素容器中。在創建真實DOM節點的同時并為其注冊事件并添加一些附屬屬性。
虛擬Dom詳解 - (一)
在上篇文章中也曾經提到過,當狀態變更的時候用修改后的新渲染的的JavaScript對象和舊的虛擬DOM的JavaScript對象作對比,記錄著兩棵樹的差異,把差別反映到真實的DOM結構上最后操作真正的DOM的時候只操作有差異的部分的更改。然而上篇文章中也只是簡簡單單的提到過一句卻沒有進行實質性的實現,這篇文章主要講述一下虛擬DOM是如何做出更新的。那就開始吧...O(∩_∩)O
在虛擬DOM中實現更新的話是使用DIFF算法進行更新的,我想大多數小伙伴都應該聽說過這個詞,DIFF是整個虛擬DOM部分最核心的部分,因為當虛擬DOM節點狀態發生改變以后不可能去替換整個DOM節點樹,若是這樣的話會出現打兩個DOM操作,無非是對性能的極大影響,真的如此的話還不如直接操作DOM來的實際一些。
第一篇文章中是通過render對虛擬DOM節點樹進行渲染的,但是在render函數中只做了一件事情,只是對虛擬DOM進行了新建也就是初始化工作,其實回過頭來想一下,無論是新建操作還是修改操作,都應該通過render函數來做,在react中所有的DOM渲染都是通過其中的render函數完成的,那么也就得出了這個結論。
// 渲染虛擬DOM // 虛擬DOM節點樹 // 承載DOM節點的容器,父元素 function render(vnode,container) { // 首次渲染 mount(vnode,container); };
既然更新和創建操作都是通過render函數來做的,在方法中又應該如何區分當前的操作到底是新建還是更新呢?畢竟在react我們并沒有給出明確的標識來告訴其方法,當前是進行的哪個操作。在執行render函數的時候有兩個參數,一個是傳入的vnode節點樹,還有一個就是承載真實DOM節點的容器,其實我們可以把其虛擬DOM節點樹掛載在其容器中,若容器中存在其節點樹則是更新操作,反之則是新建操作。
// 渲染虛擬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節點已經保存在了父容器中,直接使用即可。
// 更新函數 // 舊的虛擬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數據進行了變更,然后才能對其進行替換操作,這樣的話需要確定要更改的數據,然后替換掉原有數據,才能進行下一步更新操作。
// 更新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種情況:
舊元素只有一個
舊元素為空
舊元素為多個
新元素只有一個
新元素為空
新元素為多個
// 更新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上面代碼比較亂,因為嵌套了多層循環,大致邏輯就是使用上述六種情況一一對接配對并且使用其對應的解決方案。
上述六中情況,switch匹配邏輯:
新數據 舊數據 舊元素只有一個 新元素只有一個 舊元素只有一個 新元素為空 舊元素只有一個 新元素為多個 舊元素為空 新元素只有一個 舊元素為空 新元素為空 舊元素為空 新元素為多個 舊元素為多個 新元素只有一個 舊元素為多個 新元素為空 舊元素為多個 新元素為多個 最為復雜的就是最后一種情況,新舊元素各為多個,然而對于這一部分react和vue的處理方式都是不一樣的。以下借鑒的是react的diff算法。
在進行虛擬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算法有所幫助。
非常感謝大家用這么長時間來閱讀本文章,文章中代碼篇幅過長,若有錯誤請在評論區指出,我會及時做出改正。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106751.html
摘要:為此也做了一些學習簡單的侃一侃虛擬到底是什么虛擬詳解二什么是虛擬虛擬首次產生是框架最先提出和使用的,其卓越的性能很快得到廣大開發者的認可,繼之后也在其核心引入了虛擬的概念。所謂的虛擬到底是什么也就是通過語言來描述一段代碼。 隨著Vue和React的風聲水起,伴隨著諸多框架的成長,虛擬DOM漸漸成了我們經常議論和討論的話題。什么是虛擬DOM,虛擬DOM是如何渲染的,那么Vue的虛擬Dom...
摘要:打開瀏覽器輸入,會看到構建的項目的主頁目錄結構使用編輯器打開推薦使用,下面具體看看目錄結構在中,根據我們在構建項目的時候的選項,有以下幾個命令。 構建一個 vue 項目最簡單的方式就是使用腳手架工具 vue-cli 。前端的三大框架都有自己的腳手架工具,其作用就是用配置好的模板迅速搭建起一個項目工程來,省去自己配置 webpack 配置文件的基本內容,大大降低了初學者構建項目的難度。這...
摘要:添加事件偵聽器時使用模式。只當事件是從偵聽器綁定的元素本身觸發時才觸發回調。只當點擊鼠標右鍵時觸發只當點擊鼠標中鍵時觸發以模式添加偵聽器,減少額外的監聽,提高性能表示永遠不會調用。記住,指令函數能夠接受所有合法的表達式。 思維導圖 showImg(https://segmentfault.com/img/bVbphXZ?w=1920&h=2408); 指令 v-for 關于key 官方...
閱讀 966·2022-06-21 15:13
閱讀 1853·2021-10-20 13:48
閱讀 1035·2021-09-22 15:47
閱讀 1369·2019-08-30 15:55
閱讀 3124·2019-08-30 15:53
閱讀 523·2019-08-29 12:33
閱讀 717·2019-08-28 18:15
閱讀 3464·2019-08-26 13:58