React-咱們村剛通網之虛擬 DOM(一)

這是我參與更文挑戰的第6天,活動詳情查看: 更文挑戰javascript

標題靈感來源評論區:html

image.png

1、什麼是虛擬 DOM

什麼是虛擬 DOM?簡單來講,虛擬 DOM 就是一個模擬真實 DOM 的樹形結構,這個樹結構包含了整個 DOM 結構的信息。java

正常咱們看到的真實 DOM 是這樣的:node

image.png

而虛擬 DOM 則是這樣的,包含了標籤名稱、標籤屬性、子節點等真實 DOM 信息:react

image.png

2、爲何使用虛擬 DOM

虛擬 DOM 既然是模擬真實 DOM 的樹形結構,那麼爲何要用虛擬 DOM 呢?直接操做 DOM 有什麼缺點嗎?git

直接操做 DOM 沒有缺點,可是頻繁的操做 DOM 就缺點很大,由於操做 DOM 會引發重排,頻繁操做 DOM 時,瀏覽器會頻繁重排,致使頁面卡頓。github

瀏覽器渲染的大體流程以下:web

  1. 解析 HTML 文檔,構建 DOM 樹;
  2. 解析 CSS 屬性,構建 CSSOM 樹;
  3. 結合 DOM 樹和 CSSOM 樹,構建 render 樹;
  4. 在 render 樹的基礎上進行佈局, 計算每一個節點的幾何結構(重排);
  5. 把每一個節點繪製在屏幕上(重繪);

image.png

重排(也叫回流、reflow)就是當涉及到 DOM 節點的佈局屬性發生變化時,就會從新計算該屬性,瀏覽器會從新描繪相應的元素(上述第 4 步)。算法

DOM Tree 裏的每一個節點都會有 reflow 方法,一個節點的 reflow 頗有可能致使子節點,甚至父點以及同級節點的 reflow。編程

所以,爲了提高性能,咱們應該儘可能減小 DOM 操做。

1. 減小 DOM 操做

當有一個表格須要作排序功能時,有出生年月、性別等排序方式可選,當選擇某排序方式時,表格將按該方式從新排序。

  • 真實 DOM:排序操做須要將表格內的全部 DOM 樹刪除後新建;
  • 虛擬 DOM:使用 diff 算法獲得須要修改的部分,僅更新須要發生修改的 DOM 節點;

從上可知,虛擬 DOM 經過 diff 算法,幫助咱們大量的減小 DOM 操做。

2. 函數式的 UI 編程方式

從另外一個角度看,虛擬 DOM 爲咱們提供了函數式的編程方式,使代碼可讀性和可維護性更高。

image.png

3、虛擬 DOM 的實現原理

注:該章節的虛擬 DOM 實現原理並非參比 React 源碼,而是參比 simple-virtual-dom,可經過該章節簡單瞭解虛擬 DOM 實現原理,React 中的虛擬 DOM 實現可查看 React 官網 Virtual DOM 及內核

虛擬 DOM 經過如下步驟實現:

  1. 構建虛擬 DOM 樹;
  2. 比較新舊虛擬 DOM 樹差別;
  3. 更新真實 DOM;

1. 構建虛擬 DOM

模擬真實 DOM 樹,構建虛擬 DOM 樹結構,包含標籤名 tagName、屬性對象 props、子節點 children、子節點數 count 等屬性。

function Element (tagName, props = {}, children = []) {
  // 標籤名
  this.tagName = tagName
  // 屬性對象
  this.props = props
  // 子節點
  this.children = children
  // key標誌
  const { key = void 666 } = this.props
  this.key = key

  // 子節點數量
  let count = 0
  this.children.forEach((child, index) => {
    if (child instanceof Element) {
      count += child.count
    }
    count++
  })
  this.count = count
}
複製代碼

建立虛擬 DOM 對象:

console.log(el('div', {'id': 'container'}, [
  el('h1', {style: 'color: red'}, ['simple virtal dom'])
  ]))
複製代碼

生成的虛擬 DOM 對象如圖:

image.png

將虛擬 DOM 轉換爲真實 DOM:

