자신만의 가상 DOM을 구축하려면 두 가지를 알아야 합니다. React나 다른 가상 DOM 구현의 소스 코드는 너무 크고 복잡하기 때문에 자세히 살펴볼 필요도 없습니다. 하지만 실제로 가상 DOM의 주요 부분에는 50줄 미만의 코드만 필요합니다.
두 가지 개념이 있습니다.
먼저 DOM 트리를 어떻게든 메모리에 저장해야 합니다. 이는 일반 JS 객체를 사용하여 수행할 수 있습니다. 다음과 같은 트리가 있다고 가정해 보겠습니다.
간단해 보이죠? 어떻게 JS 객체로 표현할 수 있을까요?
{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [ { type: ‘li’, props: {}, children: [‘item 1’] }, { type: ‘li’, props: {}, children: [‘item 2’] } ] }
여기서 주목해야 할 두 가지가 있습니다.
{ type: ‘…’, props: { … }, children: [ … ] }
하지만 이런 방식으로 콘텐츠가 많은 Dom 트리를 표현하는 것은 꽤 어렵습니다. 이해하기 쉽도록 여기에 보조 함수를 작성해 보겠습니다.
function h(type, props, …children) { return { type, props, children }; }
이 방법을 사용하여 초기 코드를 재구성합니다.
h(‘ul’, { ‘class’: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’), );
훨씬 간단해 보이고 더 나아갈 수 있습니다. 여기서는 JSX를 다음과 같이 사용합니다.
는 다음과 같이 컴파일됩니다.
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 */
它实际上告诉 Babel ' 嘿,小老弟帮我编译 JSX 语法,用 h(...)
函数代替 React.createElement(…)
,然后 Babel 就开始编译。'
综上所述,我们将DOM写成这样:
/** @jsx h */ const a = (
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’] } ] } );
好了,现在我们有了 DOM 树,用普通的 JS 对象表示,还有我们自己的结构。这很酷,但我们需要从它创建一个真正的DOM。
首先让我们做一些假设并声明一些术语:
$
'开头的变量表示真正的DOM节点(元素,文本节点),因此 $parent 将会是一个真实的DOM元素node
的变量表示* 就像在 React 中一样,只能有一个根节点——所有其他节点都在其中
那么,来编写一个函数 createElement(…)
,它将获取一个虚拟 DOM 节点并返回一个真实的 DOM 节点。这里先不考虑 props
和 children
属性:
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 = (
JSX
구문 컴파일을 도와주세요.h(...)React.createElement(…)
대신 /code> 함수를 호출하면 function updateElement($parent, newNode, oldNode) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } }
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 노드(요소, 텍스트 노드)를 나타내므로 $parent는 실제 DOM 요소가 됩니다. 가상 DOM은 node
라는 변수를 사용하여 표현됩니다.* React와 마찬가지로 루트 노드는 하나만 있을 수 있습니다. 다른 모든 노드는 그 안에 있습니다
그래서 함수 createElement(…)는 가상 DOM 노드를 가져오고 실제 DOM 노드를 반환합니다. 여기에서props
및 children
속성을 무시하세요. 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] ); } }
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(…) 함수를 사용하여 생성할 수도 있습니다. 예, 이것은 재귀처럼 작동하므로 각 요소의 하위 항목에 대해
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
Wow, 보기 좋습니다. 먼저 노드 props
속성을 따로 보관하세요. 나중에 얘기하세요. 복잡성이 추가되므로 가상 DOM의 기본 개념을 이해할 필요는 없습니다.
<button>RELOAD</button> <p></p>
编写一个名为 updateElement(…) 的函数,它接受三个参数—— $parent
、newNode 和 oldNode,其中 $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(…)方法,同样需要用到递归。
undefined
也没有关系,我们的函数也会正确处理它。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로그인 후 복사
HTML
<button>RELOAD</button> <p></p>
CSS
#root { border: 1px solid black; padding: 10px; margin: 30px 0 0 0; }
打开开发者工具,并观察当按下“Reload”按钮时应用的更改。
现在我们已经编写了虚拟 DOM 实现及了解它的工作原理。作者希望,在阅读了本文之后,对理解虚拟 DOM 如何工作的基本概念以及在幕后如何进行响应有一定的了解。
然而,这里有一些东西没有突出显示(将在以后的文章中介绍它们):
原文地址:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060
作者:deathmood
为了保证的可读性,本文采用意译而非直译。
更多编程相关知识,请访问:编程入门!!
위 내용은 자신만의 가상 DOM을 작성하는 방법은 무엇입니까? 방법 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!