通过写一个简单的虚拟DOM性学习虚拟DOM的原理,本文采用意译而非直译。要构建您自己的虚拟DOM,您需要了解两件事。你甚至不需要深入研究React或任何其他虚拟DOM实现的源代码,因为它们是如此庞大和复杂——但事实上,虚拟DOM的主要部分只需要不到50行代码。有两个概念:虚拟DOM是真实DOM的映射当虚拟DOM树中的某些节点发生变化时,会得到一个新的虚拟树。该算法会比较两棵树(新的和旧的),找到不同之处,然后只需要对真实的DOM进行相应的更改。用JS对象模拟DOM树首先,我们需要以某种方式将DOM树存储在内存中。你可以用普通的JS对象来做。假设我们有这样一棵树:item1item2看起来很简单吧?怎么用JS对象来表示呢?{type:'ul',props:{'class':'list'},children:[{type:'li',props:{},children:['item1']},{type:'li',props:{},children:['item2']}]}这里有两点需要注意:DOM元素由这样的对象表示{type:'…',props:{…},children:[…]}用普通的JS字符串表示DOM文本节点,但是用这种方式表示一个内容很多的Dom树是相当困难的。这里有一个帮助函数,让它更容易理解:functionh(type,props,…children){return{type,props,children};}使用这个方法重新排列初始代码:h('ul',{'class':'list'},h('li',{},'item1'),h('li',{},'item2'),;这样看起来简洁多了,也可以更进一步。这里使用了JSX,如下:item1item2编译成:React.createElement('ul',{className:'list'},React.createElement('li',{},'item1'),React.createElement('li',{},'item2'),;这看起来很熟悉吗?如果我们可以用我们刚刚定义的h(...)函数替换React.createElement(...),那么我们也可以使用JSX语法。其实只需要在源文件的头部加上这样一条注释即可:/**@jsxh*/item1item2它实际上告诉Babel'嘿老兄,为我编译JSX语法,用h(...)函数替换React.createElement(...),然后Babel将开始编译。'综上所述,我们这样写DOM:/**@jsxh*/consta=(item1item2);Babel会帮我们编译这段代码:consta=(h('ul',{className:'list'},h('li',{},'item1'),h('li',{},'项目2'),););当函数“h”执行时,它将返回纯JS对象——即我们的虚拟DOM:consta=({type:'ul',props:{className:'list'},children:[{type:'li',props:{},children:['item1']},{type:'li',props:{},children:['item2']}]});从虚拟DOM映射到真实DOM现在我们有了一个DOM树,由普通JS对象和我们自己的结构表示。这很酷,但我们需要从中创建一个真正的DOM。首先让我们做一些假设并声明一些术语:使用以'$'开头的变量来表示真实的DOM节点(元素,文本节点),因此$parent将是一个真实的DOM元素虚拟DOM使用一个名为node的变量表示* 就像在 React 中一样,只能有一个根节点——所有其他节点都在其中所以,让我们编写一个函数createElement(…),它将获得一个虚拟DOM节点并返回一个真实的DOM节点。这里不考虑props和children的属性:functioncreateElement(node){if(typeofnode==='string'){returndocument.createTextNode(node);}returndocument.createElement(node.type);}上面的方法我还可以创建两种类型的节点:文本节点和Dom元素节点,它们是JS对象类型:{type:'…',props:{…},children:[…]}因此,您可以传入函数createElement输入虚拟文本节点和虚拟元素节点-这可行。现在让我们考虑子节点——每个子节点都是一个文本节点或元素。所以它们也可以用createElement(…)函数创建。是的,这就像递归,所以我们可以为每个元素的子元素调用createElement(...),然后使用appendChild()添加到我们的元素:functioncreateElement(node){if(typeofnode==='string'){返回document.createTextNode(node);}const$el=document.createElement(node.type);node.children.map(createElement).forEach($el.appendChild.bind($el));return$el;}哇,看起来不错。让我们先把nodeprops属性放在一边。稍后会详细介绍。我们不需要他们理解虚拟DOM的基本概念,因为他们增加了复杂性。完整代码如下:/**@jsxh*/functionh(type,props,...children){return{type,props,children};}functioncreateElement(node){if(typeofnode==='string'){returndocument.createTextNode(node);}const$el=document.createElement(node.type);node.children.map(createElement).forEach($el.appendChild.bind($el));return$el;}consta=(item1item2);const$root=document.getElementById('根');$root.appendChild(createElement(a));比较两棵虚拟DOM树的区别现在我们可以将虚拟DOM转换成真实的DOM,这需要考虑两棵DOM树的区别。基本上,我们需要一种算法来比较新树和旧树,这会让我们知道发生了什么变化,然后相应地改变真实的DOM。如何比较DOM树?以下情况需要处理:添加新节点,使用appendChild(...)方法添加节点移除旧节点,使用removeChild(...)方法移除旧节点节点替换,使用replaceChild(...)方法如果节点相同——需要写一个叫updateElement(…)的函数来深度比较子节点,它接受三个参数——$parent、newNode和oldNode,其中$parent是一个元素的父元素虚拟节点的实际DOM元素。现在让我们看看如何处理上述所有情况。添加一个新节点functionupdateElement($parent,newNode,oldNode){if(!oldNode){$parent.appendChild(createElement(newNode));}}Removetheoldnode这里有个问题——如果在新的虚拟树中当前位置没有节点——我们应该将它从实际DOM中移除——怎么办呢?如果我们知道父元素(通过参数传递),我们可以调用$parent.removeChild(…)方法将更改映射到真实的DOM。但是前提是我们得知道我们节点在父元素上的索引,我们可以通过$parent.childNodes[index]获取节点的引用。好的,我们假设这个索引将被传递给updateElement函数(它会被传递——见后面)。代码如下:}elseif(!newNode){$parent.removeChild($parent.childNodes[index]);}}节点替换首先,您需要编写一个函数来比较两个节点(旧节点和新节点)并判断该节点是否真的发生了变化。还需要考虑这个节点可以是一个元素或者一个文本节点:functionchanged(node1,node2){returntypeofnode1!==typeofnode2||typeofnode1==='string'&&node1!==node2||节点1。type!==node2.type}现在当前节点有一个index属性,它可以很容易地用一个新节点替换:functionupdateElement($parent,newNode,oldNode,index=0){if(!oldNode){$parent.appendChild(createElement(newNode));}elseif(!newNode){$parent.removeChild($parent.childNodes[index]);}elseif(changed(newNode,oldNode)){$parent.replaceChild(createElement(newNode),$parent.childNodes[index]);最后但并非最不重要的是比较子节点——我们应该遍历这两个节点的每个子节点并比较它们——实际上每个节点都调用updateElement(...)方法,这也需要递归。我们只需要比较当节点是DOM元素时(文本节点没有子节点)我们需要将当前节点的引用作为父节点传递并且我们应该将所有子节点一一比较,即使是undefined没关系,我们的函数也会正确处理它。最后是index,它是子数组中子节点的indexfunctionupdateElement($parent,newNode,oldNode,index=0){if(!oldNode){$parent.appendChild(createElement(newNode));}elseif(!newNode){$parent.removeChild($parent.childNodes[index]);}elseif(changed(newNode,oldNode)){$parent.replaceChild(createElement(newNode),$parent.childNodes[index]);}elseif(newNode.type){constnewLength=newNode.children.length;constoldLength=oldNode.children.length;for(leti=0;i项目1项目2);constb=();const$root=document.getElementById('root');const$reload=document.getElementById('reload');updateElement($root,a);$reload.addEventListener('click',()=>{updateElement($root,b,a);});HTML重新加载
CSS#root{border:1pxsolidblack;填充:10px;margin:30px000;}打开开发人员工具并观察按下“重新加载”按钮时应用的更改总结现在我们已经编写了虚拟DOM实现并了解它是如何工作的。作者希望阅读本文后,您对虚拟DOM的工作原理及其幕后响应方式等基本概念有所了解。然而,有一些事情没有在这里强调(将在以后的帖子中介绍):设置元素属性(props)和比较/更新处理事件-向元素添加事件监听器以使虚拟DOM与Reactgets等组件一起工作对实际DOM节点的引用使用带有可以直接改变真实DOM的库的虚拟DOM,例如jQuery及其插件原文:https://medium.com/@deathmood...代码部署后可能存在BUG不能实时知道。事后为了解决这些BUG,花了不少时间在日志调试上。顺便推荐一个好用的BUG监控工具Fundebug。交流文章每周更新。可以微信搜索“大千世界”阅读即时更新(比博文早一两篇)。本文已收录到GitHubhttps://github.com/qq449245884/xiaozhi。我的文档写了很多,欢迎Star和完善,面试可以参考考点复习,还有关注公众号,后台会回复福利,看福利就知道了.