Element.prototype.render = function () {
  const el = document.createElement(this.tagName)
  const props = this.props

  for (const propName in props) {
    const propValue = props[propName]
    _.setAttr(el, propName, propValue)
  }

  this.children.forEach((child) => {
    let childEl

    if (child instanceof Element) {
      childEl = child.render()
    } else {
      childEl = document.createTextNode(child)
    }
    el.appendChild(childEl)
  })

  return el
}
複製代碼

填充進頁面:

document.body.appendChild(el('div', {'id': 'container'}, [
  el('h1', {style: 'color: red'}, ['simple virtal dom'])
  ]).render())
複製代碼

效果如圖:

image.png

2. 比較兩棵虛擬 DOM 樹的差別

當數據更新時,須要對新舊虛擬 DOM 樹進行對比。

  1. 當新舊節點都是字符串類型時,直接替換;
if (_.isString(oldNode) && _.isString(newNode)) {
    if (newNode !== oldNode) {
      currentPatch.push({ type: patch.TEXT, content: newNode })
    }
  // Nodes are the same, diff old node's props and children
  }
複製代碼
  1. 當新舊節點的標籤名、key 值相等時,對比屬性 Props 以及子節點 children;
if (
  oldNode.tagName === newNode.tagName &&
  oldNode.key === newNode.key
) {
    // Diff props
    var propsPatches = diffProps(oldNode, newNode)
    if (propsPatches) {
      currentPatch.push({ type: patch.PROPS, props: propsPatches })
    }
    // Diff children. If the node has a `ignore` property, do not diff children
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
}
複製代碼
  1. 若是新節點存在,且和舊節點標籤名不一樣,或者 key 不一樣,則直接將新節點替換爲舊節點。
currentPatch.push({
    type: PATCH_KEY.REPLACE, 
    node: newNode
})
複製代碼

總結一下,虛擬 DOM 只在同層級間 Diff,若是標籤不一樣則直接替換該節點及其子節點。

嘗試對比虛擬 DOM 以下:

function renderTree () {
  return el('div', {'id': 'container'}, [
          el('h1', {style: 'color: red'}, ['simple virtal dom']),
          el('p', ['the count is :' + Math.random()])
        ])
}

let tree = renderTree()

setTimeout(() => {
    const newTree = renderTree()
    const patches = diff(tree, newTree)
    console.log(patches)
}, 2000)
複製代碼

對比差別爲 p 標籤的文本節點發生改變,輸出結果如圖:

image.png

3. 對真實 DOM 進行最小化修改

最後一步是根據 diff 結果,對真實 DOM 進行修改。

遍歷真實 DOM 樹,若是該 DOM 節點有 diff,則根據 diff 類型,處理 DOM 節點,若是該 DOM 節點無 diff,則遍歷其子節點,直至遍歷完成。

注:React 實現更優,具體請見 React fiber

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

function dfsWalk (node, walker, patches) {
  var currentPatches = patches[walker.index]

  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 renderTree () {
  return el('div', {'id': 'container'}, [
          el('h1', {style: 'color: red'}, ['simple virtal dom']),
          el('p', ['the count is :' + Math.random()])
        ])
}

let tree = renderTree()
const root = tree.render()
document.body.appendChild(root)

setTimeout(() => {
  const newTree = renderTree()
  const patches = diff(tree, newTree)
  patch(root, patches)
  tree = newTree
}, 2000)
複製代碼

效果如圖:

1.gif

上圖可見,成功更新真實 DOM。

4、總結

本文從什麼是虛擬 DOM、爲何使用虛擬 DOM、虛擬 DOM 的實現原理等 3 個角度對虛擬 DOM 進行講述。

虛擬 DOM 經過模擬真實 DOM 的樹結構,收集大量 DOM 操做,經過 diff 算法對真實 DOM 進行最小化修改,減小瀏覽器重排,提高加載速度,達到優化網站性能的做用。

虛擬 DOM 採用函數式編程,讓咱們碼得更好看更快樂。

可經過 github源碼 進行實操練習。

但願能對你有所幫助,感謝閱讀~

別忘了點個贊鼓勵一下我哦,筆芯❤️

參考資料

相關文章
相關標籤/搜索