Vue.js 2.0引入Virtual DOM,比Vue.js 1.0的初始渲染速度提高了2-4倍,並大大下降了內存消耗。那麼,什麼是Virtual DOM?爲何須要Virtual DOM?它是經過什麼方式去提高頁面渲染效率的呢?這是本文所要探討的問題。html
在正式介紹 Virtual Dom以前,咱們有必要先了解下模板轉換成視圖的過程整個過程,以下圖(參考《深刻淺出vue.js》):前端
簡單點講,在Vue的底層實現上,Vue將模板編譯成虛擬DOM渲染函數。結合Vue自帶的響應系統,在狀態改變時,Vue可以智能地計算出從新渲染組件的最小代價並應到DOM操做上。vue
Virtual DOM 其實就是一棵以 JavaScript 對象( VNode 節點)做爲基礎的樹,用對象屬性來描述節點,實際上它只是一層對真實 DOM 的抽象。最終能夠經過一系列操做使這棵樹映射到真實環境上。node
簡單來講,能夠把Virtual DOM 理解爲一個簡單的JS對象,而且最少包含標籤名( tag)、屬性(attrs)和子元素對象( children)三個屬性。不一樣的框架對這三個屬性的命名會有點差異。git
對於虛擬DOM,我們來看一個簡單的實例,就是下圖所示的這個,詳細的闡述了模板 → 渲染函數 → 虛擬DOM樹 → 真實DOM
的一個過程 github
虛擬DOM的最終目標是將虛擬節點渲染到視圖上。可是若是直接使用虛擬節點覆蓋舊節點的話,會有不少沒必要要的DOM操做。例如,一個ul標籤下不少個li標籤,其中只有一個li有變化,這種狀況下若是使用新的ul去替代舊的ul,由於這些沒必要要的DOM操做而形成了性能上的浪費。面試
爲了不沒必要要的DOM操做,虛擬DOM在虛擬節點映射到視圖的過程當中,將虛擬節點與上一次渲染視圖所使用的舊虛擬節點(oldVnode)作對比,找出真正須要更新的節點來進行DOM操做,從而避免操做其餘無需改動的DOM。算法
其實虛擬DOM在Vue.js主要作了兩件事:瀏覽器
因爲 Virtual DOM 是以 JavaScript 對象爲基礎而不依賴真實平臺環境,因此使它具備了跨平臺的能力,好比說瀏覽器平臺、Weex、Node 等。緩存
由於DOM操做的執行速度遠不如Javascript的運算速度快,所以,把大量的DOM操做搬運到Javascript中,運用patching算法來計算出真正須要更新的節點,最大限度地減小DOM操做,從而顯著提升性能。
Virtual DOM 本質上就是在 JS 和 DOM 之間作了一個緩存。能夠類比 CPU 和硬盤,既然硬盤這麼慢,咱們就在它們之間加個緩存:既然 DOM 這麼慢,咱們就在它們 JS 和 DOM 之間加個緩存。CPU(JS)只操做內存(Virtual DOM),最後的時候再把變動寫入硬盤(DOM)
Virtual DOM的優點不在於單次的操做,而是在大量、頻繁的數據更新下,可以對視圖進行合理、高效的更新。
爲了實現高效的DOM操做,一套高效的虛擬DOM diff算法顯得頗有必要。咱們經過patch 的核心----diff 算法,找出本次DOM須要更新的節點來更新,其餘的不更新。那diff 算法的實現過程是怎樣的?
Vue的diff算法是基於snabbdom改造過來的,僅在同級的vnode間作diff,遞歸地進行同級vnode的diff,最終實現整個DOM樹的更新。由於跨層級的操做是很是少的,忽略不計,這樣時間複雜度就從O(n3)變成O(n)。
diff 算法包括幾個步驟:
diff 算法自己很是複雜,實現難度很大。本文去繁就簡,粗略介紹如下兩個核心函數實現流程:
經過這個函數可讓VNode渲染成真正的DOM,咱們經過如下模擬代碼,能夠了解大體過程:
function createElement(vnode) {
var tag = vnode.tag
var attrs = vnode.attrs || {}
var children = vnode.children || []
if (!tag) {
return null
}
// 建立真實的 DOM 元素
var elem = document.createElement(tag)
// 屬性
var attrName
for (attrName in attrs) {
if (attrs.hasOwnProperty(attrName)) {
// 給 elem 添加屬性
elem.setAttribute(attrName, attrs[attrName])
}
}
// 子元素
children.forEach(function (childVnode) {
// 給 elem 添加子元素,若是還有子節點,則遞歸的生成子節點。
elem.appendChild(createElement(childVnode)) // 遞歸
}) // 返回真實的 DOM 元素
return elem
}
複製代碼
這裏咱們只考慮vnode與newVnode如何對比的狀況:
function updateChildren(vnode, newVnode) {
var children = vnode.children || []
var newChildren = newVnode.children || []
// 遍歷現有的children
children.forEach(function (childVnode, index) {
var newChildVnode = newChildren[index]
// 二者tag同樣
if (childVnode.tag === newChildVnode.tag) {
// 深層次對比,遞歸
updateChildren(childVnode, newChildVnode)
} else {
// 二者tag不同
replaceNode(childVnode, newChildVnode)
}
}
)}
複製代碼
給你們推薦一個好用的BUG監控工具Fundebug,歡迎免費試用!
歡迎關注公衆號:前端工匠,你的成長咱們一塊兒見證!