virtual DOM和真实DOM的区别_让虚拟DOM和DOMdiff不再成为你的绊脚石
來源 |?https://juejin.im/post/5c8e5e4951882545c109ae9c
Keep Moving
時至今日,前端對于知識的考量是越來越有水平了,逼格高大上了
各類框架大家已經可以說無論是工作還是日常中都已經或多或少的使用過了
曾經聽說很多人被問到過虛擬DOM和DOM-diff算法是如何實現的,有沒有研究過?
想必問出此問題的也是高手高手之高高手了,很多人都半開玩笑的說:“面試造航母,工作擰螺絲”
那么,話不多說了,今天就讓我們也來一起研究研究這個東東。
好飯不怕晚,沉淀下來收收心!我們雖然走的慢,但是卻從未停下腳步。
神奇的虛擬DOM
首先神奇不神奇的我們先不去關注,先來簡單說說何為虛擬DOM。
虛擬DOM簡而言之就是,用JS去按照DOM結構來實現的樹形結構對象,你也可以叫做DOM對象。
好了,一句話就把這么偉大的東西給解釋了,那么不再耽誤時間了,趕緊進入主環節吧。
當然,這里還有整個項目的地址方便查看。
實現一下虛擬DOM
在親自上陣之前,我們讓糧草先行,先發個圖,來看一下整個目錄結構是什么樣子的。
這個目錄結構是用create-react-app腳手架直接生成的,也是為了方便編譯調試。
// 全局安裝npm i create-react-app -g// 生成項目create-react-app dom-diff// 進入項目目錄cd dom-diff// 編譯npm run start現在我們開始正式寫吧,從創建虛擬DOM及渲染DOM起步吧
創建虛擬DOM
在element.js文件中要實現如何創建虛擬DOM以及將創建出來的虛擬DOM渲染成真實的DOM。
首先實現一下如何創建虛擬DOM,看代碼:
// element.js// 虛擬DOM元素的類,構建實例對象,用來描述DOMclass Element { constructor(type, props, children) { this.type = type; this.props = props; this.children = children; }}// 創建虛擬DOM,返回虛擬節點(object)function createElement(type, props, children) { return new Element(type, props, children);}export { Element, createElement}寫好了方法,我們就從index.js文件入手來看看是否成功吧
調用createElement方法
在主入口文件里,我們主要做的操作就是來創建一個DOM對象,渲染DOM以及通過diff后去打補丁更新DOM,不啰嗦了,直接看代碼:
// index.js// 首先引入對應的方法來創建虛擬DOMimport { createElement } from './element';let virtualDom = createElement('ul', {class: 'list'}, [ createElement('li', {class: 'item'}, ['周杰倫']), createElement('li', {class: 'item'}, ['林俊杰']), createElement('li', {class: 'item'}, ['王力宏'])]);console.log(virtualDom);createElement方法也是vue和react用來創建虛擬DOM的方法,我們也叫這個名字,方便記憶。接收三個參數,分別是type,props和children
參數分析
type: 指定元素的標簽類型,如'li', 'div', 'a'等
props: 表示指定元素身上的屬性,如class, style, 自定義屬性等
children: 表示指定元素是否有子節點,參數以數組的形式傳入
下面來看一下打印出來的虛擬DOM,如下圖。
到目前為止,已經輕而易舉的實現了創建虛擬DOM。那么,接下來進行下一步,將其渲染為真實的DOM,別猶豫,繼續回到element.js文件中。
渲染虛擬DOM
// element.jsclass Element { // 省略}function createElement() { // 省略}// render方法可以將虛擬DOM轉化成真實DOMfunction render(domObj) { // 根據type類型來創建對應的元素 let el = document.createElement(domObj.type); // 再去遍歷props屬性對象,然后給創建的元素el設置屬性 for (let key in domObj.props) { // 設置屬性的方法 setAttr(el, key, domObj.props[key]); } // 遍歷子節點 // 如果是虛擬DOM,就繼續遞歸渲染 // 不是就代表是文本節點,直接創建 domObj.children.forEach(child => { child = (child instanceof Element) ? render(child) : document.createTextNode(child); // 添加到對應元素內 el.appendChild(child); }); return el;}// 設置屬性function setAttr(node, key, value) { switch(key) { case 'value': // node是一個input或者textarea就直接設置其value即可 if (node.tagName.toLowerCase() === 'input' || node.tagName.toLowerCase() === 'textarea') { node.value = value; } else { node.setAttribute(key, value); } break; case 'style': // 直接賦值行內樣式 node.style.cssText = value; break; default: node.setAttribute(key, value); break; }}// 將元素插入到頁面內function renderDom(el, target) { target.appendChild(el);}export { Element, createElement, render, setAttr, renderDom};既然寫完了,那就趕快來看看成果吧。
調用render方法
再次回到index.js文件中,修改為如下代碼。
// index.js// 引入createElement、render和renderDom方法import { createElement, render, renderDom } from './element';let virtualDom = createElement('ul', {class: 'list'}, [ createElement('li', {class: 'item'}, ['周杰倫']), createElement('li', {class: 'item'}, ['林俊杰']), createElement('li', {class: 'item'}, ['王力宏'])]);console.log(virtualDom);// +++let el = render(virtualDom); // 渲染虛擬DOM得到真實的DOM結構console.log(el);// 直接將DOM添加到頁面內renderDom(el, document.getElementById('root'));// +++通過調用render方法轉為真實DOM,并調用renderDom方法直接將DOM添加到了頁面內。
下圖為打印后的結果:
截止目前,我們已經實現了虛擬DOM并進行了渲染真實DOM到頁面中。那么接下來我們就有請DOM-diff隆重登場,來看一下這大有來頭的diff算法是如何發光發熱的吧!
DOM-diff閃亮登場
說到DOM-diff那一定要清楚其存在的意義,給定任意兩棵樹,采用先序深度優先遍歷的算法找到最少的轉換步驟。
DOM-diff比較兩個虛擬DOM的區別,也就是在比較兩個對象的區別。
作用: 根據兩個虛擬對象創建出補丁,描述改變的內容,將這個補丁用來更新DOM。
已經了解到DOM-diff是干嘛的了,那就沒什么好說的了,繼續往下寫吧。
// diff.jsfunction diff(oldTree, newTree) { // 聲明變量patches用來存放補丁的對象 let patches = {}; // 第一次比較應該是樹的第0個索引 let index = 0; // 遞歸樹 比較后的結果放到補丁里 walk(oldTree, newTree, index, patches); return patches;}function walk(oldNode, newNode, index, patches) { // 每個元素都有一個補丁 let current = []; if (!newNode) { // rule1 current.push({ type: 'REMOVE', index }); } else if (isString(oldNode) && isString(newNode)) { // 判斷文本是否一致 if (oldNode !== newNode) { current.push({ type: 'TEXT', text: newNode }); } } else if (oldNode.type === newNode.type) { // 比較屬性是否有更改 let attr = diffAttr(oldNode.props, newNode.props); if (Object.keys(attr).length > 0) { current.push({ type: 'ATTR', attr }); } // 如果有子節點,遍歷子節點 diffChildren(oldNode.children, newNode.children, patches); } else { // 說明節點被替換了 current.push({ type: 'REPLACE', newNode}); } // 當前元素確實有補丁存在 if (current.length) { // 將元素和補丁對應起來,放到大補丁包中 patches[index] = current; }}function isString(obj) { return typeof obj === 'string';}function diffAttr(oldAttrs, newAttrs) { let patch = {}; // 判斷老的屬性中和新的屬性的關系 for (let key in oldAttrs) { if (oldAttrs[key] !== newAttrs[key]) { patch[key] = newAttrs[key]; // 有可能還是undefined } } for (let key in newAttrs) { // 老節點沒有新節點的屬性 if (!oldAttrs.hasOwnProperty(key)) { patch[key] = newAttrs[key]; } } return patch;}// 所有都基于一個序號來實現let num = 0;function diffChildren(oldChildren, newChildren, patches) { // 比較老的第一個和新的第一個 oldChildren.forEach((child, index) => { walk(child, newChildren[index], ++num, patches); });}// 默認導出export default diff;代碼雖然又臭又長,但是這些代碼就讓我們實現了diff算法了,所以大家先不要盲動,不要盲動,且聽風吟,讓我一一道來。
比較規則
新的DOM節點不存在{type: 'REMOVE', index}
文本的變化{type: 'TEXT', text: 1}
當節點類型相同時,去看一下屬性是否相同,產生一個屬性的補丁包{type: 'ATTR', attr: {class: 'list-group'}}
節點類型不相同,直接采用替換模式{type: 'REPLACE', newNode}
根據這些規則,我們再來看一下diff代碼中的walk方法這位關鍵先生
walk方法都做了什么?
每個元素都有一個補丁,所以需要創建一個放當前補丁的數組
如果沒有new節點的話,就直接將type為REMOVE的類型放到當前補丁里
如果新老節點是文本的話,判斷一下文本是否一致,再指定類型TEXT并把新節點放到當前補丁
如果新老節點的類型相同,那么就來比較一下他們的屬性props
diffChildren
遍歷oldChildren,然后遞歸調用walk再通過child和newChildren[index]去diff
diffAttr
去比較新老Attr是否相同
把newAttr的鍵值對賦給patch對象上并返回此對象
屬性比較
然后如果有子節點的話就再比較一下子節點的不同,再調一次walk
上面三個如果都沒有發生的話,那就表示節點單純的被替換了,type為REPLACE,直接用newNode替換即可
當前補丁里確實有值的情況,就將對應的補丁放進大補丁包里
以上就是關于diff算法的分析過程了,沒太明白的話沒關系,再反復看幾遍試試,意外總是不期而遇的。
diff已經完事了,那么最后一步就是大家所熟知的打補丁了。
補丁要怎么打?那么讓久違的patch出來吧。
patch補丁更新
打補丁需要傳入兩個參數,一個是要打補丁的元素,另一個就是所要打的補丁了,那么直接看代碼。
import { Element, render, setAttr } from './element';let allPatches;let index = 0; // 默認哪個需要打補丁function patch(node, patches) { allPatches = patches; // 給某個元素打補丁 walk(node);}function walk(node) { let current = allPatches[index++]; let childNodes = node.childNodes; // 先序深度,繼續遍歷遞歸子節點 childNodes.forEach(child => walk(child)); if (current) { doPatch(node, current); // 打上補丁 }}function doPatch(node, patches) { // 遍歷所有打過的補丁 patches.forEach(patch => { switch (patch.type) { case 'ATTR': for (let key in patch.attr) { let value = patch.attr[key]; if (value) { setAttr(node, key, value); } else { node.removeAttribute(key); } } break; case 'TEXT': node.textContent = patch.text; break; case 'REPLACE': let newNode = patch.newNode; newNode = (newNode instanceof Element) ? render(newNode) : document.createTextNode(newNode); node.parentNode.replaceChild(newNode, node); break; case 'REMOVE': node.parentNode.removeChild(node); break; default: break; } });}export default patch;看完代碼還需要再來簡單的分析一下。
patch做了什么?
用一個變量來得到傳遞過來的所有補丁allPatches
patch方法接收兩個參數(node, patches)
在方法內部調用walk方法,給某個元素打上補丁
walk方法里獲取所有的子節點
給子節點也進行先序深度優先遍歷,遞歸walk
如果當前的補丁是存在的,那么就對其打補丁(doPatch)
doPatch打補丁方法會根據傳遞的patches進行遍歷
判斷補丁的類型來進行不同的操作
屬性ATTR for in去遍歷attrs對象,當前的key值如果存在,就直接設置屬性setAttr;如果不存在對應的key值那就直接刪除這個key鍵的屬性
文字TEXT 直接將補丁的text賦值給node節點的textContent即可
替換REPLACE 新節點替換老節點,需要先判斷新節點是不是Element的實例,是的話調用render方法渲染新節點;
不是的話就表明新節點是個文本節點,直接創建一個文本節點就OK了。
之后再通過調用父級parentNode的replaceChild方法替換為新的節點
刪除REMOVE 直接調用父級的removeChild方法刪除該節點
將patch方法默認導出方便調用
好了,一切都安靜下來了。讓我們回歸index.js文件中,去調用一下diff和patch這兩個重要方法,看看奇跡會不會發生吧
回歸
// index.jsimport { createElement, render, renderDom } from './element';// +++ 引入diff和patch方法import diff from './diff';import patch from './patch';// +++let virtualDom = createElement('ul', {class: 'list'}, [ createElement('li', {class: 'item'}, ['周杰倫']), createElement('li', {class: 'item'}, ['林俊杰']), createElement('li', {class: 'item'}, ['王力宏']) ]);let el = render(virtualDom);renderDom(el, window.root);// +++// 創建另一個新的虛擬DOMlet virtualDom2 = createElement('ul', {class: 'list-group'}, [ createElement('li', {class: 'item active'}, ['七里香']), createElement('li', {class: 'item'}, ['一千年以后']), createElement('li', {class: 'item'}, ['需要人陪']) ]);// diff一下兩個不同的虛擬DOMlet patches = diff(virtualDom, virtualDom2);console.log(patches);// 將變化打補丁,更新到elpatch(el, patches);// +++將修改后的代碼保存,會在瀏覽器里看到DOM被更新了,如下圖。
到這里就finish了,內容有些多,可能不是很好的消耗,不過沒關系,就讓我用最后幾句話來總結一下實現的整個過程吧。
四句話
我們來梳理一下整個DOM-diff的過程:
用JS對象模擬DOM(虛擬DOM)
把此虛擬DOM轉成真實DOM并插入頁面中(render)
如果有事件發生修改了虛擬DOM,比較兩棵虛擬DOM樹的差異,得到差異對象(diff)
把差異對象應用到真正的DOM樹上(patch)
行了,就這四句話吧,說多了就有點畫蛇添足了。
總結
以上是生活随笔為你收集整理的virtual DOM和真实DOM的区别_让虚拟DOM和DOMdiff不再成为你的绊脚石的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: awvs 13使用_如何解密AWVS?1
- 下一篇: win7 nvme 支持补丁_Updat