React的diff算法(译文)

前言 

     此篇文章主要是因为在看Virtual DOM(虚拟DOM)的时候看到的主要讲的是实现Virtual

Dom 的diff算法,原文地址:https://calendar.perfplanet.com/2013/diff/

译文

React是一个Facebok开发的用于构建用户界面的JavaScript库。它的设计从根本上考虑到性能。在这篇文章中,作者将演示diff算法和渲染在React如何工作,以便您可以优化您自己的应用程序。

Diff 算法

在我们了解实现细节之前,了解React的工作原理非常重要。

1 varMyComponent=React.createClass({
2 render:function(){
3 if(this.props.first){
4 return<div className ="first">< span > A Span</span></div >;
5 }else{
6 return<div className ="second">< p > A Paragraph</p></div >;
7 }
8 }
9 });

 

在任何时间点,你描述你想要你想要的UI。重要的是要理解render的结果不是一个实际的DOM节点。这些只是轻量级的JavaScript对象。我们称之为虚拟DOM。

React将使用这个表示(译者注:虚拟DOM)来尝试找到从前一个渲染到下一个渲染的最少步骤数。例如,如果我们加载<MyComponent first={true} />,替换它<MyComponent first={false} />,然后卸载它,这里是DOM指令结果:

没有第一

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

第一到第二

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

最好的

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

逐级

找到两个任意树之间的diff是一个O(n ^ 3)问题。你可以想象,这是不适合我们的实际使用情况。React使用简单而强大的启发式方法在O(n)中找到非常好的近似。

React只尝试逐级协调树。这大大降低了复杂性,并不是一个大损失,因为在Web应用程序中将组件移动到树中的不同级别是非常罕见的。他们通常只在同级的节点间移动。

 

 

列表

假设我们有一个组件,在一次迭代渲染5个组件,然后下一次操作是插入一个新的组件在列表的中间。

默认情况下,React将上一个列表的第一个组件与下一个列表的第一个组件相关联,以此类推。您可以提供一个key属性,以帮助React找出映射。在实践中,找出每一个孩子节点的唯一key值是很容易的。

 

 

组件

React应用程序通常由许多用户定义的组件组成,最终变成主要由divs 组成的树。这个附加信息被diff算法考虑,因为React将仅匹配具有相同类的组件。

例如,如果一个 <Header>被替换<ExampleBlock>,React将删除<Header>并创建一个<ExampleBlock>。我们不需要花费宝贵的时间来尝试匹配不可能具有任何相似性的两个组件。

 

 

事件委托

将事件侦听器附加到DOM节点是非常缓慢和内存消耗。相反,React实现了一种名为“事件委托”的流行技术。React更进一步,重新实现一个符合W3C标准的事件系统。这意味着Internet Explorer 8事件处理错误是过去的事情,所有的事件名称在不同的浏览器中是一致的。

让我解释一下它是如何实现的。单个事件侦听器附加到文档的根目录。当事件触发时,浏览器会向我们提供目标DOM节点。为了通过DOM层次结构传播事件,React不在虚拟DOM层次结构上进行迭代。

相反,我们利用每一个React组件都有一个层次编码的独一无二的id值得事实。我们可以使用简单的字符串操作来获取所有父级的id。通过将事件侦听器存储在一个hash map中,我们发现它比将它们附加到虚拟DOM表现更好。下面的实例展示了 事件在虚拟DOM分发的过程

1 dispatchEvent('click','a.b.c', event);
2 clickCaptureListeners['a'](event);
3 clickCaptureListeners['a.b'](event);
4 clickCaptureListeners['a.b.c'](event);
5 clickBubbleListeners['a.b.c'](event);
6 clickBubbleListeners['a.b'](event);
7 clickBubbleListeners['a'](event);

浏览器为每个事件和每个侦听器创建一个新的事件对象。这有一个nice的特性 ,您可以保留对事件对象的引用或甚至修改它。然而,这意味着进行大量的内存分配。React在启动时分配这些对象的分发池。每当需要一个事件对象时,它都会从该分发池中重用。这显着减少了垃圾收集。

渲染(render)

 

批处理

每当你调用setState一个组件时,React会将其标记为脏。在事件循环结束时,React会查看所有脏组件并重新呈现它们。

此批处理意味着在事件循环期间,正好有一次DOM正在更新。这个特性是构建高性能应用程序的关键,但是使用常用的JavaScript极难获得。在React应用程序中,默认情况下会得到它。

 

 

子树渲染

setState调用时,组件为其子代重建虚拟DOM。如果你调用setState根元素,那么整个React应用程序将被重新渲染。所有的组件,即使他们没有改变,将render调用他们的方法。这听起来可怕和低效,但在实践中,这工作正常,因为我们不触摸实际的DOM。

首先,我们正在谈论显示用户界面。由于屏幕空间有限,您通常一次显示数百到数千个元素的顺序。JavaScript已经得到足够快的业务逻辑为整个接口是可管理的。

另一个重要的点是,当编写React代码时,通常不会在每次更改时在根节点上调用setState。您在接收到更改事件或上面的几个组件的组件上调用它。你很少去一路到顶部。这意味着更改会本地化到用户交互的位置。

 

 

选择性子树渲染

最后,你有可能防止一些子树重新渲染。如果在组件上实现以下方法:

1 boolean shouldComponentUpdate (object nextProps ,object nextState )

基于组件的上一个和下一个属性/状态,你可以告诉React这个组件没有改变,没有必要重新渲染它。当正确实施时,这可以提供巨大的性能改进。

为了能够使用它,你必须能够比较JavaScript对象。有很多关于这个的issues,比如应该浅层比较还是深层比较;如果它是深层的,我们应该使用不可变的数据结构或做深拷贝。

并且你想记住这个函数将一直被调用,所以你想确保计算所需的时间比启发式的渲染组件所需的时间要少,不是严格需要渲染。

 

 

结论

使React快速的技术不是新的。我们已经知道很长时间,触摸DOM是昂贵的,你应该批量写和读操作,事件委派更快...

人们仍然谈论他们,因为在实践中,他们很难在常规JavaScript代码中实现。使React脱颖而出的是所有这些优化默认发生。这使得很难射击自己在脚,使你的应用程序缓慢。

React的性能成本模型也很容易理解:每个setState重新呈现整个子树。如果你想挤出性能,调用setState尽可能低,并使用shouldComponentUpdate来防止重新渲染一个大的子树。

 
 





posted @ 2017-03-11 15:57  Kasmine  阅读(622)  评论(0编辑  收藏  举报