React’s diff algorithm

来源:互联网 发布:阿里云报价 编辑:程序博客网 时间:2024/06/04 01:19

React’s diff algorithm(React中的Diff算法)

原文地址
原文地址

React是Facebook开发出来的一个JavaScript库用来创建UI接口。从一开始设计的时候,React就考虑到了性能问题。这篇文章我将演示一下diff算法怎么工作以及React的rendering以便你可以优化你自己的app。

Diff Algorithm

在我们进入到实现细节之前,首先对React是怎么工作的有一个大致的了解。

var MyComponent=React.createClass({    render:function(){        if(this.props.first){            return(                <div className="first">                    <span>A span</span>                </div>            );        }else{            return(                <div className="second">                    <p>A Paragraph</p>                </div>            );        }});

在任何时间点,你都在描述你的UI看起来是怎么样的。理解这一点很重要,render方法的结果其实并不是真正的DOM节点。它们只是轻量级的JavaScript对象,我们把它们成为虚拟DOM。
React会根据render方法返回的这个描述(虚拟DOM),寻找最少的步数,从之前的render到这新个返回的render。比如说我们加载<MyComponent first={true} />,再用<MyComponent first={false} />,然后卸载它,DOM操作的结果会是这样的:

none到first

  • 创建节点<div className="first"> <span>A span</span></div>

first到second

  • 替换属性:className="first"替换为"className="second"
  • 替换节点:<span>A Span</span>替换为<p>A Paragraph</p>

Second 到 none

  • 移除节点:<div className="second"><p>A Paragraph</p></div>

层对层

找到两颗任意树的最小转换步数是一个O(n^3)的问题。正如你所想,这可解决不了我们的性能问题。React使用了很简单但是极具启发性方法使其近似O(n)。
React只会reconcile对应层之间的树节点(只会对比图中颜色相同的部分)。这种对比大幅度减少了复杂度,而且并不是一个失败之举,因为在web应用中很少把一个组件节点移动到这颗树的其他层次(结构稳定)。它们通常只是在孩子节点的同层之间移动。

图片

List

比如说有一个组件,只有一代孩子节点,render出五个孩子组件,然后插入一个新的到这个孩子列表中间。
insert
如果没有其他的信息,React无法识别每一个节点,更新就会很低效。 即A更新为A,B更新为B,C更新为F,D更新为C,E更新为D,即按顺序更新。
without key
但是如果给每个节点加上一个唯一的key值,那么在更新列表时,React就可以判断新来的节点是不是已经存在于现有的DOM子树中了。就可以很快地插入节点:
with key

with or without key

Components

一个React的app通常是由很多开发者定义的Component组合而成,这些Components最终会组成一棵树,主要是divs。在diff算法中,React只会匹配那些有相同类型的Components。
比如说如果一个<Header>被一个<ExampleBlock>所替换,那么会直接移除<Header>并创建一个<ExampleBlock>元素添加到DOM上。我们不会花费时间去对比这两个元素有多么像。即节点类型不同,直接走删除创建添加这个流程,被删除的节点下面的子树也一并被删除,而创建的节点如果下面有子树,也会一并创建,即创建整个以新节点为根的子树。
但是如果节点类型相同,那么我们就会进行下一步的比较。比较这个节点的属性,然后进入下一层迭代这个以上过程。

总结一下

diff算法主要是React中的render返回新的DOM描述和已存在的DOM进行对比,寻找差异的过程。这个过程是DOM更新的基础,React根据这个过程的结果进行reconcile。

  • 首先,对比是层与层之间的对比,因为web应用中很少会把节点在不用层次之间移动。
  • 对比节点时,首先对比类型,如果类型不同则直接发生删除创建添加等流程
  • 如果节点类型相同,则对比节点属性,不同则会更新
  • 然后会进入下一层迭代以上过程
  • 列表节点会添加唯一的key标识,以期简化更新的过程

Event Delegation

给DOM节点添加事件代理又慢又浪费内存。React使用了一种叫做“事件代理”的流行方法。React甚至做的更多,它重新实现了一个兼容W3C标准的事件系统。这意味中IE8中的事件处理bug不会发生,而且事件的名字跨浏览器一致。
让我解释一下它是如何实现的。在DOM的root上布置了一个单独的事件监听器。当某个事件发生的时候,浏览器会给我们事件的目标节点。React并没有使用遍历虚拟DOM层级结构的方式来在DOM层级中传播这个事件。(如果没有事件代理,那么就以为着要一个个的更新节点以期使得事件在DOM树中传播)我们利用了React中的组件层级系统中的每个组件都有一个唯一的id这个事实,我们可以使用简单的字符串操作就可以得到所有父组件的id。把事件监听器储存在一个哈希表里,我们发现这样比把它们附在虚拟DOM节点上性能更好。这里有个例子说明了当一个事件被触发时虚拟DOM上发生了什么。

//dispatchEvent('click','a.b.c',event)clickCaptureListener['a'](event);clickCaptureListeners['a.b'](event);clickCaptureListeners['a.b.c'](event);clickBubbleListeners['a.b.c'](event);clickBubbleListeners['a.b'](event);clickBubbleListeners['a'](event);  

浏览器会为每一个事件和监听器创建一个对象,这是个很好的方法可以让你保持对事件的引用,甚至修改它。然而这也需要分配大量的内存。然后,React一开始就为这些对象分配了一个内存池,无论什么时候需要一个事件对象,都会从这个池子里复用一个对象,这大大地减少了垃圾收集。
(这部分建议看原文)

Rendering

Batching

无论什么时候你在一个组件上调用setState,React就会把它标记为dirty。在循环事件的末尾,React会寻找所有的dirty组件然后重新渲染它们。
batching意味着在一个循环事件中,恰好有一个时间DOM本更新。这是构建一个高性能的app的关键而且很难用JavaScript实现,而在React中这是默认的。
batching

Sub-tree Rendering

setState在一个组件上被调用是,这个组件会为它的孩子节点重新建议虚拟DOM。如果你在根节点上调用setState那么整个React应用都会被re-render,这样,所有的组件哪怕是那些没有改变的,都会调用它们的render方法。这也许听起来很可怕,没有效率,但是事实上不会,因为我们并没有直接操作真正DOM,render返回的结果只是虚拟节点。
首先,我们讨论的是显示UI接口,因为屏幕尺寸的限制,你通常一次只会显示几百到几千的组件。JavaScript拥有足够快的业务逻辑处理速度来管理整个应用的接口;其次很重要的一点就是你很少会在整个应用的顶点调用setState,一般你会在改变的那个组件或者之上两三层的组件上调用,也就是说改变只会在用户交互的那个部分区域发生。

Selective Sub-tree Rendering

最后,你可以选择更新子树与否。只要你在一个组件里实现下面这个函数:

boolean shouldComponentUpdate(object nextProps,object nextState)
根据传入的下一个props/state和当前的props和state对比,判断是否改变以及是否需要更新。如果实现得好,这个函数可以给你的应用巨大的性能提升。
为了能够使用它,你要能够比较JavaScript对象。这会引发很多的问题比如浅比较还是深比较,如果是深比较的话是使用不可变数据结构还是深复制。
要记住,这个函数会一直被不停地调用,所以,你要保证实现它的话要比默认的render机制花费的时间要少,即便re-render不会发生也成立。

Conclusion

使得React快的方法并不是新发现的。我们很早就知道了DOM操作花费更多,应该批量读写操作,甚至事件委托更快等等。
在实践中,人们仍然在讨论这些,因为把很难它们应用到原始的JavaScript中。真正让React出色的是那些默认的性能机制。使用React,你很难把自己陷入困境,让自己的app变得缓慢。
React的性能花费模型也很容易理解:每次调用setState都会re-render整个子树。如果你想提升性能,尽可能少的调用setStae,使用shouldComponentUpdate来阻止大的子树的更新或者不必要的更新。

0 0
原创粉丝点击