Maison  >  Article  >  interface Web  >  Comment écrire votre propre DOM virtuel ? Présentation de la méthode

Comment écrire votre propre DOM virtuel ? Présentation de la méthode

青灯夜游
青灯夜游avant
2020-10-29 17:30:212665parcourir

Comment écrire votre propre DOM virtuel ? Présentation de la méthode

Pour créer votre propre DOM virtuel, vous devez savoir deux choses. Vous n'avez même pas besoin de fouiller dans le code source de React ou de toute autre implémentation du DOM virtuel car ils sont si volumineux et complexes - mais en fait, la partie principale du DOM virtuel ne prend que moins de 50 lignes de code.

Il existe deux concepts :

  • Le DOM virtuel est une cartographie du DOM réel
  • Lorsque certains nœuds de l'arborescence du DOM virtuel changent, vous obtenez un nouvel arbre virtuel. L'algorithme compare les deux arbres (nouveau et ancien), trouve les différences, puis apporte simplement les modifications correspondantes sur le vrai DOM.

Simuler l'arborescence DOM avec des objets JS

Tout d'abord, nous devons stocker l'arborescence DOM en mémoire d'une manière ou d'une autre. Cela peut être fait en utilisant des objets JS normaux. Supposons que nous ayons un arbre comme celui-ci :


      
  • item 1
  •   
  • item 2

Ça a l'air simple, non ? Comment le représenter avec un objet JS ?

{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [
  { type: ‘li’, props: {}, children: [‘item 1’] },
  { type: ‘li’, props: {}, children: [‘item 2’] }
] }

Il y a deux choses à noter ici :

  • Utilisez les objets suivants pour représenter les éléments DOM
{ type: ‘…’, props: { … }, children: [ … ] }
  • Utilisez des chaînes JS ordinaires pour représenter les nœuds de texte DOM

Mais il y a beaucoup de contenu exprimé de cette façon, les arbres Dom sont assez difficiles. Écrivons ici une fonction auxiliaire pour faciliter la compréhension :

function h(type, props, …children) {
  return { type, props, children };
}

Utilisez cette méthode pour réorganiser le code initial :

h(‘ul’, { ‘class’: ‘list’ },
  h(‘li’, {}, ‘item 1’),
  h(‘li’, {}, ‘item 2’),
);

Cela semble beaucoup plus simple et vous pouvez aller plus loin. JSX est utilisé ici, comme suit :


      
  • item 1
  •   
  • item 2

est compilé en :

React.createElement(‘ul’, { className: ‘list’ },
  React.createElement(‘li’, {}, ‘item 1’),
  React.createElement(‘li’, {}, ‘item 2’),
);

Cela vous semble-t-il familier ? Si nous pouvons remplacer h(...) par la fonction React.createElement(…) que nous venons de définir, alors nous pouvons également utiliser la syntaxe JSX. En fait, il vous suffit d'ajouter ce commentaire en tête du fichier source :

/** @jsx h */
      
  • item 1
  •   
  • item 2

Il dit en fait à Babel 'Hé, petit frère, aide-moi à compiler la syntaxe JSX et à utiliser le h(...) fonctionne à la place React.createElement(…), puis Babel commence à compiler. '

Pour résumer, on écrit le DOM comme ceci :

/** @jsx h */
const a = (
  
        
  • item 1
  •     
  • item 2
  •   
);

Babel va nous aider à le compiler en code comme ceci :

const a = (
  h(‘ul’, { className: ‘list’ },
    h(‘li’, {}, ‘item 1’),
    h(‘li’, {}, ‘item 2’),
  );
);

Quand la fonction “h” est exécuté, il renverra un objet JS normal - c'est-à-dire notre DOM virtuel :

const a = (
  { type: ‘ul’, props: { className: ‘list’ }, children: [
    { type: ‘li’, props: {}, children: [‘item 1’] },
    { type: ‘li’, props: {}, children: [‘item 2’] }
  ] }
);

mappe du DOM virtuel au DOM réel

D'accord, nous avons maintenant l'arborescence DOM, représentée par un objet JS normal , Et notre propre structure. C'est cool, mais nous devons en créer un véritable DOM.

Faisons d'abord quelques hypothèses et déclarons quelques termes :

  • Utilisons des variables commençant par ' $ ' pour représenter de vrais nœuds DOM (éléments, nœuds de texte), donc $ parent le fera être un véritable élément DOM
  • DOM virtuel représenté par une variable nommée node

* Tout comme dans React, il ne peut y avoir qu'un seul nœud racine - —Tous les autres nœuds sont à l'intérieur

Alors, écrivons une fonction createElement(…) qui obtiendra un nœud DOM virtuel et renverra un nœud DOM réel. Ignorez les attributs props et children ici :

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  return document.createElement(node.type);
}

Avec la méthode ci-dessus, je peux également créer deux types de nœuds, à savoir les nœuds de texte et les nœuds d'éléments Dom, qui sont des objets JS de type :

{ type: ‘…’, props: { … }, children: [ … ] }

Par conséquent, vous pouvez transmettre des nœuds de texte factices et des nœuds d'éléments factices dans functioncreateElement - cela fonctionne.

Considérons maintenant les nœuds enfants – chacun d’eux est un nœud ou un élément de texte. Ils peuvent donc également être créés avec la fonction createElement(…). Oui, c'est comme la récursion, nous pouvons donc appeler createElement(…) pour les enfants de chaque élément, puis utiliser appendChild() pour ajouter à notre élément :

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

