No Silver Bullet.
"烂记性不如好笔头."
Toggle navigation
No Silver Bullet.
主页
前端开发基础
Vue.js
质量
Python/Linux
其他基础相关
CSS
归档
标签
2018-09-05 [T]Medium:如何写一个你自己的虚拟DOM
无
2018-09-05 09:29:02
203
0
0
magefox
链接至:[How To Write Your Virtual DOM](https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060) 想要构建自己的虚拟DOM系统,你只需要明白两件事.你甚至不需要去追溯React的源码或者其他虚拟DOM的实践-它们往往庞大而复杂.但是实际上的虚拟DOM主要逻辑可以用少于50行的代码实现.只要50行! 这里有两个概念: > 1.虚拟DOM是真实DOM以任意形式的一种表现. > 2.当我们改变了虚拟DOM树时,我们会得到一个新的虚拟DOM树.算法会比较这两个树(旧的和新的),寻找它们之间的区别 ,并且只会做出让真实DOM变动最小的操作来改变反映的虚拟DOM树. 就是这样!让我们深入探讨这些概念。 #1.表现我们的DOM树 首先,我们需要在内存中存储一下我们的DOM树,我们能用简单的老JavaScriptObject来完成它.假设我们有这样一个树: <ul class=”list”> <li>item 1</li> <li>item 2</li> </ul> 看起来是不是很简单?我们怎样才能用JavaScript的Object来表示它? { type: ‘ul’, props: { ‘class’: ‘list’ }, children: [ { type: ‘li’, props: {}, children: [‘item 1’] }, { type: ‘li’, props: {}, children: [‘item 2’] } ] } 这里你需要注意两个地方: 1.我们使用类如Object的东西来表示DOM. { type: ‘…’, props: { … }, children: [ … ] } 但是写一个大的树型结构是非常困难的.所以让我们来写一个帮助函数,它能够帮助我们更轻易的理解这个结构. function h(type, props, …children) { return { type, props, children }; } 现在我们能够以这种方式来写DOM tree了: h(‘ul’, { ‘class’: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’), ); 这样看起来清楚多了,是吗?但是我们能够更进一步.我们都听说过JSX,是吗?在这儿我想讨论一下它.它是如何工作的? 如果你曾经阅读过 Babel的JSX文档 [这里](https://babeljs.io/docs/en/babel-plugin-transform-react-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编译的东西.我们只需要在我们的源代码文件头部添加一些注释: (//smth = something else) /** @jsx h */ <ul className=”list”> <li>item 1</li> <li>item 2</li> </ul> 那么,现在其实已经可以告诉Babel:"(。・∀・)ノ゙嗨,把这个React.createElement用h'替换掉吧."你可以用任何东西来取代这里的这个h'.它们都将被转译. 所以,总结一下我上面说过的,我们能够用这种方式来写一个我们自己的DOM: /** @jsx h */ const a = ( <ul className=”list”> <li>item 1</li> <li>item 2</li> </ul> ); 然后它们将被Babael转译成这样的代码: const a = ( h(‘ul’, { className: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’), ); ); 当函数`h`执行时,它将会清楚的返回JavaScript对象-以我们的虚拟DOM表示: const a = ( { type: ‘ul’, props: { className: ‘list’ }, children: [ { type: ‘li’, props: {}, children: [‘item 1’] }, { type: ‘li’, props: {}, children: [‘item 2’] } ] } ); 在JSFiddle上试试(不要忘记设置你语言的Babel) [点我](https://jsfiddle.net/deathmood/5qyLubt4/?utm_source=website&utm_medium=embed&utm_campaign=5qyLubt4) #2 应用我们的DOM表示 好的,现在我们已经拥有了我们用自己定义结构的JavaScript Object所表达出来的DOM树.这很Cool,但是现在我们需要根据它们来创建一些真实的DOM,因为我们没法将它们加入DOM. 首先我们做一些假设来建立一些术语: 1. 我将会把所有真实DOM节点(元素,文本节点)以`$`为开头书写----所以**$parent** 将会是一个真实的DOM元素 2. 虚拟DOM表示将会是可变的,并以**node**命名. 3. 和React类似,你只能有一个**root node** 其他的节点将会被它包括在里面. OK,现在让我们来写`createElement(…)` 这个函数.这个函数将会接受一个虚拟DOM然后返回一个真实的DOM节点.现在先忘记`props`和`children`,我们稍后会建立它们. function createElement(node) { if (typeof node === ‘string’) { return document.createTextNode(node); } return document.createElement(node.type); } 所以,因为我们能够有text node -它们是清晰的JS字符串和元素-它们的JS Object类型类似于这样: { type: ‘…’, props: { … }, children: [ … ] } 因此,我们能够清晰的了解到virtual text nodes 和 virtual element nodes,它们将会生效. 现在,我们开始考虑children.它们也是text node,同时也是一个元素.所以,它们也能被`createElement(...)`所创建.是的,你感觉到了吗?这是个递归:)所以我们能够将`createElement(…)`用在每个元素的子元素上,然后使用`appendChild()`来将它们append进我们的元素里. function createElement(node) { if (typeof node === ‘string’) { return document.createTextNode(node); } const $el = document.createElement(node.type); node.children .map(create∂Element) .forEach($el.appendChild.bind($el)); return $el; } Wow,这看起来很棒.我们先把`props`放在一边,我们将会稍后谈论它.我们不需要它们来理解基础的虚拟DOM概念,它们的加入会增加更多的复杂性. 现在,再让我们继续在JSFiddle上试试. [点我](https://jsfiddle.net/deathmood/cL0Lc7au/?utm_source=website&utm_medium=embed&utm_campaign=cL0Lc7au) #3.处理改变 OK,现在我们能够把我们的虚拟DOM变成真实的DOM,是时候思考如何处理虚拟DOM的改变了.首先最基本的,我们需要写一个算法来比较两个不同的虚拟DOM树-老的和新的-然后只让真实DOM做出一些必要的改变. 怎么对两个树进行diff?我们需要掌握如下几个要点: 1. 某个位置出现了新的节点-因此节点是新增的,我们需要`appendChild(…)` 它.  2. 不再有新的节点出现在某个位置-因此这个节点被删除了,我们需要`removeChild(…)`它.  3. 当有不同的节点在相同的位置上-这个节点改变了,我们需要replaceChild(…)它.  4. 节点都是相同的-我们需要去更深的层次观察子节点的区别.  Ok, 让我们来写一个这样的updateElement(...)函数,它接收三个参数($parent,newNode和oldNode),而$parent是我们虚拟DOM节点的真实元素父节点.现在我们可以看看如何处理所有如上所述的情况. ##这里没有老的节点 好吧,直截了当的说,我甚至不会作任何解释. function updateElement($parent, newNode, oldNode) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } } ##这里没有新的节点 现在我们得到了一个问题.如果在新的虚拟DOM树中某个位置没有节点-我们需要在真实的DOM中移除它.那么我们应该如何做到呢? 是的,我们知道父节点(它被传递给了一个函数)然后我们应该调用方法`$parent.removeChild(…)`然后将真实的DOM参考传递到这里.但是我们并没有获得它.如果我们能够知道我们这个节点在父元素上的位置,我们就能使用$parent.childNodes[index]来获取用index在父元素上定位的节点的位置. 所以,让我们假设这个index将会被传递给我们的函数(实际上它也将会被传递-你将会在稍后看到它)所以我们的代码将会变成: 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 } 然后现在,当前节点中在parent里有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] ); } } ##Diff子节点 最后但同样重要的-我们应该比较每个节点的所有儿子,并在调用每个节点的时候调用一次`updateElement(…)` ,然后依次递归. 但是这里有一些东西我们需要在写代码之前考虑的: 1. 我们应该仅仅在子节点是元素的时候比较(文本节点是不会有子节点的) 2. 现在我们以当前节点为参考的父节点. 3. 我们应该将所有子节点一个一个的全部比较一遍,即使指向的节点是'undefined'-这也是OK的,我们的方法能够掌控它. 4. 最后的**index**-它只是在`children`数组中的child索引. 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 ); } } } #将它们整合起来 是的,这就是我们要做的事情了.我们终于到了这一步.我已经把所有代码放在了JSFiddle上,而它正常的完成了它的任务,真的仅仅使用了50行-就像我给你的承诺一样.现在让我们来实现一下它. [JSFiddle](https://jsfiddle.net/deathmood/0htedLra/?utm_source=website&utm_medium=embed&utm_campaign=0htedLra) 打开开发者工具点击`Reload`按钮然后看看它会发生什么改变. #结论 恭喜!我们完成了它.我们写出了一个我们自己的虚拟DOM实现,并且它正常的运行了.我希望在看完这篇文章之后,你能够明白虚拟DOM工作的基本概念和React怎么在它的引擎下工作. 然而这里有些地方我们并没有着重的介绍它们(我将会尝试在以后的文章中覆盖它们) 1. 设置元素属性(props)并diff/updating它们. 2. 处理事件-在我们的元素上添加事件监听器. 3. 将我们的虚拟DOM系统以组件形式运行,就像React那样. 4. 获取真实DOM的引用. 5. 使用虚拟DOM操作库来改变真实DOM-就像Jquery和它们的插件一样. 6. ...More
上一篇:
2018-09-16 6Pro Tips From React Developers
下一篇:
学习方法的更新与感悟
0
赞
203 人读过
新浪微博
微信
腾讯微博
QQ空间
人人网
Please enable JavaScript to view the
comments powered by Disqus.
comments powered by
Disqus
文档导航