變化這件事
談論頁面的變化之前,咱們先看下數(shù)據(jù)和頁面(視覺層面的頁面)的關系。數(shù)據(jù)是隱藏在頁面底下,通過渲染展示給用戶。同樣的數(shù)據(jù),按照不同的頁面設計和實現(xiàn),會以不同形式、樣式的頁面呈現(xiàn)出來。有時候在一個頁面內(nèi)的不同位置,也會有相同數(shù)據(jù)的不同表現(xiàn)。
Paste_Image.png
Web 的早期,這些頁面通常是靜態(tài)的,頁面內(nèi)容不會變化。而如果數(shù)據(jù)發(fā)生了變化,通常需要重新請求頁面,得到基于新的數(shù)據(jù)渲染出的新的頁面。
Paste_Image.png
至少,這個模式理解起來挺簡單不是嗎。
直到 Web 應用復雜起來,開發(fā)者們開始關注用戶體驗,開始將大量的處理向前端遷移,頁面變得動態(tài)、靈活起來。一個顯著的特征是,數(shù)據(jù)發(fā)生變化之后,不再需要刷新頁面就能看到頁面上的內(nèi)容隨之更新了。
前端需要做的事情變得多了起來,前端工程師們也就修煉了起來,各種前端技術也就出現(xiàn)了。
首先,聰明的工程師們發(fā)現(xiàn)既然是在前端渲染頁面,如果只是部分數(shù)據(jù)發(fā)生了變化,就要把頁面整體或一大塊區(qū)域重新渲染就有點笨了。為什么不把事情做得更極致些,只更新變化的數(shù)據(jù)對應的頁面的內(nèi)容呢?
怎么做呢?操作 DOM 唄。DOM 就是瀏覽器提供給開發(fā)者用于操作頁面的模型嘛,直接通過腳本來調(diào)用 DOM 的各種接口就 OK 了。而且我們還有了像 jQuery 這樣的棒棒的工具,操作 DOM 變得 so easy。
然而,頁面越來越復雜,聰明的工程師們發(fā)現(xiàn)數(shù)據(jù)變化之后,老是需要手動編碼去操作對應的 DOM 節(jié)點執(zhí)行更新,有點煩,不夠懶啊。于是各種框架如雨后春筍般出現(xiàn)了,紛紛表示可以簡化這個過程。
稍微早期的框架有這樣的:
Paste_Image.png
開發(fā)者借助框架,監(jiān)聽數(shù)據(jù)的變更,在數(shù)據(jù)變更后更新對應的 DOM 節(jié)點。雖然還是要寫一些代碼,但是寫出來的代碼好像很有條理的樣子,至少更容易理解和維護了,也不錯嘛。
更進一步,MVVM 框架出現(xiàn)了,以 AngularJS 為代表:
Paste_Image.png
仍然是數(shù)據(jù)變化后更新對應 DOM 節(jié)點的方式,但是建立這種綁定關系的過程被框架所處理,開發(fā)者要寫的代碼變少了,而且代碼更易讀和維護了。
再然后呢,大家就在這個棒棒的模式上繼續(xù)深耕,紛紛表示還可以在性能上做得更好,前端領域一片繁榮。
再后來 React 出現(xiàn)了,它不僅不是 MVVM 框架,甚至連 MV* 框架都不是。這年頭,不是個 MV* 框架還好意思出門?可 React 還真的帶來了新的思路!
什么思路呢?
就是回到過去,回到那個簡單而美好的時候。具體而言,就是每次數(shù)據(jù)發(fā)生變化,就重新執(zhí)行一次整體渲染。的確這樣更簡單,不用去琢磨到底是數(shù)據(jù)的哪一部分變化了,需要更新頁面的哪一部分。但是壞處太明顯,體驗不好啊。而 React 給出了解決方案,就是 Virtual DOM。
Virtual DOM 概況來講,就是在數(shù)據(jù)和真實 DOM 之間建立了一層緩沖。對于開發(fā)者而言,數(shù)據(jù)變化了就調(diào)用 React 的渲染方法,而 React 并不是直接得到新的 DOM 進行替換,而是先生成 Virtual DOM,與上一次渲染得到的 Virtual DOM 進行比對,在渲染得到的 Virtual DOM 上發(fā)現(xiàn)變化,然后將變化的地方更新到真實 DOM 上。
簡單來說,React 在提供給開發(fā)者簡單的開發(fā)模式的情況下,借助 Virtual DOM 實現(xiàn)了性能上的優(yōu)化,以致于敢說自己“不慢”。
Virtual DOM
React 基于 Virtual DOM 的數(shù)據(jù)更新與UI同步機制:
React – 初始渲染
初始渲染時,首先將數(shù)據(jù)渲染為 Virtual DOM,然后由 Virtual DOM 生成 DOM。
React – 數(shù)據(jù)更新
數(shù)據(jù)更新時,渲染得到新的 Virtual DOM,與上一次得到的 Virtual DOM 進行 diff,得到所有需要在 DOM 上進行的變更,然后在 patch 過程中應用到 DOM 上實現(xiàn)UI的同步更新。
Virtual DOM 作為數(shù)據(jù)結(jié)構(gòu),需要能準確地轉(zhuǎn)換為真實 DOM,并且方便進行對比。除了 Virtual DOM 外,React 還實現(xiàn)了其他的特性,為了專注于 Virtual DOM,我另外找了兩個比較 Virtual DOM 來學習:
virtual-dom
Snabbdom
這里也推薦給感興趣且還沒有讀過兩個庫源碼的同學。
由于只關注 Virtual DOM,通過閱讀兩個庫的源碼,對于 Virtual DOM 的定位有了更深一步的理解。
首先看數(shù)據(jù)結(jié)構(gòu)。
Virtual DOM 數(shù)據(jù)結(jié)構(gòu)
DOM 通常被視為一棵樹,元素則是這棵樹上的節(jié)點(node),而 Virtual DOM 的基礎,就是 Virtual Node 了。
在 virtual-dom 中,給 Virtual Node 聲明了對應的類 VirtualNode,基本是用于存儲數(shù)據(jù),包括:
tagName
properties
children
key
namespace
count
hasWidgets
hasThunks
hooks
descendantHooks
Snabbdom 的 Virtual Node 則是純數(shù)據(jù)對象,通過 vnode 模塊來創(chuàng)建,對象屬性包括:
sel
data
children
text
elm
key
雖然有所差別,除去實現(xiàn)上的差別和庫本身的額外特性,可以看到 Virtual Node 用于創(chuàng)建真實節(jié)點的數(shù)據(jù)包括:
元素類型
元素屬性
元素的子節(jié)點
有了這些其實就可以創(chuàng)建對應的真實節(jié)點了。
創(chuàng)建 Virtual DOM
嵌套 Virtual Node 就可以得到一棵樹了。virtual-dom 和 Snabbdom 都提供了函數(shù)調(diào)用的方式來創(chuàng)建 Virtual Tree,這個過程就是渲染了:
JavaScript
var vTree = h('div', [
h('span', 'hello'),
h('span', 'world')
])
React 提供 JSX 這顆糖,使得我們可以用類似 HTML 的語法來編寫,不過編譯后實質(zhì)還是通過函數(shù)調(diào)用來得到一棵嵌套的 Virtual Tree。而且這對于理解 Virtual DOM 機制來說不是特別重要,先不管這個。
使用 Virtual DOM
首先來看初始化,virtual-dom 提供了 createElement 函數(shù):
JavaScript
var rootNode = createElement(tree)
document.body.appendChild(rootNode)
根據(jù) Virtual Node 創(chuàng)建真實 DOM 元素,然后再追加到頁面上。
再來看更新。virtual-dom 有明確的兩步操作,首先 diff,然后 patch:
JavaScript
var newTree = render(count)
var patches = diff(tree, newTree)
rootNode = patch(rootNode, patches)
而 Snabbdom 則簡單些,只有一個 patch 函數(shù),內(nèi)部在進行比對的同時將更新應用到了真實 DOM 上,而且初始化也是用的 patch 函數(shù):
JavaScript
var vnode = render(data)
var container = document.getElementById('container')
patch(container, vnode)
// after data changed
var newVnode = render(data)
patch(vnode, newVnode)
性能優(yōu)化
關于性能優(yōu)化,除了 Virtual DOM 機制本身提供的特性以外,再就是不同的 Virtual DOM 庫自身的優(yōu)化方案了,這個可以看上面兩個庫的文檔,不再贅述。
其實提到 Virtual DOM 的差異比對,有人會對其內(nèi)部如何處理數(shù)組感興趣。的確,如果數(shù)組元素的位置發(fā)生了改變,這個要識別起來是有點麻煩。為此,上面兩個庫和 React 其實都在 Virtual Node 上額外記錄了一個屬性“key”,就是用來輔助進行 Virtual Node 的比對的。
簡單來說,如果兩個 Virtual Node 的位置不同,但是 key 屬性相同,那么會將這兩個節(jié)點視為由相同數(shù)據(jù)渲染得到的,然后進一步進行差異分析。所以,并不是僅僅按照位置進行比對,具體的實現(xiàn)可以查看各個庫的源碼。
小結(jié)
OK,以上就是我要講的全部所有內(nèi)容了。
相信很多同學之前對 Virtual DOM 已經(jīng)很熟悉了,比我理解得更深入的同學相信也不會少。不過從“數(shù)據(jù)變化與UI同步更新”這個角度來理解 Virtual DOM,在我看來是比較好的,所以整理在這里了。
有個問題挺常見,AngularJS 和 React 哪個更好?
如果說各有千秋的話,估計大家就“呵呵”了。但是這兩個框架/庫從“數(shù)據(jù)變化與UI同步更新”的角度來看,的確都解決了問題,而且解決問題的方式大家都挺認可(至少在喜歡它們的同學眼里是這樣的)。
而且,如果大家關注 Vue 的話,可以看到,這個 MVVM 框架已經(jīng)發(fā)布了 2.0,其中就采用了 Virtual DOM 實現(xiàn)其UI同步更新!所以,這的確不矛盾啊。
第二個而且,技術本身不是目的,能夠更好地解決問題才是王道嘛。





