We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
原文:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060
构建你自己的虚拟DOM,你需要知道2个事情。你不需要理解 React,或者深入其他虚拟DOM的实现源码。他们太过庞大和复杂,事实上,虚拟DOM的核心代码的实现甚至可以少于50行代码。
React
下面是2个原则:
下面让我们深入上面的2个原则。
首先,我们需要一种方式来存储dom树。我们可以使用 javascript 对象。假设我们有下面这样一颗树:
javascript
<ul class=”list”> <li>item 1</li> <li>item 2</li> </ul>
看起来是不是很简单,我们如何用 javascript 对象表示这棵树呢?
{ "type":"ul", "props":{ "class":"list" }, "children":[ { "type":"li", "props":{ }, "children":[ "item 1" ] }, { "type":"li", "props":{ }, "children":[ "item 2" ] } ] }
我们可以注意到2个事情:
{ type: ‘…’, props: { … }, children: [ … ] }
但是用这种方式编写一棵很大的树会很困难,所我们需要一个帮助方法,这样可以使我们很容易理解DOM树的结构。
function h(type, props, …children) { return { type, props, children }; }
现在我们可以这样编写我们的DOM树:
h(‘ul’, { ‘class’: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’), );
看起来清晰多了,但是我们可以更进一步。你应该听说过 JSX ,我想在这样用到它。它是怎么工作的呢?
JSX
如果你读了 Babel JSX 文档 ,你会知道 Babel 把下面的代码:
Babel JSX
Babel
<ul className=”list”> <li>item 1</li> <li>item 2</li> </ul>
转化成:
React.createElement(‘ul’, { className: ‘list’ }, React.createElement(‘li’, {}, ‘item 1’), React.createElement(‘li’, {}, ‘item 2’), );
是不是很相似?如果把 React.createElement(…) 替换为 h(…),我们就可以使用 jsx 语法。我们只需要在文件头部添加一行注释。
jsx
/** @jsx h */ <ul className=”list”> <li>item 1</li> <li>item 2</li> </ul>
它会告诉 Babel 把 React.createElement(…) 替换为 h(…)。当然,h 也可以用其他任意的方法。
做个总结,我会这样写 DOM:
DOM
/** @jsx h */ const a = ( <ul className=”list”> <li>item 1</li> <li>item 2</li> </ul> );
Babel 会把它翻译为:
const a = ( h(‘ul’, { className: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’), ); );
当 h 方法执行时,它会返回 js 对象 - 虚拟DOM:
h
const a = ( { type: ‘ul’, props: { className: ‘list’ }, children: [ { type: ‘li’, props: {}, children: [‘item 1’] }, { type: ‘li’, props: {}, children: [‘item 2’] } ] } );
现在我们有了根据我们的要求用 js 对象表达的 DOM 树。但是我们需要根据 DOM 树创建真实的 DOM。因为我们不能直接追加表达式到真实的DOM中。
首先我们做一些假设,制定一下术语:
真实DOM节点的变量我会以 $ 开头,因此 $parent 就是一个真实的DOM元素
$
虚拟DOM表达式将在名为node的变量中
比如在 React 中,你只会有一个根节点(one root node),其他所有的节点都在根节点下
入上文所说,让我们创建 createElement(…),函数会把一个虚拟DOM节点转化为一个真实DOM节点。暂时忘记 props 和 children,我们会在后面设置。
props
children
function createElement(node) { if (typeof node === ‘string’) { return document.createTextNode(node); } return document.createElement(node.type); }
我们支持 文本节点 (就是js字符串) 和 元素节点 (就是js对象)。
这样,我们可以同时传递文本节点和元素节点。
现在我们考虑一下 children,children 的每个元素要么是文本节点,要么是元素节点。所以我们也可以通过 createElement(…) 方法创建。有没有感觉就是个递归,我们可以未元素 children 的调用 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 放到一边,后面再讨论。理解虚拟DOM的基础原则不需要用到它们, 而且它们会增加复杂度。
现在我们可以把一个虚拟DOM转化为真实的DOM,是时候取考虑比对我们的虚拟DOM树。就是说,我们需要实现一个算法,算法会比对2棵虚拟DOM树 - 新的和旧的,真实的DOM只做必要的更新。
如何比较2棵树?我们需要处理下一个例子:
让我们创建一个函数 updateElement(…),函数有3个参数-$parent, newNode and oldNode。$parent 是真实的DOM节点,虚拟DOM的父亲节点。现在让我们看看怎么处理上面提到的所有例子。
这个很简单,我都不用注释:
function updateElement($parent, newNode, oldNode) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } }
如果新的虚拟DOM树的当前位置没有节点,我们需要从真实DOM中删除对应的节点。我们知道父亲节点,这样就可以调用 *$parent.removeChild(…)*方法,传递真实的DOM节点引用。但是我们没有这个引用。但是如果我们知道节点在夫妻节点中的位置,我们可以通过 $parent.childNodes[index] 拿到这个引用,inex 是节点在父亲节点中的位置。
假设 index 会传递到我们的方法中。代码如下:
function updateElement($parent, newNode, oldNode, index = 0) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } else if (!newNode) { $parent.removeChild( $parent.childNodes[index] ); } }
首先我们创建一个方法用于比较2个节点(新的和旧的),告诉我们哪个节点有变化。我们需要同时考虑元素节点和文本节点。
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 < newLength || i < oldLength; i++) { updateElement( $parent.childNodes[index], newNode.children[i], oldNode.children[i], i ); } } }
/** @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 < newLength || i < oldLength; i++) { updateElement( $parent.childNodes[index], newNode.children[i], oldNode.children[i], i ); } } } // --------------------------------------------------------------------- const a = ( <ul> <li>item 1</li> <li>item 2</li> </ul> ); const b = ( <ul> <li>item 1</li> <li>hello!</li> </ul> ); const $root = document.getElementById('root'); const $reload = document.getElementById('reload'); updateElement($root, a); $reload.addEventListener('click', () => { updateElement($root, b, a); });
恭喜,我们做到了。我们实现了虚拟DOM。通过这个文章,我们读者能理解虚拟DOM的基本原则,以及 React 底层是怎么工作的。
然而有一些这里没有提到的:
jQuery
The text was updated successfully, but these errors were encountered:
No branches or pull requests
原文:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060
构建你自己的虚拟DOM,你需要知道2个事情。你不需要理解
React
,或者深入其他虚拟DOM的实现源码。他们太过庞大和复杂,事实上,虚拟DOM的核心代码的实现甚至可以少于50行代码。下面是2个原则:
下面让我们深入上面的2个原则。
表示我们的dom树
首先,我们需要一种方式来存储dom树。我们可以使用
javascript
对象。假设我们有下面这样一颗树:看起来是不是很简单,我们如何用
javascript
对象表示这棵树呢?我们可以注意到2个事情:
但是用这种方式编写一棵很大的树会很困难,所我们需要一个帮助方法,这样可以使我们很容易理解DOM树的结构。
现在我们可以这样编写我们的DOM树:
看起来清晰多了,但是我们可以更进一步。你应该听说过
JSX
,我想在这样用到它。它是怎么工作的呢?如果你读了
Babel JSX
文档 ,你会知道Babel
把下面的代码:转化成:
是不是很相似?如果把 React.createElement(…) 替换为 h(…),我们就可以使用
jsx
语法。我们只需要在文件头部添加一行注释。它会告诉
Babel
把 React.createElement(…) 替换为 h(…)。当然,h 也可以用其他任意的方法。做个总结,我会这样写
DOM
:Babel
会把它翻译为:当
h
方法执行时,它会返回 js 对象 - 虚拟DOM:应用一下 DOM 表达
现在我们有了根据我们的要求用 js 对象表达的 DOM 树。但是我们需要根据 DOM 树创建真实的 DOM。因为我们不能直接追加表达式到真实的DOM中。
首先我们做一些假设,制定一下术语:
真实DOM节点的变量我会以
$
开头,因此 $parent 就是一个真实的DOM元素虚拟DOM表达式将在名为node的变量中
比如在
React
中,你只会有一个根节点(one root node),其他所有的节点都在根节点下入上文所说,让我们创建 createElement(…),函数会把一个虚拟DOM节点转化为一个真实DOM节点。暂时忘记
props
和children
,我们会在后面设置。我们支持 文本节点 (就是js字符串) 和 元素节点 (就是js对象)。
这样,我们可以同时传递文本节点和元素节点。
现在我们考虑一下
children
,children
的每个元素要么是文本节点,要么是元素节点。所以我们也可以通过 createElement(…) 方法创建。有没有感觉就是个递归,我们可以未元素children
的调用 createElement(…) 方法,appendChild() 他们到元素中。我们暂时把 props 放到一边,后面再讨论。理解虚拟DOM的基础原则不需要用到它们, 而且它们会增加复杂度。
处理更新
现在我们可以把一个虚拟DOM转化为真实的DOM,是时候取考虑比对我们的虚拟DOM树。就是说,我们需要实现一个算法,算法会比对2棵虚拟DOM树 - 新的和旧的,真实的DOM只做必要的更新。
如何比较2棵树?我们需要处理下一个例子:
让我们创建一个函数 updateElement(…),函数有3个参数-$parent, newNode and oldNode。$parent 是真实的DOM节点,虚拟DOM的父亲节点。现在让我们看看怎么处理上面提到的所有例子。
没有旧的节点
这个很简单,我都不用注释:
没有新的节点
如果新的虚拟DOM树的当前位置没有节点,我们需要从真实DOM中删除对应的节点。我们知道父亲节点,这样就可以调用 *$parent.removeChild(…)*方法,传递真实的DOM节点引用。但是我们没有这个引用。但是如果我们知道节点在夫妻节点中的位置,我们可以通过 $parent.childNodes[index] 拿到这个引用,inex 是节点在父亲节点中的位置。
假设 index 会传递到我们的方法中。代码如下:
节点更新
首先我们创建一个方法用于比较2个节点(新的和旧的),告诉我们哪个节点有变化。我们需要同时考虑元素节点和文本节点。
有了 index,我们很容易用新节点替换它:
比对children
最好,我们需要遍历节点的子节点并且进行比对。我们把这个过程叫 *updateElement(…) *。就是个递归。
在写代码之前我们需要考虑到:
undefined
,我们的方法也可以处理。children
数组中的索引。合在一起
结论
恭喜,我们做到了。我们实现了虚拟DOM。通过这个文章,我们读者能理解虚拟DOM的基本原则,以及
React
底层是怎么工作的。然而有一些这里没有提到的:
React
jQuery
和它的插件The text was updated successfully, but these errors were encountered: