Heim >Web-Frontend >js-Tutorial >Analyse und Implementierung des Virtual-Dom-Prinzipprozesses

Analyse und Implementierung des Virtual-Dom-Prinzipprozesses

不言
不言nach vorne
2018-10-11 13:52:123799Durchsuche

Der Inhalt dieses Artikels befasst sich mit der Analyse und Implementierung des Virtual-Dom-Prinzips. Ich hoffe, dass er für Freunde in Not hilfreich ist.

Hintergrund

Wie wir alle wissen, sind DOM-Knoten die teuersten Browserressourcen auf Webseiten. DOM ist sehr langsam und sehr umfangreich. Die meisten Leistungsprobleme bei Webseiten werden durch JavaScript-Modifikationen im DOM verursacht. verursacht. Wir verwenden Javascript, um DOM zu manipulieren, und die Operationseffizienz ist oft sehr gering. Da DOM als Baumstruktur dargestellt wird, ändert sich jedes Mal etwas im DOM, sodass die Änderungen am DOM sehr schnell erfolgen, die geänderten Elemente jedoch Die untergeordneten Elemente müssen die Reflow-/Layout-Phase durchlaufen, und dann muss der Browser die Änderungen neu zeichnen, was langsam ist. Je öfter Sie also umfließen/neu zeichnen, desto verzögerter wird Ihre Anwendung. Allerdings läuft Javascript sehr schnell und das virtuelle DOM ist eine Schicht zwischen JS und HTML. Es kann die Differenzobjekte nach dem Vergleich durch Vergleichen des alten und des neuen DOM erhalten und dann die Differenzteile tatsächlich gezielt auf der Seite rendern, wodurch die tatsächlichen DOM-Vorgänge reduziert und letztendlich der Zweck der Leistungsoptimierung erreicht wird.

Virtueller DOM-Prinzipprozess

Zusammenfassend gibt es drei einfache Punkte:

  1. Verwenden Sie JavaScript, um den DOM-Baum zu simulieren und den DOM-Baum zu rendern

  2. Vergleichen Sie die alten und neuen DOM-Bäume und erhalten Sie das verglichene Differenzobjekt

  3. Wenden Sie das Differenzobjekt auf den gerenderten DOM-Baum an.

Das Folgende ist ein Flussdiagramm:

Analyse und Implementierung des Virtual-Dom-Prinzipprozesses

Nachfolgend verwenden wir Code, um einen Flussdiagrammschritt zu implementieren Schritt für Schritt

Verwenden Sie JavaScript, um den DOM-Baum zu simulieren und auf der Seite darzustellen

Tatsächlich ist virtuelles DOM eine Abbildung der JS-Objektstruktur. Lassen Sie uns diesen Prozess Schritt für Schritt implementieren.

Wir können JS verwenden, um die Struktur eines DOM-Baums einfach zu simulieren. Verwenden Sie beispielsweise eine solche Funktion createEl(tagName, props, children), um eine DOM-Struktur zu erstellen.

tagName Tag-Name, Props ist das Objekt des Attributs und Children ist der untergeordnete Knoten.

Dann rendern Sie die Seite. Der Code lautet wie folgt:

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

const vdom = createEl('p', { '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)

Rufen Sie über die obige Funktion vdom.render() auf, damit wir einen DOM-Baum wie unten gezeigt erstellen können. Dann rendern es auf die Seite

<div id="box">
  <h1 style="color: pink;">I am H1</h1>
  <ul class="list">
    <li>#list1</li>
    <li>#list2</li>
  </ul>
  <p>I am p</p>
</div>

Werfen wir einen Blick auf den CreactEl.js-Codeprozess:

import { setAttr } from './utils'
class CreateEl {
  constructor (tagName, props, children) {
    // 当只有两个参数的时候 例如 celement(el, [123])
    if (Array.isArray(props)) {
      children = props
      props = {}
    }
    // tagName, props, children数据保存到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++
    })
    // 给每一个节点设置一个count
    this.count = count
  }
  // 构建一个 dom 树
  render () {
    // 创建dom
    const el = document.createElement(this.tagName)
    const props = this.props
    // 循环所有属性,然后设置属性
    for (let [key, val] of Object.entries(props)) {
      setAttr(el, key, val)
    }
    this.children.forEach(child => {
      // 递归循环 构建tree
      let childEl = (child instanceof CreateEl) ? child.render() : document.createTextNode(child)
      el.appendChild(childEl)
    })
    return el
  }
}

