在前端领域中,接触到Diff算法基本上是基于如今的前端框架,如React、Vue等,目前的框架基本上都用到了Diff和V-dom,而Diff算法做为V-dom的加速器,在提升性能方面有极其重要的做用,在每次的update都能高效的渲染新的UI界面,使得你们不用在关心渲染方面上的问题,只须要专一于界面和业务需求;html
前端框架上的Diff算法和传统的Diff有一点区别,由于框架上有针对使用场景的策略和处理,如下只经过对比和理解传统Diff算法来去理解如今React的Diff在React中的处理;前端
传统Diff算法本质是计算一棵树形结构转换成另外一棵树形结构的最少操做,React自己是借助Virtual DOM+Diff使得它与传统的Dom操做有了极大的突破,但React的是在传统上的处理,因此咱们如今先看看传统的Diff算法;react
传统的Diff算法,操做包括替换、插入、删除,在对比时的复杂度为o(n**3)算法
o(n**3)的由来数组
编辑距离(Edit-Distance)前端框架
计算 字符串neigevor与Neigevoir的编辑距离(3)
n -> N Neigevor
r -> i Neigevoi
add r Neigevoirapp
同理对于dom这类的对象来讲也是同样,由替换、插入、删除,但相对来讲比只只有Edit-Distance复杂;框架
例如:dom
before after A A / \ / \ B D => B D / \ C E \ C
B delete C
D add E
D add C 组件化
直观感觉虽然只是作了3步,可是对于计算机来讲须要对两个树进行遍历;
可能有人有点好奇遍历的方式,我我的是这么理解的,先找到两个树的差别再进行转换操做;
一、为什么非要遍历两个树,不能只以before来遍历,不就o(n)?
若是直接before来遍历,遇到B删除C,D的时候增长E-C,看起来的确是,但after的可能性是无限的,若是变成:
before after A E / \ / \ B D => A F / / \ C B D / C
再以before来遍历,这样遍历的时候直接就变成直接生成整个after结果,这样不违背了Diff的原则(最少操做);
二、如下图为例,以直观来看只要生成一个E,而后将before放进去,而后再加一个G和F便可:
即使如此也是须要两个树嵌套遍历进行查询
before after A E / \ / \ B D => A F / / \ C B D / C / G
以A为例,是否A须要遍历完整整个after
1.在after中遇到A,break去执行B的遍历,可能出现遍历到C的时候,C-G这样的状况,break就没法清楚G节点;
2.只判断父子节点,难保F一边可能也出现before;
因此当须要进行获取不一样的节点时就须要o(n**2)的时间复杂度;
三、在获取到了差别后进行操做,多少个差别处理多少次难道不是o(n2)+o(n)或者在边找的时候边处理直接o(n2)?
第一反应是这个感受,但后来想一想是和Edit Distance相关;(我的理解)
仍是以A为例:
一、在遍历E的时候,由于A的父节点为Null,E的也是Null,而且A!==E因此直接生成; 二、遍历到A的时候,存在Edit的问题,须要清楚是删除或是插入等操做; 三、因此须要遍历A的下一层子节点,发现都是B-D,因此A不用操做; 四、类推,当before的C在after中遍历到C的时候,一样须要看子节点,因此插入一个G;
因此是O(n**3);
简单点的理解就是时找到差别o(n2),而后寻找差别进行edit的是o(n),因此是o(n3);
若是有100个元素,在一次改变后就须要1000000的计算量,这对于前端来讲确定爆炸, 但100个元素在前端来讲也不是那么罕见,例如列表等;因此直接用传统的Diff来去处理意义不是特别大, 可能还不必定有直接从新渲染来的直接,须要在传统的Diff上进行改造;
在不看React官方的优化前,其实咱们也在了解Diff的过程当中也遇到和理解的优化点;
1.在遍历查询差别节点时,只用一个做为基础O(n),上文说了会致使所有渲染,但实际这样的状况极少数发生; A、由于Dom的处理不多跨层级去移动,大部分都是appendChild、removeChild或者改变Child的内容; B、组件化,使得Dom更容易同级比对,不用考虑跨级; 2.便是减小一层遍历,也只是从o(n3)变成o(n2),因此还须要在操做节点的方面上处理,不用在遍历查找差别; A、由于同级比对,因此不进行对最优操做进行遍历,只须要考虑是否是该节点有改变; B、优化遍历过程当中,部分并未进行改变的节点,能够不用再向下遍历;
目前来看已经有了很多改进,可是当父节点不变,子节点位置改变时,按上面说的会直接删除生成,也并非好的处理方式;
before after A A / \ / \ B C => C B
最终其实须要作成的就是直接移动位置便可,下面有说到解决方法;
因此咱们再来看看,React官方的假设基础:
一、两个不一样类型的元素会产生出不一样的树; 二、开发者能够经过 key prop 来暗示哪些子元素在不一样的渲染下能保持稳定;
最终React的策略以下:
同层级对比(Tree Diff)
只对比先后树同层级的差别,直接新增或者删除; before after A D / \ / B C => A / \ B C after里面的元素所有都从新生成;
比对不一样类型的元素(Component Diff)
若是节点为不一样类型的元素时,直接进行删除而后从新建立新的元素,进而它的子节点即便和原先的子节点是同样也须要从新建立; before after <ConponentA /> <ConponentB /> ConponentA与ConponentB的元素皆为<div>test</div>,也是须要从新生成;
比对同一类型的元素(Element Diff)
before after <ConponentA /> <ConponentA /> 在先后元素类型相同后,进行ElementDiff的对比; 一、当节点为相同类型的元素时,若是只是改变属性子节点没有任何变化时,仅比对及更新有改变的属性; <div className="before" title="stuff" /> ===> <div className="after" title="stuff" /> 因此只须要修改 DOM 元素上的 className 属性 二、当节点为相同类型的元素时,子节点有变化,如位置、数目等,则会进行插入、删除、移动等操做;; before after <div> <div> <span>1</span> <span>1</span> <span>2</span> <span>2</span> </div> <span>3</span> </div> 在遍历完1和2的对比以后插入一个3便可;
key的重要性
key能够理解为身份标志,因此必需要保持惟一和稳定;
一、防止错误的操做,减小性能消耗; before after <div> <div> <span key="1">1</span> <span key="3">3</span> <span key="2">2</span> <span key="1">1</span> </div> <span key="2">2</span> </div> 经过key能够发现1和2节点都是相同的节点,只须要移动插入3而后移动一、2便可; 避免以前文中说的的问题,删除从新而不是移动; 二、减小Diff时间 React是不会给组件增长Key,可是增长key属性的节点,组件实例会基于它们的key来决定是否更新以及复用, 在key相等的时候会认为同一元素,不须要进行过多的diff,只须要判断属性是否变化,而key不一样则会销毁从新生成;
key的注意点
//父组件 state = [{id: 1}] <div> <button onClick={setState([{id:2}, {id: 1})]} >在列表头插入元素</button> { state.map((value,inedx) => (<Item id={value.id} key={index} />)) } </div> //Item组件 <div> <span>{props.id}</span> <input type="text" /> </div>
当用户在id:1的input输入value(test)后,再去点击按钮插入元素时,会出如今id:1的input输入值text出如今了id:2的input中
before after <Item id="1" key="0" /> <Item id="2" key="0" /> <Item id="1" key="1" /> 缘由值由于key,key相等致使id:1和id:2当成相等,因此进行属性判断, 因此before的Item只修改了props.id,而不是在前面插入一个Item,只是1变成2,再加一个1; **若是key是数组下标,那么修改顺序时会修改当前的key,致使非受控组件的state(好比输入框) 可能相互篡改致使没法预期的变更。因此在一些数组遍历的时候不建议用index当key使用**
简单点总结就是简单(Dom,数据)、惟一(key,类型)、稳定(操做dom,列表元素);