Virtual DOM的簡單實現

来源:https://www.cnblogs.com/isLiu/archive/2018/01/21/8325186.html
-Advertisement-
Play Games

瞭解React的同學都知道,React提供了一個高效的視圖更新機制:Virtual DOM,因為DOM天生就慢,所以操作DOM的時候要小心翼翼,稍微改動就會觸發重繪重排,大量消耗性能。 1.Virtual DOM Virtual DOM是利用JS的原生對象來模擬DOM,既然DOM是對象,我們也可以用 ...


瞭解React的同學都知道,React提供了一個高效的視圖更新機制:Virtual DOM,因為DOM天生就慢,所以操作DOM的時候要小心翼翼,稍微改動就會觸發重繪重排,大量消耗性能。

1.Virtual DOM


Virtual DOM是利用JS的原生對象來模擬DOM,既然DOM是對象,我們也可以用原生的對象來表示DOM。

var element = {
  tagName: 'ul', // 節點標簽名
  props: {
    class: 'list' // 節點的屬性,ID,class...
  },
  children: [ // 該節點的子節點
    {tagName: 'li', props: {class: 'item'}, children: ['item one']},
    {tagName: 'li', props: {class: 'item'}, children: ['item two']},
    {tagName: 'li', props: {class: 'item'}, children: ['item three']}
  ]
}

對應成相應的HTML結構為:

<ul class="list">
    <li class="item">item one</li>
    <li class="item">item two</li>
    <li class="item">item three</li>
</ul>

但是這又有什麼用呢?不還是要操作DOM嗎?

開頭我們就說過,Virtual DOM是一個高效的視圖更新機制,沒錯,主要在更新。怎麼更新呢,那就要用到了我們之前用JS對象模擬的DOM樹了,就叫它對象樹把,我們對比前後兩棵對象樹,比較出需要更新視圖的地方,對需要更新視圖的地方纔進行DOM操作,不需要更新的地方自然什麼都不做,這就避免了性能的不必要浪費,變相的提升了性能。

總之Virtual DOM演算法主要包括這幾步:

  • 初始化視圖的時候,用原生JS對象表示DOM樹,生成一個對象樹,然後根據這個對象樹來生成一個真正的DOM樹,插入到文檔中。

  • 當狀態更新的時候,重新生成一個對象樹,將新舊兩個對象樹做對比,記錄差異。

  • 把記錄的差異應用到第一步生成的真正的DOM樹上,視圖跟新完成

其實就是一個雙緩衝的原理,既然CPU這麼快,讀取硬碟又這麼慢,我們就在中間加一個Cache。那麼,既然DOM操作也慢,我們們就可以在JS和DOM之間也加一個Cache,這個Cache就是我們的Virtual DOM了。

其實說白了Virtual DOM的原理就是只更新需要更新的地方,其他的一概不管。

2.用對象樹表示DOM樹


用JS對象表示DOM節點還是比較容易的,我們這需要記錄DOM節點的節點類型、屬性、還有子節點就好了。

class objectTree {
  constructor (tagName, props, children) {
    this.tagName = tagName
    this.props = props
    this.children = children
  }
}

我們可以通過這種方式創建一個對象樹:

var ul = new objectTree('ul', {id: 'list'}, [
  createObjectTree('li', {class: 'item'}, ['Item 1']),
  createObjectTree('li', {class: 'item'}, ['Item 2']),
  createObjectTree('li', {class: 'item'}, ['Item 3'])
])

對象樹存在一個render方法來將對象樹轉換成真正的DOM樹:

objectTree.prototype.render = function () {
  var elm = document.createElement(this.tagName)

  var props = this.props
  // 設置DOM節點的屬性
  for (var key in props) {
    elm.setAttribute(key, props[key])
  }

  var children = this.children || []
  children.forEach((child) => {
    // 如果子節點也是對象樹,則遞歸渲染,否則就是文本節點
    var childElm = (child instanceof objectTree) ? child.render() : document.createTextNode(child)
    elm.appendChild(childElm)
  })

  return elm
}

我們就可以將生成好的DOM樹插入到文檔里了

var ul = new objectTree('ul', {id: 'list'}, [
  new objectTree('li', {class: 'item'}, ['Item 1']),
  new objectTree('li', {class: 'item'}, ['Item 2']),
  new objectTree('li', {class: 'item'}, ['Item 3'])
])

console.log(ul)

document.body.appendChild(ul.render())

我們生成的DOM已經添加到文檔里了

3.比較兩個對象樹的差異


