目錄:前端
1 前言node
2 技術發展史react
4 Virtual DOM 實現angularjs
5 Virtual DOM 樹的差別(Diff算法)github
6 結語web
7 參考連接算法
我會盡可能把 Virtual DOM 應用場景、實現思路、算法講述清楚,但願你們閱讀後,能讓你後端
深刻理解 Virtual DOM。
寫一個像下面的應用程序,這個表格能夠根據不一樣的字段進行升序或者降序。
最容易的方案是在你的 JavaScript 代碼裏面存儲這樣的數據:
var sortKey = "name" // 排序的字段,名稱(name)、年齡(age) var sortType = "ASC" // 升序仍是逆序 var data = [{...}, {...}, {..}, ..] // 表格數據
用三個變量分別存儲當前排序的字段、排序方向、還有表格數據;而後給表格頭部加
點擊事件,當用戶點擊排序字段時,會根據上面幾個變量存儲的值來對內容進行排序,
而後用 JS 操做 DOM,更新頁面的排序狀態和表格內容。
這麼作會致使一個問題:應用愈來愈複雜,須要在 JS 裏面維護的字段也愈來愈多,需
要監聽的事件和在事件中回調更新頁面的 DOM 也愈來愈多,最終應用變得難以維護。
以後研究出了 MVC、MVP 的架構模式,但願從代碼組織的方式來下降維護複雜應用的
難度。可是 MVC 架構沒辦法減小你所維護的狀態,也沒有下降狀態更新時需要對頁面
的更新操做,你須要操做的 DOM 仍是須要操做,只是換了個地方而已。
既然狀態改變了要操做相應的 DOM 元素,爲何不作一個東西可讓視圖和狀態進行
綁定?讓狀態變動視圖自動跟着變動,就不用手動更新頁面了。這就是後來的 MVVM
模式,只要在模版中聲明視圖組件是和什麼狀態進行綁定的,雙向綁定引擎就會在狀態
更新的時候自動更新視圖,MVVM 能夠能很好的下降維護狀態以及減小視圖的複雜程度。
但這不是惟一辦法,還有一個很是直觀的方法,能夠大大下降視圖更新的操做。一旦狀
態發生了變化,就用模版引擎從新渲染整個視圖,而後用新的視圖更換掉舊的視圖。就
像上面的表格,當用戶點擊的時,仍是在 JS 裏面更新狀態,可是頁面更新就不用手動
操做 DOM 了,直接把整個表格用模版引擎從新渲染一遍,而後設置一下 innerHTML 。
那麼這個方法會有個很大的問題,會致使 DOM 操做變慢,由於任何的狀態變動都要重
新構造整個 DOM,性價比很低。對於局部的小視圖的更新,這樣沒有問題(backbone
就是這麼幹的)。但對於大型視圖,須要更新頁面較多局部視圖時,這樣的作法就很是不
可取。
Virtual DOM 也是這麼作的,只是加了一些步驟來避免了整棵 DOM 樹變動。上面提供
的幾種方法,其實都在解決同一個問題,那就是維護狀態更新視圖。若是咱們可以很好來
應對這個問題,就下降複雜性。
DOM 很慢,爲啥說它慢,先看一下 Webkit 引擎,全部瀏覽器都遵循相似的工做流,只
是在細節處理有些不一樣。一旦瀏覽器接收到一個 HTML 文件,渲染引擎 Render Engine
就開始解析它,根據 HTML 元素 Elements 對應地生成 DOM 節點 Nodes,最終組成一
棵 DOM 樹。
構造了渲染樹之後,瀏覽器引擎開始着手佈局 Layout。佈局時,渲染樹上的每一個節點根據
其在屏幕上應該出現的精確位置,分配一組屏幕座標值。接着,瀏覽器將會經過遍歷渲染樹,
調用每一個節點的 Paint 方法來繪製這些 Render 對象。Paint 方法根據瀏覽器平臺,使用不
同的 UI後端 API(Agnostic UI Backend API)經過繪製,最終將在屏幕上展現內容。只要
在這過程當中進行一次 DOM 更新,整個渲染流程都會重作一遍。
把一個簡單的 div 元素的屬性都打印出來,你會看這些。
align, onwaiting, onvolumechange, ontimeupdate, onsuspend, onsubmit,
onstalled, onshow, onselect, onseeking, onseeked, onscroll, onresize,
onreset, onratechange, onprogress, onplaying, onplay, onpause,
onmousewheel, onmouseup, onmouseover, onmouseout, onmousemove,
onmouseleave, onmouseenter, onmousedown, onloadstart,
onloadedmetadata, onloadeddata, onload, onkeyup, onkeypress,
onkeydown, oninvalid, oninput, onfocus, onerror, onended, onemptied,
ondurationchange, ondrop, ondragstart, ondragover, ondragleave,
ondragenter, ondragend, ondrag, ondblclick, oncuechange,
oncontextmenu, onclose, onclick, onchange, oncanplaythrough,
oncanplay, oncancel, onblur, onabort, spellcheck, isContentEditable,
contentEditable, outerText, innerText, accessKey, hidden,
webkitdropzone, draggable, tabIndex, dir, translate, lang, title,
childElementCount, lastElementChild, firstElementChild, children,
nextElementSibling, previousElementSibling, onwheel,
onwebkitfullscreenerror, onwebkitfullscreenchange, onselectstart,
onsearch, onpaste, oncut, oncopy, onbeforepaste, onbeforecut,
onbeforecopy, webkitShadowRoot, dataset, classList, className,
outerHTML, innerHTML, scrollHeight, scrollWidth, scrollTop,
scrollLeft, clientHeight, clientWidth, clientTop, clientLeft,
offsetParent, offsetHeight, offsetWidth, offsetTop, offsetLeft,
localName, prefix, namespaceURI, id, style, attributes, tagName,
parentElement, textContent, baseURI, ownerDocument, nextSibling,
previousSibling, lastChild, firstChild, childNodes, parentNode,
nodeType, nodeValue, nodeName
來看看空的 div 元素有多少屬性要實現,這還只是第一層的自有屬性,沒包括原型鏈繼承而來
的。若是觸發了頁面事件,就就會致使頁面重排。相對於 DOM 對象,原生的 JavaScript 處理
起來纔會更快且更簡單。
DOM 樹上的結構、屬性信息咱們均可以很容易地用 JavaScript 對象表示出來。
var olE = { tagName: 'ol', // 標籤名 props: { // 屬性用對象存儲鍵值對 id: 'ol-list' }, children: [ // 子節點 {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]}, ] }
對應 HTML 寫法是:
<ol id='ol-list'> <li class='item'>Item 1</li> <li class='item'>Item 2</li> <li class='item'>Item 3</li> </ol>
DOM 咱們均可以用 JavaScript 對象來表示。那反過來,就能夠用 JavaScript 對象表示的樹結
構來構建一個真正的 DOM 。當狀態變動時,從新渲染這個 JavaScript 的對象結構,實現視圖
的變動,結構根據變動的地方從新渲染。
這就是所謂的 Virtual DOM 算法:
用 JavaScript 對象結構表示 DOM 樹的結構;而後用這個樹構建一個真正的 DOM 樹,插到文
檔當中當狀態變動時,從新構造一棵新的對象樹。而後用新的樹和舊的樹進行比較兩個數的差別。
而後把差別更新到久的樹上,整個視圖就更新了。Virtual DOM 本質就是在 JS 和 DOM 之間作
了一個緩存。既然已經知道 DOM 慢,就在 JS 和 DOM 之間加個緩存。JS 先操做 Virtual DOM
對比排序/變動,最後再把整個變動寫入真實 DOM。
用 JavaScript 表示一個 DOM 節點非(wo)常(cui)的(niu)簡(bi)單(ne),只須要記
錄它的節點類型、屬性,還有子節點。
export default Ele = (tagName, props, children) => { this.tagName = tagName this.props = props this.children = children }
<ol id='ol-list'> <li class='item'>Item 1</li> <li class='item'>Item 2</li> <li class='item'>Item 3</li> </ol>
例如上面的 DOM 結構就能夠簡單的表示:
import * as el from 'Ele'; var ol = el('ol', {id: 'ol-list'}, [ el('li', {class: 'item'}, ['Item 1']), el('li', {class: 'item'}, ['Item 2']), el('li', {class: 'item'}, ['Item 3']) ]);
如今 ol 只是一個 JavaScript 對象表示的 DOM 結構,但頁面上並無這個結構。咱們可以根據這
個 ol 構建來生成真正的 ol。新增一個 render 方法,根據 tagName 構建一個真正的 DOM,然
後生成 DOM 屬性、鏈接子結構等等。
Ele.prototype.render = function () { var e = document.createElement(this.tagName); // 建立元素 var props = this.props; for (var propName in props) { // 設置 DOM 屬性 var propValue = props[propName]; e.setAttribute(propName, propValue); } var children = this.children || []; children.forEach(function (child) { var childE = (child instanceof Element) ? child.render() // 子節點也是虛擬 DOM,遞歸構建 : document.createTextNode(child); // 字符串,構建文本節點 e.appendChild(childE); }); return e; }
最後只須要 render。
var olE = Ele.render() document.body.appendChild(olE);
上面的 olE 是真正的 DOM 節點,把它 append 到 body 中,這樣就有了真正的 ol DOM 元素。
<ol id='ol-list'> <li class='item'>Item 1</li> <li class='item'>Item 2</li> <li class='item'>Item 3</li> </ol>
比較兩個 DOM 樹的差別是 Virtual DOM 算法最核心的部分,這也是所謂的 Virtual DOM 的
diff 算法。在前端當中,不多會跨越層級地移動 DOM 元素。因此 Virtual DOM 只會對同一個
層級的元素進行對比,下面的 div 只會和同一層級的 div 對比,第二層級的只會跟第二層級對
比。採用的是深度優先遍歷,來記錄差別,這樣每一個節點都會有一個惟一的標記。
差別是指的是什麼呢?DOM 替換掉原來的節點,如把上面的 div 換成了 section 進行移動、刪
除、新增子節點,例如上面 div 的子節點,把 p 和 span 順序互換修改了節點的屬性。對於文本
節點,文本內容可能會改變。
若是我把左側的 p、span、div 反過來變成 div、p、span 怎麼辦?按照差別正常會被替換掉,
但這樣 DOM開銷就會異常的大了。而 React 幫咱們作到不須要替換節點,而只須要通過節點移
動就能夠達到。至於怎麼變更,會牽扯到太多的對比算法不一一介紹,有興趣的瞭解下列表對比
算法,詳細見5參考連接。
雖然只是很是粗糙的實踐,但我相信 Virtual DOM 的原理是講述通了。實際還須要處理事件監聽、
狀態監控。生成虛擬 DOM 時也能夠加入 JSX 語法。固然這些事情都作了的話,就能夠構造一個簡
單的ReactJS了。
列表算法(Edit distance):https://en.wikipedia.org/wiki/Edit_distance
列表算法(Levenshtein distance):https://en.wikipedia.org/wiki/Levenshtein_distance
參考文章(圖):http://www.javashuo.com/article/p-ghbzqlpi-ck.html
參考文章(性能原理):http://www.williambrownstreet.net/blog/2014/04/faster-angularjs-rendering-angularjs-and-reactjs/
參考文章(瀏覽器工做流):http://www.jianshu.com/p/f75c1f0af3f0
參考文章(Webkit相關):https://webkit.org/blog/
參考代碼(Vtree):https://github.com/Matt-Esch/virtual-dom/blob/master/vtree/diff.js