深刻淺出理解virtual Dom

 什麼是virtual Dom?

Virtual Dom 是虛擬DOM,咱們用JS來模擬DOM結構,結構相似下面的代碼:javascript

{
    tag:'ul',
    attrs:{
        id:'list'
    }
    children:[
    {
        tag:'li',
        attrs:{className:'item'},
        children:['item 1']
    },
    {        tag:'li',
        attrs:{className:'item'},
        children:['item 2']
    }
  ]
}複製代碼

以上代碼模擬的就是這樣的DOM結構css

<ul>
    <li class='item'>item 1</li>
    <li class='item'>item 2</li></ul>複製代碼

那麼爲何會有VDOM(virtual dom簡稱)這樣的結構呢?html


爲何會有Virtual DOM?

咱們來模擬這樣的一個場景需求。vue

1.有一堆數據,須要將數據渲染成表格java

2.隨便修改一個信息,表格也會跟着變化node

若是沒有VDOM,咱們會用這樣的代碼來完成需求jquery

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Document</title></head><body> <div id="container"></div> <button id="btn-change">change</button> <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script> <script type="text/javascript"> var data = [ { name: '張三', age: '20', address: '北京' }, { name: '李四', age: '21', address: '上海' }, { name: '王五', age: '22', address: '廣州' } ] // 渲染函數  function render(data) { var $container = $('#container') // 清空容器,重要!!! $container.html('') // 拼接 table var $table = $('<table>') $table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>')) data.forEach(function (item) { $table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td>/tr>')) }) // 渲染到頁面  $container.append($table) } $('#btn-change').click(function () { data[1].age = 30 data[2].address = '深圳' // re-render 再次渲染  render(data) }) // 頁面加載完馬上執行(初次渲染) render(data) </script> </body> </html> 複製代碼

上面的代碼雖然完成了需求,可是,遺憾的是,若是我只是修改一部分數據,整個table算法

都須要所有渲染。對於瀏覽器而言,渲染DOM是一個很是「昂貴「的過程。那麼,有沒有什麼辦法,修改部分數據的時候,只是渲染我修改的DOM呢?瀏覽器


使用VDOM實現只渲染修改的DOM

咱們首先來使用一下snabbdom這個庫,它會利用VDOM來實現局部渲染。一塊兒來感覺一下bash

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Document</title></head><body> <div id="container"></div> <button id="btn-change">change</button> <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-class.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-props.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-style.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-eventlisteners.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.0/h.js"></script> <script type="text/javascript"> var snabbdom = window.snabbdom // 定義關鍵函數 patch  var patch = snabbdom.init([ snabbdom_class, snabbdom_props, snabbdom_style, snabbdom_eventlisteners ]) // 定義關鍵函數 h  var h = snabbdom.h // 原始數據  var data = [ { name: '張三', age: '20', address: '北京' }, { name: '李四', age: '21', address: '上海' }, { name: '王五', age: '22', address: '廣州' } ] // 把表頭也放在 data 中 data.unshift({ name: '姓名', age: '年齡', address: '地址' }) var container = document.getElementById('container') // 渲染函數 var vnode function render(data) { var newVnode = h('table', {}, data.map(function (item) { var tds = [] var i for (i in item) { if (item.hasOwnProperty(i)) { tds.push(h('td', {}, item[i] + '')) } } return h('tr', {}, tds) })) if (vnode) { // 若是已經渲染了 patch(vnode, newVnode) } else { // 初次渲染 patch(container, newVnode) } // 存儲當前的 vnode 結果  vnode = newVnode } // 初次渲染 render(data) var btnChange = document.getElementById('btn-change') btnChange.addEventListener('click', function () { data[1].age = 30 data[2].address = '深圳' // re-render render(data) }) </script> </body> </html>複製代碼


以上代碼,則實現了當你修改了部分數據時,只渲染一部分數據。

那麼,以上代碼的核心就是兩個函數,咱們須要對此來作探討。一個是函數h,一個是函數patch


關鍵函數h和關鍵函數patch

關鍵函數h

函數h返回的值是一個vnode,也就是虛擬DOM節點,如圖所示


也就是說使用h函數能夠生成相似於右邊的vnode結構。


關鍵函數patch

那麼關鍵函數patch的做用,則是將vnode渲染成真實的DOM節點,而後塞入到容器裏面。

若是容器裏面已經有生成好的vnode,那麼,則會將新生成的newVnode和以前的vnode相比較,而後將不一樣的節點找出來,而後代替舊的節點。


到如今爲止,已經基本上了解了VDOM的含義以及爲何會用VDOM,咱們來作一個簡單的總結

  1. 若是隻有部分數據變更,卻要所以渲染整個DOM
  2. DOM是很是「昂貴」的操做,所以咱們須要減小DOM操做
  3. 找出必需要更新的節點,其餘的則能夠不更新


那麼,接下來又出現了一個問題,咱們如何知道哪一個節點須要更新呢?這就是diff算法的做用


diff算法找出須要更新的DOM

由於diff算法自己太過於複雜,因此只須要理解一下核心的思想便可。

那麼咱們只須要關注在渲染的時候,發生了什麼事情,理解下面這兩個事件的核心流程便可。

  1. patch(container, newVnode)
  2. patch(vnode, newVnode)

也就是說,咱們須要理解的是:

  1. 初次渲染的時候,將VDOM渲染成真正的DOM而後插入到容器裏面。
  2. 再次渲染的時候,將新的vnode和舊的vnode相對比,而後如何進行局部渲染的過程。


1.patch(container, newVnode)

咱們要實現的是這樣的過程:


咱們來模擬一下上面的建立過程,只是僞代碼,咱們瞭解大體的流程

function createElement(vnode) {    
var tag = vnode.tag  // 'ul' 
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
}複製代碼

那麼,經過上面的模擬代碼,已經能夠很好的瞭解最開始將vdom渲染到容器的過程。


2.patch(vnode, newVnode)

這個過程就是將newVnode和vnode對比,將差別進行渲染的部分。



那麼僞代碼流程以下:

function updateChildren(vnode, newVnode) {
    var children = vnode.children || []
    var newChildren = newVnode.children || []
    children.forEach(function (childVnode, index) {
        var newChildVnode = newChildren[index]
        if (childVnode.tag === newChildVnode.tag) {
            // 深層次對比,遞歸
            updateChildren(childVnode, newChildVnode)
        } else { 
           // 替換 
           replaceNode(childVnode, newChildVnode) 
       }
    }
)}
function replaceNode(vnode, newVnode) {
    var elem = vnode.elem  // 取得舊的 真實的 DOM 節點
    var newElem = createElement(newVnode)//生成新的真實的dom節點 
   // 替換
}複製代碼

那麼真正的替換過程有哪些呢?簡單的總結一下:

  • 找到對應的真實dom,稱爲elem
  • 判斷newVnodeoldVnode是否指向同一個對象,若是是,那麼直接return
  • 若是他們都有文本節點而且不相等,那麼將el的文本節點設置爲Vnode的文本節點。
  • 若是oldVnode有子節點而newVnode沒有,則刪除el的子節點
  • 若是oldVnode沒有子節點而newVnode有,則將Vnode的子節點真實化以後添加到elem
  • 若是二者都有子節點,則執行updateChildren函數比較子節點,這一步很重要,請參考這篇文章


以上只是簡單的理解了diff算法的流程,關於更多的diff算法的詳細過程,能夠閱讀參考文章。


參考文章

詳解vue的diff算法

相關文章
相關標籤/搜索