Die Funktion der obigen render-Funktion besteht darin, den Knoten zu erstellen und dann die Knotenattribute festzulegen und schließlich rekursiv erstellen. Auf diese Weise erhalten wir einen DOM-Baum und fügen dann (appendChild) in die Seite ein.

Vergleichen Sie die alten und neuen DOM-Bäume und erhalten Sie die Differenzobjekte

Oben haben wir einen DOM-Baum erstellt, dann einen anderen DOM-Baum erstellt und ihn dann verglichen, um das Differenzobjektobjekt zu erhalten .

Der Vergleich der Differenz zwischen zwei DOM-Bäumen ist der Kernbestandteil des virtuellen DOM. Dies ist auch der Diff-Algorithmus des virtuellen DOM, der häufig als O(n) bezeichnet wird ^ 3). Ebenenübergreifende DOM-Baumvergleiche werden in unserem Web jedoch selten verwendet, sodass beim Vergleich einer Ebene mit einer anderen die Komplexität des Algorithmus O(n) erreichen kann. Wie unten gezeigt

Analyse und Implementierung des Virtual-Dom-Prinzipprozesses

Tatsächlich beginnen wir im Code mit dem Wurzelknoten, um den Durchlauf zu markieren, und beim Durchlaufen die Unterschiede zwischen Jeder Knoten (einschließlich Textunterschiede, unterschiedliche Attribute, unterschiedliche Knoten) wird gespeichert. Wie unten gezeigt:

Analyse und Implementierung des Virtual-Dom-Prinzipprozesses

Die Unterschiede zwischen den beiden Knoten können wie folgt zusammengefasst werden:

0 直接替换原有节点
1 调整子节点,包括移动、删除等
2 修改节点属性
3 修改节点文本内容

Zum Beispiel die folgenden beiden Bäume Vergleichen und notieren Sie die Unterschiede.

Analyse und Implementierung des Virtual-Dom-Prinzipprozesses

Durchläuft hauptsächlich den Index (siehe Abbildung 3), startet dann den Vergleich am Wurzelknoten, zeichnet die Differenzobjekte nach dem Vergleich auf und fährt fort links Vergleichen Sie Teilbäume, zeichnen Sie Unterschiede auf und fahren Sie mit dem Durchlaufen fort. Der Hauptprozess ist wie folgt:

// 这是比较两个树找到最小移动量的算法是Levenshtein距离,即O(n * m)
// 具体请看 https://www.npmjs.com/package/list-diff2
import listDiff from 'list-diff2'
// 比较两棵树
function diff (oldTree, newTree) {
  // 节点的遍历顺序
  let index = 0
  // 在遍历过程中记录节点的差异
  let patches = {}
  // 深度优先遍历两棵树
  deepTraversal(oldTree, newTree, index, patches)
  // 得到的差异对象返回出去
  return patches
}

function deepTraversal(oldNode, newNode, index, patches) {
  let currentPatch = []
  // ...中间有很多对patches的处理
  // 递归比较子节点是否相同
  diffChildren(oldNode.children, newNode.children, index, patches, currentPatch)
  if (currentPatch.length) {
    // 那个index节点的差异记录下来
    patches[index] = currentPatch
  }
}

// 子数的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
  })
}

Dann rufen wir diff(tree, newTree) auf und warten, bis das endgültige Differenzobjekt so aussieht.

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

key stellt diesen Knoten dar, hier sind wir der zweite, das heißt, h1 wird in h3 geändert und die beiden ausgelassenen Differenzobjektcodes werden nicht gepostet~~

Sehen Sie sich dann den vollständigen Code von diff.js wie folgt an

import listDiff from 'list-diff2'
// 每个节点有四种变动
export const REPLACE = 0 // 替换原有节点
export const REORDER = 1 // 调整子节点,包括移动、删除等
export const PROPS = 2 // 修改节点属性
export const TEXT = 3 // 修改节点文本内容