所謂Virtual DOM的diff演算法,就是比較兩個對象樹的差異,也正是Virtual DOM的核心。

傳統的比較兩棵樹差異的演算法,時間複雜度是O(n^3),大量操作DOM的時候肯定是接受不了的。所以React做了妥協,React結合WEB界面的特點,做了兩個簡單的假設,使得演算法的複雜度降低到了O(n)。

  1. 相同的組件產生相似的DOM樹,不同的組件產生不同的DOM樹。

  2. 對於同一層次的一組子節點,它們可以通過唯一的id進行區分。

不同節點類型的比較

不同節點類型分為兩種情況:

  1. 節點類型不同。

  2. 節點類型相同,但是屬性不同。

先看第一種情況,如果是我們會怎麼做呢?肯定是直接刪除老的節點,然後在老節點的位置上將新節點插入。React也和我們的想法一樣,也符合我們對真實DOM操作的理解。

如果將老節點刪除,那麼老節點的子節點也同時被刪除,並且子節點也不會參與後續的比較。這也是演算法複雜度能降低到O(n)的原因之一。

既然節點類型不同是這樣操作的,那麼組件也是一樣的邏輯了。應用第一個假設,不同組件之間有不同的DOM樹,與其花時間比較它們的DOM結構,還不如創建一個新的組件加到原來的組件上。

從不同節點的操作上我們可以推斷,React的diff演算法是只對對象樹逐層比較。

逐層進行節點比較

在React中對樹的演算法非常簡單,那就是對兩棵樹同一層次的節點進行比較。

有一張非常經典的圖:

React只會對相同顏色方框內的DOM節點進行比較,即同一個父節點下的所有子節點。當發現節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用於進一步的比較。這樣只需要對樹進行一次遍歷,便能完成整個DOM樹的比較。

考慮下如果有這樣的DOM結構的變化:

我們想的操作是:R.remove(A), D.append(A)

但是因為React只會對同一層次的節點進行比較,當發現新的對象樹中沒有A節點時,就會完全刪除A,同理,會新創建一個A節點作為D的子節點。實際React的操作是:A.destroy(), A = new A(), A.append(new B()), A.append(new C()), D.append(A)

由此我們可以根據React只對同一層次的節點比較可以作出的優化是:儘量不要跨層級的修改DOM

相同節點類型的比較

剛纔我們說過,相通節點類型屬性可能不同,React會對屬性進行重設,但要註意:Virtual DOM中style必須是個對象。

renderA: <div style={{color: 'red'}} />
renderB: <div style={{fontWeight: 'bold'}} />
=> [removeStyle color], [addStyle font-weight 'bold']

key值的使用

我們經常在遍歷一個數組或列表需要一個標識一個唯一的key,這個key是乾什麼的呢?

這是初始視圖:

我們現在想在它們中間加一個F,也就是一個insert操作。

如果每個節點沒有一個唯一的key,React不能識別每個節點,那React就會將C更新成F,將D更新成C,最後在末尾插入一個D。

如果每個節點有一個唯一的key做標識,React會找到正確的位置去插入新的節點,從而提高了視圖更新的效率。

對於key我們可以給出的優化是:給每個列表元素加上一個唯一的key

4.diff演算法的簡單實現


我們先對兩棵對象樹做一個深度優先的遍歷,這樣每一個節點都有一個唯一的標記:

在深度優先遍歷的時候,每遍歷到一個節點就把該節點和新的的樹進行對比。如果有差異的話就記錄到一個對象裡面。

// diff 函數,對比兩棵樹
function diff (oldTree, newTree) {
  var index = 0 // 當前節點的標誌
  var patches = {} // 用來記錄每個節點差異的對象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 對兩棵樹進行深度優先遍歷
function dfsWalk (oldNode, newNode, index, patches) {
  // 對比oldNode和newNode的不同,記錄下來
  patches[index] = [...]

  diffChildren(oldNode.children, newNode.children, index, patches)
}

// 遍歷子節點
function diffChildren (oldChildren, newChildren, index, patches) {
  var leftNode = null
  var currentNodeIndex = index
  oldChildren.forEach(function (child, i) {
    var newChild = newChildren[i]
    currentNodeIndex = (leftNode && leftNode.count) // 計算節點的標識
      ? currentNodeIndex + leftNode.count + 1
      : currentNodeIndex + 1
    dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍歷子節點
    leftNode = child
  })
}

例如,上面的div和新的div有差異,當前的標記是0,那麼

patches[0] = [{difference}, {difference}, ...] // 用數組存儲新舊節點的不同

那我們所說的差異是什麼呢?

  1. 節點被替換

  2. 增加、刪除、移動子節點

  3. 修改了節點的屬性

  4. 若是文本節點,則文本內容可能會被改變

所以我們定義了幾種類型:

var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3

舉個例子,如果最外層的div被換成了section,則相應的記錄如下:

patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}]

