Maison >interface Web >js tutoriel >Comment implémenter l'algorithme de comparaison React
Cette fois, je vais vous montrer comment implémenter l'algorithme React diff et quelles sont les précautions pour implémenter l'algorithme React diff Ce qui suit est un cas pratique, jetons un coup d'œil.
Préface
Dans l'article précédent, nous avons implémenté la fonction composant de React. D'un point de vue fonctionnel, nous avons implémenté les fonctions principales de React. .
Mais il y a de gros problèmes avec notre implémentation : chaque mise à jour nécessite de restituer l'intégralité de l'application ou l'intégralité du composant, les opérations DOM sont très coûteuses et la perte de performances est très importante.
Afin de réduire les mises à jour du DOM, nous devons trouver les parties qui ont vraiment changé avant et après le rendu, et mettre à jour uniquement cette partie du DOM. L'algorithme qui compare les modifications et trouve les parties qui doivent être mises à jour est appelé algorithme diff.
Stratégie de comparaison
Après les deux articles précédents, nous avons implémenté une méthode de rendu qui peut restituer le DOM virtuel en DOM réel, nous devons améliorer maintenant pour qu'il ne restitue plus bêtement l'intégralité de l'arborescence DOM, mais découvre les parties qui ont vraiment changé.
De nombreux frameworks de type React implémentent cette partie de différentes manières. Certains frameworks choisiront de sauvegarder le dernier DOM virtuel rendu, puis compareront les modifications avant et après le DOM virtuel pour obtenir une série de données mises à jour, et puis ces mises à jour sont appliquées au vrai DOM.
Mais il existe également certains frameworks qui choisissent de comparer directement le DOM virtuel et le DOM réel, de sorte qu'il n'est pas nécessaire de sauvegarder le dernier DOM virtuel rendu et qu'il peut être mis à jour lors de la comparaison. C'est aussi la méthode. nous choisissons.
Qu'il s'agisse de DOM ou de DOM virtuel, leur structure est un arbre. La complexité temporelle de l'algorithme pour comparer complètement les changements entre les deux arbres est O(n^3), mais étant donné que nous traversons rarement les niveaux. DOM, il suffit donc de comparer les changements au même niveau.
Seuls les nœuds dans la même boîte de couleur doivent être comparés
En bref, notre algorithme de comparaison a deux principes :
Comparez le DOM réel et le DOM virtuel actuels, et mettez directement à jour le DOM réel pendant le processus de comparaison
Comparez uniquement les changements au même niveau
Nous devons implémenter une méthode diff, qui est utilisée pour comparer le DOM réel et le DOM virtuel, et enfin renvoyer le DOM mis à jour
/** * @param {HTMLElement} dom 真实DOM * @param {vnode} vnode 虚拟DOM * @returns {HTMLElement} 更新后的DOM */ function diff( dom, vnode ) { // ... }
L'étape suivante consiste à implémenter cette méthode.
Avant cela, rappelons la structure de notre DOM virtuel :
La structure du DOM virtuel peut être divisée en trois types, qui représentent le texte, le nœud DOM natif et composants respectivement.
// 原生DOM节点的vnode { tag: 'p', attrs: { className: 'container' }, children: [] } // 文本节点的vnode "hello,world" // 组件的vnode { tag: ComponentConstrucotr, attrs: { className: 'container' }, children: [] }
Comparez les nœuds de texte
Considérez d'abord le nœud de texte le plus simple Si le DOM actuel est un nœud de texte, mettez à jour le contenu directement. , sinon créez un nouveau nœud de texte et supprimez le DOM d'origine.
// diff text node if ( typeof vnode === 'string' ) { // 如果当前的DOM就是文本节点,则直接更新内容 if ( dom && dom.nodeType === 3 ) { // nodeType: https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType if ( dom.textContent !== vnode ) { dom.textContent = vnode; } // 如果DOM不是文本节点,则新建一个文本节点DOM,并移除掉原来的 } else { out = document.createTextNode( vnode ); if ( dom && dom.parentNode ) { dom.parentNode.replaceChild( out, dom ); } } return out; }
Le nœud texte est très simple. Il n'a ni attribut ni élément enfant, le résultat peut donc être renvoyé directement après cette étape.
Comparez les nœuds DOM non-texte
Si vnode représente un nœud DOM non-texte, alors il existe plusieurs situations :
Si les types du DOM réel et du DOM virtuel sont différents, par exemple, le DOM réel actuel est un p et la valeur de la balise vnode est 'bouton', alors le p d'origine n'a aucune valeur d'usage et crée directement un nouvel élément bouton et déplacez tous les nœuds enfants de p sous le bouton, puis utilisez la méthode replaceChild pour remplacer p par le bouton.
if ( !dom || dom.nodeName.toLowerCase() !== vnode.tag.toLowerCase() ) { out = document.createElement( vnode.tag ); if ( dom ) { [ ...dom.childNodes ].map( out.appendChild ); // 将原来的子节点移到新节点下 if ( dom.parentNode ) { dom.parentNode.replaceChild( out, dom ); // 移除掉原来的DOM对象 } } }
Si le DOM réel et le DOM virtuel sont du même type, alors nous n'avons rien d'autre à faire pour l'instant, il nous suffit d'attendre la comparaison des attributs et la comparaison des nœuds enfants plus tard.
Comparer les attributs
En fait, l'algorithme diff trouve non seulement les changements dans les types de nœuds, il trouve également les attributs et les événements des nœuds. Écoutez les changements . Nous séparons l'attribut de comparaison en tant que méthode :
function diffAttributes( dom, vnode ) { const old = dom.attributes; // 当前DOM的属性 const attrs = vnode.attrs; // 虚拟DOM的属性 // 如果原来的属性不在新的属性当中,则将其移除掉(属性值设为undefined) for ( let name in old ) { if ( !( name in attrs ) ) { setAttribute( dom, name, undefined ); } } // 更新新的属性值 for ( let name in attrs ) { if ( old[ name ] !== attrs[ name ] ) { setAttribute( dom, name, attrs[ name ] ); } } }
Pour l'implémentation de la méthode setAttribute, veuillez vous référer au premier article
Nœuds enfants de contraste
La comparaison du nœud lui-même est terminée et l'étape suivante consiste à comparer ses nœuds enfants.
这里会面临一个问题,前面我们实现的不同diff方法,都是明确知道哪一个真实DOM和虚拟DOM对比,但是子节点是一个数组,它们可能改变了顺序,或者数量有所变化,我们很难确定要和虚拟DOM对比的是哪一个。
为了简化逻辑,我们可以让用户提供一些线索:给节点设一个key值,重新渲染时对比key值相同的节点。
// diff方法 if ( vnode.children && vnode.children.length > 0 || ( out.childNodes && out.childNodes.length > 0 ) ) { diffChildren( out, vnode.children ); }
function diffChildren( dom, vchildren ) { const domChildren = dom.childNodes; const children = []; const keyed = {}; // 将有key的节点和没有key的节点分开 if ( domChildren.length > 0 ) { for ( let i = 0; i < domChildren.length; i++ ) { const child = domChildren[ i ]; const key = child.key; if ( key ) { keyedLen++; keyed[ key ] = child; } else { children.push( child ); } } } if ( vchildren && vchildren.length > 0 ) { let min = 0; let childrenLen = children.length; for ( let i = 0; i < vchildren.length; i++ ) { const vchild = vchildren[ i ]; const key = vchild.key; let child; // 如果有key,找到对应key值的节点 if ( key ) { if ( keyed[ key ] ) { child = keyed[ key ]; keyed[ key ] = undefined; } // 如果没有key,则优先找类型相同的节点 } else if ( min < childrenLen ) { for ( let j = min; j < childrenLen; j++ ) { let c = children[ j ]; if ( c && isSameNodeType( c, vchild ) ) { child = c; children[ j ] = undefined; if ( j === childrenLen - 1 ) childrenLen--; if ( j === min ) min++; break; } } } // 对比 child = diff( child, vchild ); // 更新DOM const f = domChildren[ i ]; if ( child && child !== dom && child !== f ) { if ( !f ) { dom.appendChild(child); } else if ( child === f.nextSibling ) { removeNode( f ); } else { dom.insertBefore( child, f ); } } } } }
对比组件
如果vnode是一个组件,我们也单独拿出来作为一个方法:
function diffComponent( dom, vnode ) { let c = dom && dom._component; let oldDom = dom; // 如果组件类型没有变化,则重新set props if ( c && c.constructor === vnode.tag ) { setComponentProps( c, vnode.attrs ); dom = c.base; // 如果组件类型变化,则移除掉原来组件,并渲染新的组件 } else { if ( c ) { unmountComponent( c ); oldDom = null; } c = createComponent( vnode.tag, vnode.attrs ); setComponentProps( c, vnode.attrs ); dom = c.base; if ( oldDom && dom !== oldDom ) { oldDom._component = null; removeNode( oldDom ); } } return dom; }
下面是相关的工具方法的实现,和上一篇文章的实现相比,只需要修改renderComponent方法其中的一行。
function renderComponent( component ) { // ... // base = base = _render( renderer ); // 将_render改成diff base = diff( component.base, renderer ); // ... }
完整diff实现看这个文件
渲染
现在我们实现了diff方法,我们尝试渲染上一篇文章中定义的Counter组件,来感受一下有无diff方法的不同。
class Counter extends React.Component { constructor( props ) { super( props ); this.state = { num: 1 } } onClick() { this.setState( { num: this.state.num + 1 } ); } render() { return ( <p> <h1>count: { this.state.num }</h1> <button onClick={ () => this.onClick()}>add</button> </p> ); } }
不使用diff
使用上一篇文章的实现,从chrome的调试工具中可以看到,闪烁的部分是每次更新的部分,每次点击按钮,都会重新渲染整个组件。
使用diff
而实现了diff方法后,每次点击按钮,都只会重新渲染变化的部分。
后话
在这篇文章中我们实现了diff算法,通过它做到了每次只更新需要更新的部分,极大地减少了DOM操作。React实现远比这个要复杂,特别是在React 16之后还引入了Fiber架构,但是主要的思想是一致的。
实现diff算法可以说性能有了很大的提升,但是在别的地方仍然后很多改进的空间:每次调用setState后会立即调用renderComponent重新渲染组件,但现实情况是,我们可能会在极短的时间内多次调用setState。
假设我们在上文的Counter组件中写出了这种代码
onClick() { for ( let i = 0; i < 100; i++ ) { this.setState( { num: this.state.num + 1 } ); } }
那以目前的实现,每次点击都会渲染100次组件,对性能肯定有很大的影响。
相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
推荐阅读:
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!