> 웹 프론트엔드 > JS 튜토리얼 > 자신만의 가상 DOM을 작성하는 방법은 무엇입니까? 방법 소개

자신만의 가상 DOM을 작성하는 방법은 무엇입니까? 방법 소개

青灯夜游
풀어 주다: 2020-10-29 17:30:21
앞으로
2700명이 탐색했습니다.

자신만의 가상 DOM을 작성하는 방법은 무엇입니까? 방법 소개

자신만의 가상 DOM을 구축하려면 두 가지를 알아야 합니다. React나 다른 가상 DOM 구현의 소스 코드는 너무 크고 복잡하기 때문에 자세히 살펴볼 필요도 없습니다. 하지만 실제로 가상 DOM의 주요 부분에는 50줄 미만의 코드만 필요합니다.

두 가지 개념이 있습니다.

  • 가상 DOM은 실제 DOM의 매핑입니다.
  • 가상 DOM 트리의 일부 노드가 변경되면 새로운 가상 트리가 획득됩니다. 알고리즘은 두 개의 트리(새 트리와 이전 트리)를 비교하고 차이점을 찾은 다음 실제 DOM에 해당 변경을 적용합니다.

JS 객체로 DOM 트리 시뮬레이션

먼저 DOM 트리를 어떻게든 메모리에 저장해야 합니다. 이는 일반 JS 객체를 사용하여 수행할 수 있습니다. 다음과 같은 트리가 있다고 가정해 보겠습니다.

로그인 후 복사
로그인 후 복사
      
  • item 1
  •   
  • item 2

간단해 보이죠? 어떻게 JS 객체로 표현할 수 있을까요?

{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [
  { type: ‘li’, props: {}, children: [‘item 1’] },
  { type: ‘li’, props: {}, children: [‘item 2’] }
] }
로그인 후 복사

여기서 주목해야 할 두 가지가 있습니다.

  • 다음 객체를 사용하여 DOM 요소를 표현하세요
{ type: ‘…’, props: { … }, children: [ … ] }
로그인 후 복사
로그인 후 복사
  • 일반 JS 문자열을 사용하여 DOM 텍스트 노드를 표현합니다

하지만 이런 방식으로 콘텐츠가 많은 Dom 트리를 표현하는 것은 꽤 어렵습니다. 이해하기 쉽도록 여기에 보조 함수를 작성해 보겠습니다.

function h(type, props, …children) {
  return { type, props, children };
}
로그인 후 복사

이 방법을 사용하여 초기 코드를 재구성합니다.

h(‘ul’, { ‘class’: ‘list’ },
  h(‘li’, {}, ‘item 1’),
  h(‘li’, {}, ‘item 2’),
);
로그인 후 복사

훨씬 간단해 보이고 더 나아갈 수 있습니다. 여기서는 JSX를 다음과 같이 사용합니다.

로그인 후 복사
로그인 후 복사
      
  • item 1
  •   
  • item 2

는 다음과 같이 컴파일됩니다.

React.createElement(‘ul’, { className: ‘list’ },
  React.createElement(‘li’, {}, ‘item 1’),
  React.createElement(‘li’, {}, ‘item 2’),
);
로그인 후 복사

좀 친숙해 보이죠? React.createElement(…)를 방금 정의한 h(...) 함수로 바꿀 수 있다면 JSX 구문을 사용할 수도 있습니다. 실제로 소스 파일의 헤드에 이 주석만 추가하면 됩니다. h(...) 函数代替 React.createElement(…),那么我们也能使用JSX 语法。其实,只需要在源文件头部加上这么一句注释:

/** @jsx h */
로그인 후 복사
      
  • item 1
  •   
  • item 2

它实际上告诉 Babel ' 嘿,小老弟帮我编译 JSX 语法,用 h(...) 函数代替 React.createElement(…),然后 Babel 就开始编译。'

综上所述,我们将DOM写成这样:

/** @jsx h */
const a = (
  
로그인 후 복사
        
  • item 1
  •     
  • item 2
  •   
);

Babel 会帮我们编译成这样的代码:

const a = (
  h(‘ul’, { className: ‘list’ },
    h(‘li’, {}, ‘item 1’),
    h(‘li’, {}, ‘item 2’),
  );
);
로그인 후 복사

当函数 “h” 执行时,它将返回普通JS对象-即我们的虚拟DOM:

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

从Virtual DOM 映射到真实 DOM

好了,现在我们有了 DOM 树,用普通的 JS 对象表示,还有我们自己的结构。这很酷,但我们需要从它创建一个真正的DOM。

首先让我们做一些假设并声明一些术语:

  • 使用以' $ '开头的变量表示真正的DOM节点(元素,文本节点),因此 $parent 将会是一个真实的DOM元素
  • 虚拟 DOM 使用名为 node 的变量表示

* 就像在 React 中一样,只能有一个根节点——所有其他节点都在其中

那么,来编写一个函数 createElement(…),它将获取一个虚拟 DOM 节点并返回一个真实的 DOM 节点。这里先不考虑 propschildren 属性:

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  return document.createElement(node.type);
}
로그인 후 복사

上述方法我也可以创建有两种节点分别是文本节点和 Dom 元素节点,它们是类型为的 JS 对象:

{ type: ‘…’, props: { … }, children: [ … ] }
로그인 후 복사
로그인 후 복사

因此,可以在函数 createElement 传入虚拟文本节点和虚拟元素节点——这是可行的。

现在让我们考虑子节点——它们中的每一个都是文本节点或元素。所以它们也可以用 createElement(…) 函数创建。是的,这就像递归一样,所以我们可以为每个元素的子元素调用 createElement(…),然后使用 appendChild() 添加到我们的元素中:

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;
}
로그인 후 복사

