成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

淺析虛擬dom原理并實現(xiàn)

charles_paul / 1599人閱讀

摘要:虛擬原理流程簡單概括有三點用模擬樹,并渲染這個樹比較新老樹,得到比較的差異對象把差異對象應用到渲染的樹。下面是流程圖下面我們用代碼一步步去實現(xiàn)一個流程圖用模擬樹并渲染到頁面上其實虛擬,就是用對象結構的一種映射,下面我們一步步實現(xiàn)這個過程。

背景

大家都知道,在網(wǎng)頁中瀏覽器資源開銷最大便是DOM節(jié)點了,DOM很慢并且非常龐大,網(wǎng)頁性能問題大多數(shù)都是有JavaScript修改DOM所引起的。我們使用Javascript來操縱DOM,操作效率往往很低,由于DOM被表示為樹結構,每次DOM中的某些內(nèi)容都會發(fā)生變化,因此對DOM的更改非??欤暮蟮脑?,并且它的子項必須經(jīng)過Reflow / Layout階段,然后瀏覽器必須重新繪制更改,這很慢的。因此,回流/重繪的次數(shù)越多,您的應用程序就越卡頓。但是,Javascript運行速度很快,虛擬DOM是放在JS 和 HTML中間的一個層。它可以通過新舊DOM的對比,來獲取對比之后的差異對象,然后有針對性的把差異部分真正地渲染到頁面上,從而減少實際DOM操作,最終達到性能優(yōu)化的目的。

虛擬dom原理流程

簡單概括有三點:

用JavaScript模擬DOM樹,并渲染這個DOM樹

比較新老DOM樹,得到比較的差異對象

把差異對象應用到渲染的DOM樹。

下面是流程圖:

下面我們用代碼一步步去實現(xiàn)一個流程圖

用JavaScript模擬DOM樹并渲染到頁面上

其實虛擬DOM,就是用JS對象結構的一種映射,下面我們一步步實現(xiàn)這個過程。

我們用JS很容易模擬一個DOM樹的結構,例如用這樣的一個函數(shù)createEl(tagName, props, children)來創(chuàng)建DOM結構。

tagName標簽名、props是屬性的對象、children是子節(jié)點。

然后渲染到頁面上,代碼如下:

const createEl = (tagName, props, children) => new CreactEl(tagName, props, children)

const vdom = createEl("div", { "id": "box" }, [
  createEl("h1", { style: "color: pink" }, ["I am H1"]),
  createEl("ul", {class: "list"}, [createEl("li", ["#list1"]), createEl("li", ["#list2"])]),
  createEl("p", ["I am p"])
])

const rootnode = vdom.render()
document.body.appendChild(rootnode)

通過上面的函數(shù),調(diào)用vdom.render()這樣子我們就很好的構建了如下所示的一個DOM樹,然后渲染到頁面上

I am H1

  • #list1
  • #list2

I am p

下面我們看看CreactEl.js代碼流程:

import { setAttr } from "./utils"
class CreateEl {
  constructor (tagName, props, children) {
    // 當只有兩個參數(shù)的時候 例如 celement(el, [123])
    if (Array.isArray(props)) {
      children = props
      props = {}
    }
    // tagName, props, children數(shù)據(jù)保存到this對象上
    this.tagName = tagName
    this.props = props || {}
    this.children = children || []
    this.key = props ? props.key : undefined

    let count = 0
    this.children.forEach(child => {
      if (child instanceof CreateEl) {
        count += child.count
      } else {
        child = "" + child
      }
      count++
    })
    // 給每一個節(jié)點設置一個count
    this.count = count
  }
  // 構建一個 dom 樹
  render () {
    // 創(chuàng)建dom
    const el = document.createElement(this.tagName)
    const props = this.props
    // 循環(huán)所有屬性,然后設置屬性
    for (let [key, val] of Object.entries(props)) {
      setAttr(el, key, val)
    }
    this.children.forEach(child => {
      // 遞歸循環(huán) 構建tree
      let childEl = (child instanceof CreateEl) ? child.render() : document.createTextNode(child)
      el.appendChild(childEl)
    })
    return el
  }
}

上面render函數(shù)的功能是把節(jié)點創(chuàng)建好,然后設置節(jié)點屬性,最后遞歸創(chuàng)建。這樣子我們就得到一個DOM樹,然后插入(appendChild)到頁面上。

比較新老dom樹,得到比較的差異對象

上面,我們已經(jīng)創(chuàng)建了一個DOM樹,然后在創(chuàng)建一個不同的DOM樹,然后做比較,得到比較的差異對象。

