摘要:為此也做了一些學習簡單的侃一侃虛擬到底是什么虛擬詳解二什么是虛擬虛擬首次產生是框架最先提出和使用的,其卓越的性能很快得到廣大開發者的認可,繼之后也在其核心引入了虛擬的概念。所謂的虛擬到底是什么也就是通過語言來描述一段代碼。
隨著Vue和React的風聲水起,伴隨著諸多框架的成長,虛擬DOM漸漸成了我們經常議論和討論的話題。什么是虛擬DOM,虛擬DOM是如何渲染的,那么Vue的虛擬Dom和React的虛擬DOM到底有什么區別等等等...一系列的話題都在不斷的討論中。為此也做了一些學習簡單的侃一侃虛擬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:
節點一
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, // 保存子節點內容 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的時候只操作有差異的部分就可以了。
下次再見,若有哪里有錯誤請大佬們及時指出,文章中若有錯誤請在評論區留言,我會盡快做出改正。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106468.html
摘要:第一篇文章中主要講解了虛擬基本實現,簡單的回顧一下,虛擬是使用數據描述的一段虛擬節點樹,通過函數生成其真實節點。并添加到其對應的元素容器中。在創建真實節點的同時并為其注冊事件并添加一些附屬屬性。 第一篇文章中主要講解了虛擬DOM基本實現,簡單的回顧一下,虛擬DOM是使用json數據描述的一段虛擬Node節點樹,通過render函數生成其真實DOM節點。并添加到其對應的元素容器中。在創建...
摘要:添加事件偵聽器時使用模式。只當事件是從偵聽器綁定的元素本身觸發時才觸發回調。只當點擊鼠標右鍵時觸發只當點擊鼠標中鍵時觸發以模式添加偵聽器,減少額外的監聽,提高性能表示永遠不會調用。記住,指令函數能夠接受所有合法的表達式。 思維導圖 showImg(https://segmentfault.com/img/bVbphXZ?w=1920&h=2408); 指令 v-for 關于key 官方...
閱讀 3143·2021-11-23 10:02
閱讀 3117·2021-11-16 11:53
閱讀 3092·2021-09-23 11:21
閱讀 3368·2019-08-30 13:02
閱讀 1621·2019-08-29 16:18
閱讀 1556·2019-08-29 12:55
閱讀 1456·2019-08-26 12:24
閱讀 2085·2019-08-26 10:36