摘要: 什麼是虛擬DOM?javascript
Fundebug經受權轉載,版權歸原做者全部。vue
Vue.js 2.0引入Virtual DOM,比Vue.js 1.0的初始渲染速度提高了2-4倍,並大大下降了內存消耗。那麼,什麼是Virtual DOM?爲何須要Virtual DOM?它是經過什麼方式去提高頁面渲染效率的呢?這是本文所要探討的問題。java
在正式介紹 Virtual Dom以前,咱們有必要先了解下模板轉換成視圖的過程整個過程(以下圖):node
簡單點講,在Vue的底層實現上,Vue將模板編譯成虛擬DOM渲染函數。結合Vue自帶的響應系統,在狀態改變時,Vue可以智能地計算出從新渲染組件的最小代價並應到DOM操做上。git
咱們先對上圖幾個概念加以解釋:github
Virtual DOM 其實就是一棵以 JavaScript 對象( VNode 節點)做爲基礎的樹,用對象屬性來描述節點,實際上它只是一層對真實 DOM 的抽象。最終能夠經過一系列操做使這棵樹映射到真實環境上。算法
簡單來講,能夠把Virtual DOM 理解爲一個簡單的JS對象,而且最少包含標籤名( tag)、屬性(attrs)和子元素對象( children)三個屬性。不一樣的框架對這三個屬性的命名會有點差異。編程
對於虛擬DOM,我們來看一個簡單的實例,就是下圖所示的這個,詳細的闡述了模板 → 渲染函數 → 虛擬DOM樹 → 真實DOM
的一個過程小程序
虛擬DOM的最終目標是將虛擬節點渲染到視圖上。可是若是直接使用虛擬節點覆蓋舊節點的話,會有不少沒必要要的DOM操做。例如,一個ul標籤下不少個li標籤,其中只有一個li有變化,這種狀況下若是使用新的ul去替代舊的ul,由於這些沒必要要的DOM操做而形成了性能上的浪費。segmentfault
爲了不沒必要要的DOM操做,虛擬DOM在虛擬節點映射到視圖的過程當中,將虛擬節點與上一次渲染視圖所使用的舊虛擬節點(oldVnode)作對比,找出真正須要更新的節點來進行DOM操做,從而避免操做其餘無需改動的DOM。
其實虛擬DOM在Vue.js主要作了兩件事:
給你們推薦一個好用的BUG監控工具Fundebug,歡迎免費試用!
因爲 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須要更新的節點來更新,其餘的不更新。好比修改某個model 100次,從1加到100,那麼有了Virtual DOM的緩存以後,只會把最後一次修改patch到view上。那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,歡迎免費試用!
Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有陽光保險、核桃編程、荔枝FM、掌門1對一、微脈、青團社等衆多品牌企業。歡迎你們免費試用!