比較兩棵DOM樹的差異,是虛擬DOM的最核心部分,這也是人們常說的虛擬DOM的diff算法,兩顆完全的樹差異比較一個時間復雜度為 O(n^3)。但是在我們的web中很少用到跨層級DOM樹的比較,所以一個層級跟一個層級對比,這樣算法復雜度就可以達到 O(n)。如下圖

其實在代碼中,我們會從根節(jié)點開始標志遍歷,遍歷的時候把每個節(jié)點的差異(包括文本不同,屬性不同,節(jié)點不同)記錄保存起來。如下圖:

兩個節(jié)點之間的差異有總結起來有下面4種

0 直接替換原有節(jié)點
1 調(diào)整子節(jié)點,包括移動、刪除等
2 修改節(jié)點屬性
3 修改節(jié)點文本內(nèi)容

如下面兩棵樹比較,把差異記錄下來。

主要是簡歷一個遍歷index(看圖3),然后從根節(jié)點開始比較,比較萬之后記錄差異對象,繼續(xù)從左子樹比較,記錄差異,一直遍歷下去。主要流程如下

// 這是比較兩個樹找到最小移動量的算法是Levenshtein距離,即O(n * m)
// 具體請看 https://www.npmjs.com/package/list-diff2
import listDiff from "list-diff2"
// 比較兩棵樹
function diff (oldTree, newTree) {
  // 節(jié)點的遍歷順序
  let index = 0
  // 在遍歷過程中記錄節(jié)點的差異
  let patches = {}
  // 深度優(yōu)先遍歷兩棵樹
  deepTraversal(oldTree, newTree, index, patches)
  // 得到的差異對象返回出去
  return patches
}

function deepTraversal(oldNode, newNode, index, patches) {
  let currentPatch = []
  // ...中間有很多對patches的處理
  // 遞歸比較子節(jié)點是否相同
  diffChildren(oldNode.children, newNode.children, index, patches, currentPatch)
  if (currentPatch.length) {
    // 那個index節(jié)點的差異記錄下來
    patches[index] = currentPatch
  }
}

// 子數(shù)的diff
function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
  const diffs = listDiff(oldChildren, newChildren)
  newChildren = diffs.children
  // ...省略記錄差異對象
  let leftNode = null
  let currentNodeIndex = index
  oldChildren.forEach((child, i) => {
    const newChild = newChildren[i]
    // index相加
    currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1
    // 深度遍歷,遞歸
    deepTraversal(child, newChild, currentNodeIndex, patches)
    // 從左樹開始
    leftNode = child
  })
}

然后我們調(diào)用完diff(tree, newTree)等到最后的差異對象是這樣子的。

{
  "1": [
    {
      "type": 0,
      "node": {
        "tagName": "h3",
        "props": {
          "style": "color: green"
        },
        "children": [
          "I am H1"
        ],
        "count": 1
      }
    }
  ]
  ...
}

key是代表那個節(jié)點,這里我們是第二個,也就是h1會改變成h3,還有省略的兩個差異對象代碼沒有貼出來~~

然后看下diff.js的完整代碼,如下

import listDiff from "list-diff2"
// 每個節(jié)點有四種變動
export const REPLACE = 0 // 替換原有節(jié)點
export const REORDER = 1 // 調(diào)整子節(jié)點,包括移動、刪除等
export const PROPS = 2 // 修改節(jié)點屬性
export const TEXT = 3 // 修改節(jié)點文本內(nèi)容

export function diff (oldTree, newTree) {
  // 節(jié)點的遍歷順序
  let index = 0
  // 在遍歷過程中記錄節(jié)點的差異
  let patches = {}
  // 深度優(yōu)先遍歷兩棵樹
  deepTraversal(oldTree, newTree, index, patches)
  // 得到的差異對象返回出去
  return patches
}

function deepTraversal(oldNode, newNode, index, patches) {
  let currentPatch = []
  if (newNode === null) { // 如果新節(jié)點沒有的話直接不用比較了
    return
  }
  if (typeof oldNode === "string" && typeof newNode === "string") {
    // 比較文本節(jié)點
    if (oldNode !== newNode) {
      currentPatch.push({
        type: TEXT,
        content: newNode
      })
    }
  } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    // 節(jié)點類型相同
    // 比較節(jié)點的屬性是否相同
    let propasPatches = diffProps(oldNode, newNode)
    if (propasPatches) {
      currentPatch.push({
        type: PROPS,
        props: propsPatches
      })
    }
    // 遞歸比較子節(jié)點是否相同
    diffChildren(oldNode.children, newNode.children, index, patches, currentPatch)
  } else {
    // 節(jié)點不一樣,直接替換
    currentPatch.push({ type: REPLACE, node: newNode })
  }

  if (currentPatch.length) {
    // 那個index節(jié)點的差異記錄下來
    patches[index] = currentPatch
  }

}

