隨着Vue
和React
的風聲水起,伴隨着諸多框架的成長,虛擬DOM
漸漸成了我們經常議論和討論的話題。什么是虛擬DOM
,虛擬DOM
是如何渲染的,那么Vue
的虛擬Dom
和React
的虛擬DOM
到底有什么區別等等等...一系列的話題都在不斷的討論中。為此也做了一些學習簡單的侃一侃虛擬DOM
到底是什么?
什么是虛擬Dom
虛擬Dom首次產生是React
框架最先提出和使用的,其卓越的性能很快得到廣大開發者的認可,繼React
之后vue2.0
也在其核心引入了虛擬DOM
的概念。在沒有虛擬DOM
的時候,我們在創建頁面的時候一般都是使用HTML
標簽一個一個的去搭建我們的頁面,既然有了DOM
節點以后,為什么不直接使用原生DOM
,那么原生DOM
到底有什么弊端呢?原因是這個樣子的,原生DOM
中一個Node
節點有N
多的屬性,一旦對DOM
進行操作的時候會影響頁面性能的核心問題主要在於DOM
操作導致了頁面的重繪或重排,為了減少由於重繪和重排對網頁性能的影響,所以無論在什么項目中盡可能少的去操作DOM
節點是性能優化的一大重點。
所謂的虛擬DOM
到底是什么?也就是通過JavaScript
語言來描述一段HTML
代碼。其實使用JavaScript
描述一段HTML
代碼是很簡單的:
HTML:
<div class="" id="app">
<p class="text">節點一</p>
</div>
JavaScript:
const createElement = () => {
return {
"tag":"div",
"prop":{
"id":"app"
},
"children":[
{
"tag":"p",
"prop":{
"class":"text"
},
"children":["節點一"]
}
]
}
}
上面的代碼中,只是簡單的使用了JavaScript
語言簡單描述了一下HTML
部分相對應的代碼,此時我們只需要再寫入一個創建DOM
的方法,按照文檔描述將創建好的DOM
按照層級添加到里面頁面中就好了。
上述JavaScript
中所描述的數據類型也就可以簡單的理解為是虛擬DOM
,雖然這個虛擬DOM
是那么的簡陋,但是足可以說明情況啦,像Vue
和React
當需要對頁面進行渲染更新的時候,則是對比的就是虛擬DOM
更新前后的差異只對有差異的部分進行更新,大大減少了對DOM
的操作。這里也就是我們經常所說的DIFF
算法。
通過上述描述可以總結得出,由於原生DOM
節點中的屬性和方法過於復雜,操作時過於影響性能,所以使用Object
來描述頁面中的HTML
結構,以達到對性能的提升。
如何創建虛擬DOM
如果熟悉Vue
或React
的朋友可能會知道一點,首先說下Vue
,在使用中Vue
中的虛擬DOM
是使用template
完成的,也就是平時我們項目中書寫最多的模板,Vue
通過vue-loader
對其進行編譯處理最后形成我們所需要的虛擬DOM
,然而在React
中則是不是這樣的,React
是沒有template
的,React
則是使用的是JSX
對進行編譯,最后產生虛擬DOM,無論是Vue
還是React
最終的想要得到的就是虛擬DOM
。
若想要知道虛擬DOM是如何創建的,那么就可簡單的實現一下其創建過程,在上面中可以得到一個描述DOM
節點的數據文本,我們可以根據其需要對其進行創建:
const vnodeTypes = {
// HTML節點類型
"HTML":"HTML",
// 文本類型
"TEXT":"TEXT",
// 組件類型
"COMPONENT":"COMPONENT"
};
const childTeyps = {
// 為空
"EMPTY":"EMPTY",
// 單個
"SINGLE":"SINGLE",
// 多個
"MULTIPLE":"MULTIPLE"
};
// 新建虛擬DOM
// 所需創建標簽名稱
// 標簽屬性
// 標簽子元素
function createElement (tag,data,children = null){
// 當前元素的標簽類型
let flag;
// 子元素的標簽類型
let childrenFlag;
if(typeof tag === "string"){
// 如果是文本的則認為是,普通的HTML標簽
// 將其元素的flag設置成HTML類型
flag = vnodeTypes.HTML;
}else if(typeof tag === "function"){
// 如果為函數,則認為其為組件
flag = vnodeTypes.COMPONENT;
}
else {
// 否則是文本類型
flag = vnodeTypes.TEXT;
};
// 判斷子元素情況
if(children === null){
// 如果 children 為空
// 則子元素類型為空
childrenFlag = childTeyps.EMPTY;
}else if (Array.isArray(children)){
// 如果 children 為數組
// 獲取子元素長度
let len = children.length;
// 如果長度存在
if(len){
// 則設置子元素類型為多個
childrenFlag = childTeyps.MULTIPLE;
}else{
// 否則設置為空
childrenFlag = childTeyps.EMPTY;
}
}else {
// 如果存在並且不為空
// 則設置為單個
childrenFlag = childTeyps.SINGLE;
// 創建文本類型方法,並將 children 的值轉為字符串
children = createTextVNode(children+"");
}
// 返回虛擬DOM
return {
flag, // 虛擬DOM類型
tag, // 標簽
data, // 虛擬DOM屬性
children, // 虛擬DOM子節點
childrenFlag, // 虛擬DOM子節點類型
el:null // 掛載元素的父級
};
};
// 新建文本類型虛擬DOM
function createTextVNode (text){
return {
// 節點類型設置為文本
flag:vnodeTypes.TEXT,
// 設置為沒有標簽
tag:null,
// 沒有任何屬性
data:null,
// 子元素類型設置為單個
childrenFlag:childTeyps.EMPTY
};
};
通過上面的代碼可以簡單的實現對虛擬DOM
的創建,可以通過調用createElement
並傳入用來描述虛擬DOM
的對象,就可以打印出已經創建好的虛擬DOM
節點:
const VNODEData = [
"div",
{id:"test"},
[
createElement("p",{},"節點一")
]
];
let div = createElement(...VNODEData);
console.log(div);
結果:
{
"flag": "HTML",
"tag": "div",
"data": {
"id": "test"
},
"children": [{
"flag": "HTML",
"tag": "p",
"data": {},
"children": {
"flag": "TEXT",
"tag": null,
"data": null,
"childrenFlag": "EMPTY"
},
"childrenFlag": "SINGLE"
}],
"childrenFlag": "MULTIPLE"
}
通過上述方法打印出來的則是按照傳入的描述虛擬DOM
的對象,已經創建好了一個虛擬DOM
樹,是不是一件很神奇的事情,其實仔細看下代碼也沒有什么特別重要的邏輯,只是該變了數據結構而已(可以這樣理解,但是不能對外這么說,很丟人的,哈哈)。
既然虛擬DOM
節點已經出來了,下一步就是如何渲染出虛擬DOM
了,渲染虛擬DOM
則需要一個特定的方法,在Vue
和React
中會在HTML
有一個id
為app
的真實DOM
節點,最終渲染的時候被替換成了虛擬DOM
節點生成的真是的DOM
節點,接下來就按照這個思路繼續實現一下,在Vue
和React
都有render
函數,這里也就同樣使用這個名稱進行命名了,在開始之前,首先要確認一點的是,無論是首次渲染還是更新都是通過render
函數來完成的,所以要對其進行判斷,其余的就不多贅述了。
// 渲染虛擬DOM
// 虛擬DOM節點樹
// 承載DOM節點的容器,父元素
function render(vnode,container) {
// 首次渲染
mount(vnode,container);
};
// 首次渲染
function mount (vnode,container){
// 所需渲染標簽類型
let {flag} = vnode;
// 如果是節點
if(flag === vnodeTypes.HTML){
// 調用創建節點方法
mountMethod.mountElement(vnode,container);
} // 如果是文本
else if(flag === vnodeTypes.TEXT){
// 調用創建文本方法
mountMethod.mountText(vnode,container);
};
};
// 創建各種元素的方法
const mountMethod = {
// 創建HTML元素方法
mountElement(vnode,container){
// 屬性,標簽名,子元素,子元素類型
let {tag,children,childrenFlag} = vnode;
// 創建的真實節點
let dom = document.createElement(tag);
// 在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));
};
};
// 添加元素節點
container.appendChild(dom);
},
// 創建文本元素方法
mountText(vnode,container){
// 創建真實文本節點
let dom = document.createTextNode(vnode.children);
// 保存dom
vnode.el = dom;
// 添加元素
container.appendChild(dom);
}
};
通過上面的代碼,就可完成真實DOM
的渲染工作了,雖然但是這也只是完成了其中的一小部分而已。但是很多東西沒有添加進去,比如動態添加style
樣式,給元素綁定樣式,添加class
等等等,一系列的問題都還沒有解決,現在工作也只是簡單的初始化而已。其實想要完成上述的功能也不是很難,要知道剛剛所說的所有東西都是添加到DOM
節點上的,我們只需要在DOM
節點上做文章就可以了,改進mountElement
方法:
const mountMethod = {
// 創建HTML元素方法
mountElement(vnode,container){
// 屬性,標簽名,子元素,子元素類型
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));
};
};
// 添加元素節點
container.appendChild(dom);
}
};
// dom添加屬性方法
const domAttributeMethod = {
addData (dom,data){
// 掛載屬性
for(let key in data){
// dom節點,屬性名,舊值(方便做更新),新值
this.patchData(dom,key,null,data[key]);
}
},
patchData (el,key,prv,next){
switch(key){
case "style":
this.setStyle(el,key,prv,next);
break;
case "class":
this.setClass(el,key,prv,next);
break;
default :
this.defaultAttr(el,key,prv,next);
break;
}
},
setStyle(el,key,prv,next){
for(let attr in next){
el.style[attr] = next[attr];
}
},
setClass(el,key,prv,next){
el.setAttribute("class",next);
},
defaultAttr(el,key,prv,next){
if(key[0] === "@"){
this.addEvent(el,key,prv,next);
}
else {
this.setAttribute(el,key,prv,next);
}
},
addEvent(el,key,prv,next){
if(next){
el.addEventListener(key.slice(1),next);
}
},
setAttribute(el,key,prv,next){
el.setAttribute(key,next);
}
};
以上就簡單的實現了對虛擬DOM
的創建以及屬性的以及事件的掛載,算是有一個很大的跨越了,只是完成初始化是遠遠不夠的,還需要對其進一步處理,so有時間的話會繼續對虛擬`DOM`的更新進行說明。也就是其`DIFF`算法部分。單一職責,一篇博客只做一件事,哈哈
總結
虛擬DOM
在目前流行的幾大框架中都作為核心的一部分使用,可見其性能的高效,本文只是簡單的做一個簡單的剖析,說到頭來其實虛擬DOM
就是使用JavaScript
對象來表示DOM
樹的信息和結構,這個JavaScript
對象可以構建一個真正的DOM
樹。當狀態變更的時候用修改后的新渲染的的JavaScript
對象和舊的虛擬DOM
的JavaScript
對象作對比,記錄着兩棵樹的差異。把差別反映到真實的DOM
結構上最后操作真正的DOM
的時候只操作有差異的部分就可以了。
下次再見,若有哪里有錯誤請大佬們及時指出,文章中若有錯誤請在評論區留言,我會盡快做出改正。