前言上一篇我们实现了React的组件功能,从功能的角度实现了React的核心功能。但是我们的实现有一个很大的问题:整个应用或者整个组件每次更新都要重新渲染,DOM操作非常昂贵,性能损失非常大。为了减少DOM更新,我们需要找到渲染前后真正变化的部分,只更新这部分DOM。比较变化并找出需要更新的部分的算法称为diff算法。对比攻略经过前两篇文章,我们实现了一个render方法,可以将虚拟DOM渲染成真实DOM。我们现在需要对其进行改进,使其不再傻傻地重新渲染整个DOM树,而是找出真正发生变化的部分。这部分的很多React框架都有不同的实现方式。有些框架会选择保存上次渲染的虚拟DOM,然后对比虚拟DOM前后的变化,得到一系列更新的数据,再将这些更新应用到真实的DOM上。在DOM上。但是也有一些框架选择直接比较虚拟DOM和真实DOM,这样就不需要额外保存最后渲染的虚拟DOM,可以边比较边更新,这也是我们选择的方式。不管是DOM还是虚拟DOM,它们的结构都是一棵树。完全比较两棵树的变化的算法的时间复杂度是O(n^3),但是考虑到我们很少跨层移动DOM,所以我们只需要比较同一层的变化。只需要比较相同颜色框内的节点总而言之,我们的diff算法有两个原则:比较当前的真实DOM和虚拟DOM,比较过程中直接更新真实DOM,只比较同一层次的变化.为此,我们需要实现一个diff方法,它的作用是比较真实DOM和虚拟DOM,最后返回更新后的DOM/***@param{HTMLElement}domrealDOM*@param{vnode}vnodevirtualDOM*@returns{HTMLElement}updatedDOM*/functiondiff(dom,vnode){//...}下一步是实现这个方法。在此之前,让我们回顾一下我们的虚拟DOM的结构:虚拟DOM的结构可以分为三种类型,分别代表文本、原生DOM节点和组件。//原生DOM节点的vnode{tag:'div',attrs:{className:'container'},children:[]}//文本节点的vnode"hello,world"//组件的vnode{tag:ComponentConstructr,attrs:{className:'container'},children:[]}比较文本节点时,首先考虑最简单的文本节点。如果当前DOM是文本节点,则直接更新内容;否则,创建一个新的文本节点并将其删除原始DOM。//difftextnodeif(typeofvnode==='string'){//如果当前DOM是文本节点,直接更新内容if(dom&&dom.nodeType===3){//nodeType:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeTypeif(dom.textContent!==vnode){dom.textContent=vnode;}//如果DOM不是文本节点,则创建一个新的文本节点DOM,并删除原来的}else{out=document.createTextNode(vnode);if(dom&&dom.parentNode){dom.parentNode.replaceChild(out,dom);}}returnout;}文本节点很简单,没有属性,也没有子元素,所以这一步就可以直接返回结果了。与非文本DOM节点相比,如果vnode表示一个非文本DOM节点,有以下几种情况:如果真实DOM和虚拟DOM的类型不同,比如当前真实DOM是一个div,而值为vnode的tag如果是'button',那么原来的div就没用了。直接新建一个button元素,并将div的所有子节点移动到button上,然后使用replaceChild方法将div替换成button。if(!dom||dom.nodeName.toLowerCase()!==vnode.tag.toLowerCase()){out=document.createElement(vnode.tag);如果(dom){[...dom.childNodes]。映射(out.appendChild);//将原来的子节点移动到新节点if(dom.parentNode){dom.parentNode.replaceChild(out,dom);//移除原来的DOM对象}}}if真实DOM和虚拟DOM是同一类型的,所以我们暂时不需要做任何其他事情,只需要等待稍后比较属性和子节点.比较属性diff算法其实不仅要找出节点类型的变化,还要找出节点属性和事件监听的变化。我们单独取出比较属性作为方法:functiondiffAttributes(dom,vnode){constold=dom.attributes;//当前DOM属性constattrs=vnode.attrs;//virtualDOMattributes//如果原来的Ifattribute不在newattribute中,则移除它(属性值设置为undefined)for(letnameinold){if(!(nameinattrs)){setAttribute(dom,名称,未定义);}}//更新新的属性值for(letnameinattrs){if(old[name]!==attrs[name]){setAttribute(dom,name,attrs[name]);一篇文章比较子节点节点本身的比较就完成了,接下来就是比较它的子节点了。这里会有问题。我们之前实现的不同的diff方法都知道比较哪个真实DOM和虚拟DOM,但是子节点是一个数组,它们可能改变了顺序或者编号。我们很难确定将哪一个与虚拟DOM进行比较。为了简化逻辑,我们可以让用户提供一些线索:为节点设置一个key值,重新渲染时比较具有相同key值的节点。//diff方法if(vnode.children&&vnode.children.length>0||(out.childNodes&&out.childNodes.length>0)){diffChildren(out,vnode.children);}functiondiffChildren(dom,vchildren){constdomChildren=dom.childNodes;const孩子=[];常量键控={};//将有关键的节点和没有关键的节点分开if(domChildren.length>0){for(leti=0;icount:{this.state.num}