其他變化同理。

5.patch方法的實現


我們比較完了兩棵對象樹的差異,接下來就是將差異應用到DOM上了。這個過程有點像打補丁,所以我們叫它patch。

我們第一步構建出來的對象樹和真正的DOM樹的屬性、結構是一樣的,所以我們可以對DOM樹進行一次深度優先遍歷,遍歷的時候按著diff生成的patch對象進行patch操作,修改需要patch的地方。

我們還要根據不同的差異進行不同的DOM操作。

function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
  var currentPatches = patches[walker.index] // 從patches拿出當前節點的差異

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  for (var i = 0; i < len; i++) { // 深度遍歷子節點
    var child = node.childNodes[i]
    walker.index++
    dfsWalk(child, walker, patches)
  }

  if (currentPatches) {
    applyPatches(node, currentPatches) // 對當前節點進行DOM操作
  }
}

function applyPatches (node, currentPatches) {
  currentPatches.forEach(function (currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        node.parentNode.replaceChild(currentPatch.node.render(), node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        node.textContent = currentPatch.content
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}

看過了別人的文章,也借鑒了別人的思想,加上自己的總結,代碼正在整理中。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • HTTP 緩存體系 首先我將 Http 緩存體系分為以下三個部分: 1. 緩存存儲策略 用來確定 Http 響應內容是否可以被客戶端緩存,以及可以被哪些客戶端緩存 這個策略的作用只有一個,用於決定 Http 響應內容是否可緩存到客戶端 對於 Cache-Control 頭裡的 Public、Priv ...
  • 計算屬性computed 模板內的表達式非常便利,但是設計它們的初衷是用於簡單運算的。在模板中放入太多的邏輯會讓模板過重且難以維護。例如: 所以,對於任何複雜邏輯,你都應當使用計算屬性。 例子 結果: 這裡我們聲明瞭一個計算屬性 reversedMessage。我們提供的函數將用作屬性 vm.rev ...
  • 我新建了一個web前端的新手交流群,包括基礎知識和剛入職的技術分享,人還不多,期待著每一個人的加入,希望可以得到你的認同哦~你的加入是我組織交流群的一大動力哦!~ web前端交流QQ群:314439765 ...
  • 隨著各大瀏覽器對HTML5技術支持的不斷完善,未來HTML5必將改變我們創建Web應用程式的方式。而很多html5的初學者都想找一款好用的編寫軟體,這裡主機吧就給大家推薦七款好用的html5編寫軟體。 1、Brackets ,推薦指數:★★★★☆(四星)Adobe 開源的前端開發工具。這個基本上是完 ...
  • 雖然最新版的前端開發利器WebStorm支持了Vue,但是大部分人的WebStorm依然是預設不支持Vue的老版本(比如之前的我),所以需要手動添加WebStorm對Vue的支持。要想讓WebStorm支持Vue主要分兩步,第一步是安裝Vue.js插件,使得WebStorm能夠對Vue語法進行提示; ...
  • 前言 見解有限,如有描述不當之處,請幫忙及時指出,如有錯誤,會及時修正。 超長文+多圖預警,需要花費不少時間。 如果看完本文後,還對進程線程傻傻分不清,不清楚瀏覽器多進程、瀏覽器內核多線程、JS單線程、JS運行機制的區別。那麼請回覆我,一定是我寫的還不夠清晰,我來改。。。 __ 正文開始 __ 最近 ...
  • 轉載請註明出處: "Generator函數語法解析" Generator函數是ES6提供的一種非同步編程解決方案,語法與傳統函數完全不同。以下會介紹一下Generator函數。 寫下這篇文章的目的其實很簡單,是想梳理一下自己對於Generator的理解,同時呢,為學習async函數做一下知識儲備。 G ...
  • 1. 項目組織方式 :嚴格依賴註入模式。 在angularJS中,如果要使用模塊中的內容,只要提供相關模塊的名稱即可,不需要自己查找、創建、初始化等等,就可以直接得到需要註入的模塊進行使用,這種方式即所謂的 依賴註入 。 但是在JS代碼壓縮混淆(minify/uglify)的時候,參數的名稱會被壓縮 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...