虛擬Dom詳解 - (一)

隨着VueReact的風聲水起,伴隨着諸多框架的成長,虛擬DOM漸漸成了咱們常常議論和討論的話題。什麼是虛擬DOM,虛擬DOM是如何渲染的,那麼Vue的虛擬DomReact的虛擬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是那麼的簡陋,可是足能夠說明狀況啦,像VueReact當須要對頁面進行渲染更新的時候,則是對比的就是虛擬DOM更新先後的差別只對有差別的部分進行更新,大大減小了對DOM的操做。這裏也就是咱們常常所說的DIFF算法。app

經過上述描述能夠總結得出,因爲原生DOM節點中的屬性和方法過於複雜,操做時過於影響性能,因此使用Object來描述頁面中的HTML結構,以達到對性能的提高。框架

如何建立虛擬DOM

若是熟悉VueReact的朋友可能會知道一點,首先說下Vue,在使用中Vue中的虛擬DOM是使用template完成的,也就是平時咱們項目中書寫最多的模板,Vue經過vue-loader對其進行編譯處理最後造成咱們所須要的虛擬DOM,然而在React中則是否是這樣的,React是沒有template的,React則是使用的是JSX對進行編譯,最後產生虛擬DOM,不管是Vue仍是React最終的想要獲得的就是虛擬DOMdom

若想要知道虛擬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則須要一個特定的方法,在VueReact中會在HTML有一個idapp的真實DOM節點,最終渲染的時候被替換成了虛擬DOM節點生成的真是的DOM節點,接下來就按照這個思路繼續實現一下,在VueReact都有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對象和舊的虛擬DOMJavaScript對象做對比,記錄着兩棵樹的差別。把差異反映到真實的DOM結構上最後操做真正的DOM的時候只操做有差別的部分就能夠了。

下次再見,如有哪裏有錯誤請大佬們及時指出,文章中如有錯誤請在評論區留言,我會盡快作出改正。

相關文章
相關標籤/搜索