export function diff (oldTree, newTree) {
  // 节点的遍历顺序
  let index = 0
  // 在遍历过程中记录节点的差异
  let patches = {}
  // 深度优先遍历两棵树
  deepTraversal(oldTree, newTree, index, patches)
  // 得到的差异对象返回出去
  return patches
}

function deepTraversal(oldNode, newNode, index, patches) {
  let currentPatch = []
  if (newNode === null) { // 如果新节点没有的话直接不用比较了
    return
  }
  if (typeof oldNode === 'string' && typeof newNode === 'string') {
    // 比较文本节点
    if (oldNode !== newNode) {
      currentPatch.push({
        type: TEXT,
        content: newNode
      })
    }
  } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    // 节点类型相同
    // 比较节点的属性是否相同
    let propasPatches = diffProps(oldNode, newNode)
    if (propasPatches) {
      currentPatch.push({
        type: PROPS,
        props: propsPatches
      })
    }
    // 递归比较子节点是否相同
    diffChildren(oldNode.children, newNode.children, index, patches, currentPatch)
  } else {
    // 节点不一样,直接替换
    currentPatch.push({ type: REPLACE, node: newNode })
  }

  if (currentPatch.length) {
    // 那个index节点的差异记录下来
    patches[index] = currentPatch
  }

}

// 子数的diff
function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
  var diffs = listDiff(oldChildren, newChildren)
  newChildren = diffs.children
  // 如果调整子节点,包括移动、删除等的话
  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
}

Nachdem Sie das Differenzobjekt erhalten haben, müssen Sie nur noch das Differenzobjekt auf unseren Dom-Knoten anwenden.

Wenden Sie das Differenzobjekt auf den gerenderten DOM-Baum an

An dieser Stelle ist es tatsächlich viel einfacher. Nach dem oben erhaltenen Differenzobjekt wählen wir dann die gleiche Tiefendurchquerung aus. Wenn es einen Unterschied in diesem Knoten gibt, bestimmen Sie, um welchen der oben genannten vier Typen es sich handelt, und ändern Sie den Knoten direkt entsprechend dem Differenzobjekt.

function patch (node, patches) {
  // 也是从0开始
  const step = {
    index: 0
  }
  // 深度遍历
  deepTraversal(node, step, patches)
}

// 深度优先遍历dom结构
function deepTraversal(node, step, patches) {
  // 拿到当前差异对象
  const currentPatches = patches[step.index]
  const len = node.childNodes ? node.childNodes.length : 0
  for (let i = 0; i <p>Auf diese Weise können durch den Aufruf von patch(rootnode, patches) direkt und gezielt die verschiedenen Knoten verändert werden. </p><p>Der vollständige Code von path.js lautet wie folgt: </p><pre class="brush:php;toolbar:false">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)
}

// 深度优先遍历dom结构
function deepTraversal(node, step, patches) {
  // 拿到当前差异对象
  const currentPatches = patches[step.index]
  const len = node.childNodes ? node.childNodes.length : 0
  for (let i = 0; i  {
    switch (currentPatch.type) {
      // 0: 替换原有节点
      case REPLACE:
        var newNode = (typeof currentPatch.node === 'string') ?  document.createTextNode(currentPatch.node) : currentPatch.node.render()
        node.parentNode.replaceChild(newNode, node)
        break
      // 1: 调整子节点,包括移动、删除等
      case REORDER: 
        moveChildren(node, currentPatch.moves)
        break
      // 2: 修改节点属性
      case PROPS:
        for (let [key, val] of Object.entries(currentPatch.props)) {
          if (val === undefined) {
            node.removeAttribute(key)
          } else {
            setAttr(node, key, val)
          }
        }
        break;
      // 3:修改节点文本内容
      case TEXT:
        if (node.textContent) {
          node.textContent = currentPatch.content
        } else {
          node.nodeValue = currentPatch.content
        }
        break;
      default: 
        throw new Error('Unknow patch type ' + currentPatch.type);
    }
  })
}

// 调整子节点,包括移动、删除等
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) { // 变动类型为删除的节点
      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原理已经讲完了,也简单了实现了一个虚拟DOM.

Das obige ist der detaillierte Inhalt vonAnalyse und Implementierung des Virtual-Dom-Prinzipprozesses. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen