1. 程式人生 > >你不知道的 Virtual DOM(一):Virtual Dom 介紹

你不知道的 Virtual DOM(一):Virtual Dom 介紹

  • 前言

目前最流行的兩大前端框架,React和Vue,都不約而同的藉助Virtual DOM技術提高頁面的渲染效率。那麼,什麼是Virtual DOM?它是通過什麼方式去提升頁面渲染效率的呢?本系列文章會詳細講解Virtual DOM的建立過程,並實現一個簡單的Diff演算法來更新頁面。本文的內容脫離於任何的前端框架,只講最純粹的Virtual DOM。敲單詞太累了,下文Virtual DOM一律用VD表示。

  • VD是什麼

本質上來說,VD只是一個簡單的JS物件,並且最少包含tag、props和children三個屬性。不同的框架對這三個屬性的命名會有點差別,但表達的意思是一致的。它們分別是標籤名(tag)、屬性(props)和子元素物件(children)。下面是一個典型的VD物件例子:

12345678910111213141516171819 {tag:"div",props:{},children:["Hello World",{tag:"ul",props:{},children:[{tag:"li",props:{id:1,class:"li-1"},children:["第",1]}]}]}

VD跟dom物件有一一對應的關係,上面的VD是由以下的HTML生成的

12345678 <div>Hello World<ul><li id="1"class="li-1">1</li></ul></div>

一個dom物件,比如li,由tag(li), props({id: 1, class: "li-1"})children(["第", 1])三個屬性來描述。

  • 為什麼需要VD

藉助VD,可以達到有效減少頁面渲染次數的目的,從而提高渲染效率。我們先來看下頁面的更新一般會經過幾個階段。 clipboard.png

從上面的例子中,可以看出頁面的呈現會分以下3個階段:

  • JS計算
  • 生成渲染樹
  • 繪製頁面

這個例子裡面,JS計算用了691毫秒,生成渲染樹578毫秒,繪製73毫秒。如果能有效的減少生成渲染樹和繪製所花的時間,更新頁面的效率也會隨之提高。 通過VD的比較,我們可以將多個操作合併成一個批量的操作,從而減少dom重排的次數,進而縮短了生成渲染樹和繪製所花的時間。至於如何基於VD更有效率的更新dom,是一個很有趣的話題,日後有機會將另寫一篇文章介紹。

  • 如何實現VD與真實DOM的對映

我們先從如何生成VD說起。藉助JSX編譯器,可以將檔案中的HTML轉化成函式的形式,然後再利用這個函式生成VD。看下面這個例子:

JavaScript
123456789101112 functionrender(){return(<div>Hello World<ul><li id="1"class="li-1">1</li></ul></div>);}
這個函式經過JSX編譯後,會輸出下面的內容: JavaScript
12345678910111213141516 functionrender(){returnh('div',null,'Hello World',h('ul',null,h('li',{id:'1','class':'li-1'},'\u7B2C1')));}

這裡的h是一個函式,可以起任意的名字。這個名字通過babel進行配置:

12345678 // .babelrc檔案{"plugins":[["transform-react-jsx",{"pragma":"h"// 這裡可配置任意的名稱}]]}

接下來,我們只需要定義h函式,就能構造出VD

1234567891011 functionflatten(arr){return[].concat.apply([],arr);}functionh(tag,props,...children){return{tag,props:props||{},children:flatten(children)||[]};}

h函式會傳入三個或以上的引數,前兩個引數一個是標籤名,一個是屬性物件,從第三個引數開始的其它引數都是children。children元素有可能是陣列的形式,需要將陣列解構一層。比如:

JavaScript
123456789101112131415161718192021222324252627282930313233 functionrender(){return(<ul><li>0</li>{[1,2,3].map(i=>(<li>{i}</li>))}</ul>);}// JSX編譯後functionrender(){returnh('ul',null,h('li',null,'0'),/*         * 需要將下面這個陣列解構出來再放到children陣列中         */[1,2,3].map(i=>h('li',null,i)));}

繼續之前的例子。執行h函式後,最終會得到如下的VD物件:

12345678910111213141516171819 {tag:"div",props:{},children:["Hello World",{tag:"ul",props:{},children:[{tag:"li",props:{id:1,class:"li-1"},children:["第",1]}]}]}

下一步,通過遍歷VD物件,生成真實的dom

1234567891011121314151617181920212223242526272829 // 建立dom元素functioncreateElement(vdom){// 如果vdom是字串或者數字型別,則建立文字節點,比如“Hello World”if(typeof vdom==='string'||typeof vdom==='number'){returndoc.createTextNode(vdom);}const{tag,props,children}=vdom;// 1. 建立元素constelement=doc.createElement(tag);// 2. 屬性賦值setProps(element,props);// 3. 建立子元素// appendChild在執行的時候,會檢查當前的this是不是dom物件,因此要bind一下children.map(createElement).forEach(element.appendChild.bind(element));returnelement;}// 屬性賦值functionsetProps(element,props){for(let key inprops){element.setAttribute(key,props[key]);}}

createElement函式執行完後,dom元素就建立完並展示到頁面上了(頁面比較醜,不要介意…)。

clipboard.png

  • 總結

本文介紹了VD的基本概念,並講解了如何利用JSX編譯HTML標籤,然後生成VD,進而建立真實dom的過程。下一篇文章將會實現一個簡單的VD Diff演算法,找出2個VD的差異並將更新的元素對映到dom中去:你不知道的Virtual DOM(二):Virtual Dom的更新

P.S.: 想看完整程式碼見這裡,如果有必要建一個倉庫的話請留言給我:程式碼