Maison  >  Article  >  interface Web  >  Analyse et mise en œuvre du processus de principe du dom virtuel

Analyse et mise en œuvre du processus de principe du dom virtuel

不言
不言avant
2018-10-11 13:52:123680parcourir

Le contenu de cet article concerne l'analyse et la mise en œuvre du processus du principe du dom virtuel. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer.

Contexte

Comme nous le savons tous, les nœuds DOM sont les ressources de navigateur les plus coûteuses sur les pages Web. Le DOM est très lent et très volumineux. La plupart des problèmes de performances des pages Web sont causés par la modification du DOM par JavaScript. provoqué. Nous utilisons Javascript pour manipuler le DOM, et l'efficacité opérationnelle est souvent très faible. Étant donné que le DOM est représenté sous forme d'arborescence, quelque chose dans le DOM changera à chaque fois, donc les modifications apportées au DOM sont très rapides, mais les éléments modifiés et. ses enfants doivent passer par la phase de redistribution/mise en page, puis le navigateur doit redessiner les modifications, ce qui est lent. Par conséquent, plus vous redistribuez/redessinez, plus votre application devient lente. Cependant, Javascript s'exécute très rapidement et le DOM virtuel est une couche placée entre JS et HTML. Il peut obtenir les objets de différence après comparaison en comparant l'ancien et le nouveau DOM, puis restituer les parties de différence sur la page de manière ciblée, réduisant ainsi les opérations DOM réelles et atteignant finalement l'objectif d'optimisation des performances.

Processus de principe du DOM virtuel

Il y a trois points simples en résumé :

  1. Utilisez JavaScript pour simuler l'arborescence DOM et restituer l'arborescence DOM

  2. Comparez les anciens et les nouveaux arbres DOM et obtenez l'objet de différence comparé

  3. Appliquez l'objet de différence à l'arbre DOM rendu.

Ce qui suit est un organigramme :

Analyse et mise en œuvre du processus de principe du dom virtuel

Ci-dessous, nous utilisons du code pour implémenter une étape d'organigramme par étape

Utilisez JavaScript pour simuler l'arborescence DOM et restituez-la sur la page

En fait, le DOM virtuel est un mappage de la structure des objets JS. Implémentons ce processus étape par étape.

Nous pouvons utiliser JS pour simuler facilement la structure d'un arbre DOM. Par exemple, utilisez une telle fonction createEl(tagName, props, children) pour créer une structure DOM.

tagName nom de la balise, props est l'objet des attributs et children est le nœud enfant.

Ensuite, rendez sur la page, le code est le suivant :

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)

Grâce à la fonction ci-dessus, appelez vdom.render() afin que nous puissions construire un DOM comme indiqué ci-dessous l'arborescence, puis restituez-le sur la page

<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>

Jetons un coup d'œil au processus de code CreactEl.js :

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
  }
}

La fonction de la fonction render ci-dessus est de créer le node puis définissez-le. Les attributs de nœud sont créés de manière récursive à la fin. De cette façon, nous obtenons une arborescence DOM, puis insérons (appendChild) dans la page.

Comparez les anciens et les nouveaux arbres DOM et obtenez les objets de différence

Ci-dessus, nous avons créé un arbre DOM, puis créé un arbre DOM différent, puis l'avons comparé pour obtenir l'objet d'objets de différence .

La comparaison de la différence entre deux arbres DOM est la partie centrale du DOM virtuel. C'est aussi l'algorithme de comparaison du DOM virtuel que les gens appellent souvent. La complexité temporelle de la comparaison de la différence entre deux arbres complets est O(n). ^ 3). Cependant, les comparaisons d'arbres DOM entre niveaux sont rarement utilisées sur notre Web, donc en comparant un niveau avec un autre, la complexité de l'algorithme peut atteindre O(n). Comme indiqué ci-dessous

Analyse et mise en œuvre du processus de principe du dom virtuel

En fait, dans le code, nous partirons du nœud racine pour marquer le parcours, et lors du parcours, les différences de les enregistrements de chaque nœud (y compris les différences de texte, les différents attributs, les différents nœuds) sont enregistrés. Comme indiqué ci-dessous :

Analyse et mise en œuvre du processus de principe du dom virtuel

Les différences entre les deux nœuds peuvent être résumées comme suit :

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

Comme indiqué ci-dessous Comparez arbres et notez les différences.

Analyse et mise en œuvre du processus de principe du dom virtuel

Parcourt principalement l'index (voir Figure 3), puis démarre la comparaison à partir du nœud racine, enregistre les objets de différence après la comparaison et continue à partir de la gauche Comparez les sous-arbres, enregistrez les différences et continuez la traversée. Le processus principal est le suivant

// 这是比较两个树找到最小移动量的算法是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
  })
}

Ensuite, nous appelons diff(tree, newTree) et attendons que l'objet de différence final ressemble à ceci.

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

key représente ce nœud, nous sommes ici le deuxième, c'est-à-dire que h1 sera remplacé par h3, et les deux codes d'objet de différence omis ne sont pas publiés ~~

Ensuite, jetez un œil au code complet de diff.js, comme suit

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
}

Après avoir obtenu l'objet différence, il ne reste plus qu'à appliquer l'objet différence à notre nœud dom.

Appliquer l'objet différence à l'arborescence DOM rendue

C'est en fait beaucoup plus simple à ce stade. Après l'objet de différence que nous avons obtenu ci-dessus, nous sélectionnons ensuite le même parcours de profondeur. S'il y a une différence dans ce nœud, déterminons de quel type des quatre types ci-dessus il s'agit et modifions directement le nœud en fonction de l'objet de différence.

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>De cette façon, l'appel de patch(rootnode, patches) peut directement modifier les différents nœuds de manière ciblée. </p><p>Le code complet de path.js est le suivant : </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.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer