隨着Vue
和React
的風聲水起,伴隨着諸多框架的成長,虛擬DOM
漸漸成了咱們常常議論和討論的話題。什麼是虛擬DOM
,虛擬DOM
是如何渲染的,那麼Vue
的虛擬Dom
和React
的虛擬DOM
到底有什麼區別等等等...一系列的話題都在不斷的討論中。爲此也作了一些學習簡單的侃一侃虛擬DOM
究竟是什麼?vue
虛擬Dom詳解 - (二)node
什麼是虛擬Dom
虛擬DOM
首次產生是React
框架最早提出和使用的,其卓越的性能很快獲得廣大開發者的承認,繼React
以後vue2.0
也在其核心引入了虛擬DOM
的概念。在沒有虛擬DOM
的時候,咱們在建立頁面的時候通常都是使用HTML
標籤一個一個的去搭建咱們的頁面,既然有了DOM
節點之後,爲何不直接使用原生DOM
,那麼原生DOM
到底有什麼弊端呢?緣由是這個樣子的,原生DOM
中一個Node
節點有N
多的屬性,一旦對DOM
進行操做的時候會影響頁面性能的核心問題主要在於DOM
操做致使了頁面的重繪或重排,爲了減小因爲重繪和重排對網頁性能的影響,因此不管在什麼項目中儘量少的去操做DOM
節點是性能優化的一大重點。算法
所謂的虛擬DOM
究竟是什麼?也就是經過JavaScript
語言來描述一段HTML
代碼。其實使用JavaScript
描述一段HTML
代碼是很簡單的:segmentfault
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
算法。app
經過上述描述能夠總結得出,因爲原生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
是如何建立的,那麼就可簡單的實現一下其建立過程,在上面中能夠獲得一個描述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, // 保存子節點內容 children:text }; };
經過上面的代碼能夠簡單的實現對虛擬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); } };
最終使用:
const VNODEData = [ "div", {id:"test"}, [ 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"));
以上就簡單的實現了對虛擬DOM
的建立以及屬性的以及事件的掛載,算是有一個很大的跨越了,只是完成初始化是遠遠不夠的,還須要對其進一步處理,so~有時間的話會繼續對虛擬DOM
的更新進行說明。也就是其DIFF
算法部分。單一職責,一篇博客只作一件事,哈哈~
總結
虛擬DOM
在目前流行的幾大框架中都做爲核心的一部分使用,可見其性能的高效,本文只是簡單的作一個簡單的剖析,說到頭來其實虛擬DOM
就是使用JavaScript
對象來表示DOM
樹的信息和結構,這個JavaScript
對象能夠構建一個真正的DOM
樹。當狀態變動的時候用修改後的新渲染的的JavaScript
對象和舊的虛擬DOM
的JavaScript
對象做對比,記錄着兩棵樹的差別。把差異反映到真實的DOM
結構上最後操做真正的DOM
的時候只操做有差別的部分就能夠了。
下次再見,如有哪裏有錯誤請大佬們及時指出,文章中如有錯誤請在評論區留言,我會盡快作出改正。