哇,看起来不错。先把节点 props

/** @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)); 실제로 Babel에게 '안녕, 동생아,

JSX

구문 컴파일을 도와주세요. h(...)React.createElement(…) 대신 /code> 함수를 호출하면

Babel

이 컴파일을 시작합니다. '

요약하자면 다음과 같이 DOM을 작성합니다.

function updateElement($parent, newNode, oldNode) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  }
}
로그인 후 복사
로그인 후 복사
Babel은 이를 다음과 같은 코드로 컴파일하는 데 도움이 됩니다.
    function updateElement($parent, newNode, oldNode, index = 0) {
      if (!oldNode) {
        $parent.appendChild(
          createElement(newNode)
        );
      } else if (!newNode) {
        $parent.removeChild(
          $parent.childNodes[index]
        );
      }
    }
    로그인 후 복사
    로그인 후 복사
  • 함수 "h"가 실행되면 반환됩니다. 일반 JS 개체 - 즉, 가상 DOM:
    function changed(node1, node2) {
      return typeof node1 !== typeof node2 ||
             typeof node1 === ‘string’ && node1 !== node2 ||
             node1.type !== node2.type
    }
    로그인 후 복사
    로그인 후 복사
    가상 DOM에서 실제 DOM으로 매핑
  • 자, 이제 우리는 자체 구조를 가진 일반 JS 개체로 표현되는 DOM 트리를 갖게 되었습니다. 이것은 멋지지만 여기서는 실제 DOM을 생성해야 합니다.

    자신만의 가상 DOM을 작성하는 방법은 무엇입니까? 방법 소개먼저 몇 가지 가정을 하고 몇 가지 용어를 선언해 보겠습니다.

    • ' $'로 시작하는 변수를 사용하여 실제 DOM 노드(요소, 텍스트 노드)를 나타내므로 $parent는 실제 DOM 요소가 됩니다. 가상 DOM은 node라는 변수를 사용하여 표현됩니다.

    * React와 마찬가지로 루트 노드는 하나만 있을 수 있습니다. 다른 모든 노드는 그 안에 있습니다자신만의 가상 DOM을 작성하는 방법은 무엇입니까? 방법 소개

    그래서 함수 createElement(…)는 가상 DOM 노드를 가져오고 실제 DOM 노드를 반환합니다. 여기에서 propschildren 속성을 ​​무시하세요.
      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]
          );
        }
      }
      로그인 후 복사
      로그인 후 복사
    • 위의 방법을 사용하면 두 가지 유형의 노드, 즉 텍스트 노드와 Dom 요소 노드를 생성할 수도 있습니다. 유형 JS 객체:
      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 
      로그인 후 복사
      따라서 createElement 함수에 가상 텍스트 노드와 가상 요소 노드를 전달할 수 있습니다. 이는 작동합니다.
    • 이제 하위 노드를 고려해 보겠습니다. 각 노드는 텍스트 노드 또는 요소입니다. 따라서

      createElement(…)자신만의 가상 DOM을 작성하는 방법은 무엇입니까? 방법 소개 함수를 사용하여 생성할 수도 있습니다. 예, 이것은 재귀처럼 작동하므로 각 요소의 하위 항목에 대해

      createElement(…)

      를 호출한 다음 appendChild()를 사용하여 요소에 추가할 수 있습니다.

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

      Wow, 보기 좋습니다. 먼저 노드 props 속성을 ​​따로 보관하세요. 나중에 얘기하세요. 복잡성이 추가되므로 가상 DOM의 기본 개념을 이해할 필요는 없습니다. 자신만의 가상 DOM을 작성하는 방법은 무엇입니까? 방법 소개

      전체 코드는 다음과 같습니다. 🎜
      <button>RELOAD</button>
      <p></p>
      로그인 후 복사
      로그인 후 복사
      🎜두 가상 DOM 트리의 차이점 비교🎜🎜이제 가상 DOM을 실제 DOM으로 변환할 수 있으므로 두 DOM 트리의 차이점을 비교하는 것을 고려해야 합니다. 기본적으로 새 트리와 이전 트리를 비교하여 무엇이 변경되었는지 파악하고 이에 따라 실제 DOM을 변경하는 알고리즘이 필요합니다. 🎜🎜DOM 트리를 비교하는 방법은 무엇입니까? 다음 상황을 처리해야 합니다. 🎜🎜🎜새 노드 추가, 🎜appendChild(…)🎜 메서드를 사용하여 노드 추가🎜🎜🎜🎜🎜🎜🎜이전 노드 제거, 🎜removeChild(…)🎜 메서드를 사용하여 이전 노드 제거 🎜🎜 🎜🎜🎜🎜🎜노드를 교체하려면 🎜replaceChild(...)🎜 메서드를 사용하세요🎜🎜🎜🎜🎜🎜노드가 동일한 경우 - 하위 노드를 깊이 비교해야 합니다🎜🎜🎜🎜

      编写一个名为 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 

      完整的代码

      Babel+JSX
      /* @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;
      }
      
      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”按钮时应用的更改。

      자신만의 가상 DOM을 작성하는 방법은 무엇입니까? 방법 소개

      总结

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

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

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

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

      作者:deathmood

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

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

      위 내용은 자신만의 가상 DOM을 작성하는 방법은 무엇입니까? 방법 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

      원천:segmentfault.com
      본 웹사이트의 성명
      본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
      인기 튜토리얼
      더>
      최신 다운로드
      더>
      웹 효과
      웹사이트 소스 코드
      웹사이트 자료
      프론트엔드 템플릿