Wow, ça a l'air bien. Mettons d’abord de côté l’attribut node props. On se parle plus tard. Nous n'avons pas besoin qu'ils comprennent les concepts de base du DOM virtuel car ils ajoutent de la complexité.

Le code complet est le suivant :

/** @jsx h */

function h(type, props, ...children) {
  return { type, props, children };
}

function createElement(node) {
  if (typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

const a = (
  
        
  • item 1
  •     
  • item 2
  •   
); const $root = document.getElementById('root'); $root.appendChild(createElement(a));

Comparez la différence entre deux arbres DOM virtuels

Nous pouvons maintenant convertir le DOM virtuel en un DOM réel, ce qui nécessite de comparer les deux arbres différences d'arbre DOM. Fondamentalement, nous avons besoin d'un algorithme pour comparer le nouvel arbre avec l'ancien arbre, ce qui nous permet de savoir ce qui a changé, puis de modifier le vrai DOM en conséquence.

Comment comparer les arbres DOM ? Les situations suivantes doivent être gérées :

  • Pour ajouter un nouveau nœud, utilisez la méthode appendChild(…) pour ajouter un nœud

Comment écrire votre propre DOM virtuel ? Présentation de la méthode

  • Supprimez les anciens nœuds, utilisez la méthode removeChild(...) pour supprimer les anciens nœuds

Comment écrire votre propre DOM virtuel ? Présentation de la méthode

  • Remplacement des nœuds, utilisez la méthode replaceChild(…)

Comment écrire votre propre DOM virtuel ? Présentation de la méthode

Si les nœuds sont les mêmes - vous devez comparer profondément les nœuds enfants

Comment écrire votre propre DOM virtuel ? Présentation de la méthode

编写一个名为 updateElement(…) 的函数,它接受三个参数—— $parentnewNodeoldNode,其中 $parent 是虚拟节点的一个实际 DOM 元素的父元素。现在来看看如何处理上面描述的所有情况。

添加新节点

function updateElement($parent, newNode, oldNode) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  }
}

移除老节点

这里遇到了一个问题——如果在新虚拟树的当前位置没有节点——我们应该从实际的 DOM 中删除它—— 这要如何做呢?

如果我们已知父元素(通过参数传递),我们就能调用 $parent.removeChild(…) 方法把变化映射到真实的 DOM 上。但前提是我们得知道我们的节点在父元素上的索引,我们才能通过 $parent.childNodes[index] 得到该节点的引用。

好的,让我们假设这个索引将被传递给 updateElement 函数(它确实会被传递——稍后将看到)。代码如下:

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  }
}

节点的替换

首先,需要编写一个函数来比较两个节点(旧节点和新节点),并告诉节点是否真的发生了变化。还有需要考虑这个节点可以是元素或是文本节点:

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
         typeof node1 === ‘string’ && node1 !== node2 ||
         node1.type !== node2.type
}

现在,当前的节点有了 index 属性,就可以很简单的用新节点替换它:

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  }
}

比较子节点

最后,但并非最不重要的是——我们应该遍历这两个节点的每一个子节点并比较它们——实际上为每个节点调用updateElement(…)方法,同样需要用到递归。

  • 当节点是 DOM 元素时我们才需要比较( 文本节点没有子节点 )
  • 我们需要传递当前的节点的引用作为父节点
  • 我们应该一个一个的比较所有的子节点,即使它是 undefined 也没有关系,我们的函数也会正确处理它。
  • 最后是 index,它是子数组中子节点的 index
function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  } else if (newNode.type) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    for (let i = 0; i <h2>完整的代码</h2><p><strong>Babel+JSX</strong><br>/<em>* @jsx h </em>/</p><pre class="brush:php;toolbar:false">function h(type, props, ...children) {
  return { type, props, children };
}

function createElement(node) {
  if (typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
         typeof node1 === 'string' && node1 !== node2 ||
         node1.type !== node2.type
}

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  } else if (newNode.type) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    for (let i = 0; i 
    
  • item 1
  •     
  • item 2
  •    ); const b = (   
          
    • item 1
    •     
    • hello!
    •   
    ); const $root = document.getElementById('root'); const $reload = document.getElementById('reload'); updateElement($root, a); $reload.addEventListener('click', () => {   updateElement($root, b, a); });

    HTML

    <button>RELOAD</button>
    <p></p>

    CSS

    #root {
      border: 1px solid black;
      padding: 10px;
      margin: 30px 0 0 0;
    }

    打开开发者工具,并观察当按下“Reload”按钮时应用的更改。

    Comment écrire votre propre DOM virtuel ? Présentation de la méthode

    总结

    现在我们已经编写了虚拟 DOM 实现及了解它的工作原理。作者希望,在阅读了本文之后,对理解虚拟 DOM 如何工作的基本概念以及在幕后如何进行响应有一定的了解。

    然而,这里有一些东西没有突出显示(将在以后的文章中介绍它们):

    • 设置元素属性(props)并进行 diffing/updating
    • 处理事件——向元素中添加事件监听
    • 让虚拟 DOM 与组件一起工作,比如React
    • 获取对实际DOM节点的引用
    • 使用带有库的虚拟 DOM,这些库可以直接改变真实的 DOM,比如 jQuery 及其插件

    原文地址:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060

    作者:deathmood

    为了保证的可读性,本文采用意译而非直译。

    更多编程相关知识,请访问:编程入门!!

    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