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.
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.
Il y a trois points simples en résumé :
Utilisez JavaScript pour simuler l'arborescence DOM et restituer l'arborescence DOM
Comparez les anciens et les nouveaux arbres DOM et obtenez l'objet de différence comparé
Appliquez l'objet de différence à l'arbre DOM rendu.
Ce qui suit est un organigramme :
Ci-dessous, nous utilisons du code pour implémenter une étape d'organigramme par étape
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.
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
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 :
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.
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.
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 < len; i++) { const child = node.childNodes[i] step.index++ deepTraversal(child, step, patches) } //如果当前节点存在差异 if (currentPatches) { // 把差异对象应用到当前节点上 applyPatches(node, currentPatches) } }
De cette façon, l'appel de patch(rootnode, patches) peut directement modifier les différents nœuds de manière ciblée.
Le code complet de path.js est le suivant :
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 < len; i++) { const child = node.childNodes[i] step.index++ deepTraversal(child, step, patches) } //如果当前节点存在差异 if (currentPatches) { // 把差异对象应用到当前节点上 applyPatches(node, currentPatches) } } // 把差异对象应用到当前节点上 function applyPatches(node, currentPatches) { currentPatches.forEach(currentPatch => { 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!