// 子數(shù)的diff
function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
  var diffs = listDiff(oldChildren, newChildren)
  newChildren = diffs.children
  // 如果調(diào)整子節(jié)點,包括移動、刪除等的話
  if (diffs.moves.length) {
    var reorderPatch = {
      type: REORDER,
      moves: diffs.moves
    }
    currentPatch.push(reorderPatch)
  }

  var leftNode = null
  var currentNodeIndex = index
  oldChildren.forEach((child, i) => {
    var newChild = newChildren[i]
    // index相加
    currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1
    // 深度遍歷,從左樹開始
    deepTraversal(child, newChild, currentNodeIndex, patches)
    // 從左樹開始
    leftNode = child
  })
}

// 記錄屬性的差異
function diffProps (oldNode, newNode) {
  let count = 0 // 聲明一個有沒沒有屬性變更的標志
  const oldProps = oldNode.props
  const newProps = newNode.props
  const propsPatches = {}

  // 找出不同的屬性
  for (let [key, val] of Object.entries(oldProps)) {
    // 新的不等于舊的
    if (newProps[key] !== val) {
      count++
      propsPatches[key] = newProps[key]
    }
  }
  // 找出新增的屬性
  for (let [key, val] of Object.entries(newProps)) {
    if (!oldProps.hasOwnProperty(key)) {
      count++
      propsPatches[key] = val
    }
  }
  // 沒有新增 也沒有不同的屬性 直接返回null
  if (count === 0) {
    return null
  }

  return propsPatches
}

得到差異對象之后,剩下就是把差異對象應用到我們的dom節(jié)點上面了。

把差異對象應用到渲染的dom樹

到了這里其實就簡單多了。我們上面得到的差異對象之后,然后選擇同樣的深度遍歷,如果那個節(jié)點有差異的話,判斷是上面4種中的哪一種,根據(jù)差異對象直接修改這個節(jié)點就可以了。

function patch (node, patches) {
  // 也是從0開始
  const step = {
    index: 0
  }
  // 深度遍歷
  deepTraversal(node, step, patches)
}

// 深度優(yōu)先遍歷dom結構
function deepTraversal(node, step, patches) {
  // 拿到當前差異對象
  const currentPatches = patches[step.index]
  const len = node.childNodes ? node.childNodes.length : 0
  for (let i = 0; i < len; i++) {
    const child = node.childNodes[i]
    step.index++
    deepTraversal(child, step, patches)
  }
  //如果當前節(jié)點存在差異
  if (currentPatches) {
    // 把差異對象應用到當前節(jié)點上
    applyPatches(node, currentPatches)
  }
}

這樣子,調(diào)用patch(rootnode, patches)就直接有針對性的改變有差異的節(jié)點了。

path.js完整代碼如下:

import {REPLACE, REORDER, PROPS, TEXT} from "./diff"
import { setAttr } from "./utils"

export function patch (node, patches) {
  // 也是從0開始
  const step = {
    index: 0
  }
  // 深度遍歷
  deepTraversal(node, step, patches)
}

// 深度優(yōu)先遍歷dom結構
function deepTraversal(node, step, patches) {
  // 拿到當前差異對象
  const currentPatches = patches[step.index]
  const len = node.childNodes ? node.childNodes.length : 0
  for (let i = 0; i < len; i++) {
    const child = node.childNodes[i]
    step.index++
    deepTraversal(child, step, patches)
  }
  //如果當前節(jié)點存在差異
  if (currentPatches) {
    // 把差異對象應用到當前節(jié)點上
    applyPatches(node, currentPatches)
  }
}

// 把差異對象應用到當前節(jié)點上
function applyPatches(node, currentPatches) {
  currentPatches.forEach(currentPatch => {
    switch (currentPatch.type) {
      // 0: 替換原有節(jié)點
      case REPLACE:
        var newNode = (typeof currentPatch.node === "string") ?  document.createTextNode(currentPatch.node) : currentPatch.node.render()
        node.parentNode.replaceChild(newNode, node)
        break
      // 1: 調(diào)整子節(jié)點,包括移動、刪除等
      case REORDER: 
        moveChildren(node, currentPatch.moves)
        break
      // 2: 修改節(jié)點屬性
      case PROPS:
        for (let [key, val] of Object.entries(currentPatch.props)) {
          if (val === undefined) {
            node.removeAttribute(key)
          } else {
            setAttr(node, key, val)
          }
        }
        break;
      // 3:修改節(jié)點文本內(nèi)容
      case TEXT:
        if (node.textContent) {
          node.textContent = currentPatch.content
        } else {
          node.nodeValue = currentPatch.content
        }
        break;
      default: 
        throw new Error("Unknow patch type " + currentPatch.type);
    }
  })
}

// 調(diào)整子節(jié)點,包括移動、刪除等
function moveChildren (node, moves) {
  let staticNodelist = Array.from(node.childNodes)
  const maps = {}
  staticNodelist.forEach(node => {
    if (node.nodeType === 1) {
      const key = node.getAttribute("key")
      if (key) {
        maps[key] = node
      }
    }
  })
  moves.forEach(move => {
    const index = move.index
    if (move.type === 0) { // 變動類型為刪除的節(jié)點
      if (staticNodeList[index] === node.childNodes[index]) {
        node.removeChild(node.childNodes[index]);
      }
      staticNodeList.splice(index, 1);
    } else {
      let insertNode = maps[move.item.key] 
          ? maps[move.item.key] : (typeof move.item === "object") 
          ? move.item.render() : document.createTextNode(move.item)
      staticNodelist.splice(index, 0, insertNode);
      node.insertBefore(insertNode, node.childNodes[index] || null)
    }
  })
}

到這里,最基本的虛擬DOM原理已經(jīng)講完了,也簡單了實現(xiàn)了一個虛擬DOM,如果本文有什么不對的地方請指正。

文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉載請注明本文地址:http://systransis.cn/yun/98275.html

相關文章

  • H5 緩存機制淺析 - 移動端 Web 加載性能優(yōu)化

    摘要:根據(jù)標準,到目前為止,一共有種緩存機制,有些是之前已有,有些是才新加入的。首次請求緩存有效期內(nèi)請求緩存過期后請求一般瀏覽器會將緩存記錄及緩存文件存在本地文件夾中。 騰訊 Bugly 特約作者:賀輝超 1. H5 緩存機制介紹 H5,即 HTML5,是新一代的 HTML 標準,加入很多新的特性。離線存儲(也可稱為緩存機制)是其中一個非常重要的特性。H5 引入的離線存儲,這意味著 web ...

    alin 評論0 收藏0
  • Web緩存相關知識整理

    摘要:緩存緩存,也叫網(wǎng)關緩存反向代理緩存。瀏覽器先向網(wǎng)關發(fā)起請求,網(wǎng)關服務器后面對應著一臺或多臺負載均衡源服務器,會根據(jù)它們的負載請求,動態(tài)將請求轉發(fā)到合適的源服務器上。雖然這種架構負載均衡源服務器之間的緩存沒法共享,但卻擁有更好的處擴展性。 一、前言? 工作上遇到一個這樣的需求,一個H5頁面在APP端,如果勾選已讀狀態(tài),則下次打開該鏈接,會跳過此頁面。用到了HTML5 的本地存儲 API ...

    rickchen 評論0 收藏0
  • webkit渲染機制淺析

    摘要:模塊和將下面的渲染機制,安全機制,插件機制等等隱藏起來,提供一個接口層。進行網(wǎng)頁的渲染進程,可能有多個。最后進程將結果由線程傳遞給進程最后,進程接收到結果并將結果繪制出來。 這是之前在簡書上面的處女作,也搬過來了,以后就一直在 segmentfault 上面寫文章了,webkit技術內(nèi)幕-朱永盛是我大四買的書,很舊的一本書了,當時只看了一點點,一直沒繼續(xù)看完它,現(xiàn)在才看完,,,說來慚愧...

    Cobub 評論0 收藏0
  • 2017文章總結

    摘要:歡迎來我的個人站點性能優(yōu)化其他優(yōu)化瀏覽器關鍵渲染路徑開啟性能優(yōu)化之旅高性能滾動及頁面渲染優(yōu)化理論寫法對壓縮率的影響唯快不破應用的個優(yōu)化步驟進階鵝廠大神用直出實現(xiàn)網(wǎng)頁瞬開緩存網(wǎng)頁性能管理詳解寫給后端程序員的緩存原理介紹年底補課緩存機制優(yōu)化動 歡迎來我的個人站點 性能優(yōu)化 其他 優(yōu)化瀏覽器關鍵渲染路徑 - 開啟性能優(yōu)化之旅 高性能滾動 scroll 及頁面渲染優(yōu)化 理論 | HTML寫法...

    dailybird 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<