React中的差分算法(React's diff algorithm) --- by Christopher Chedeau

React 是由Facebook开发的一个用于构建用户界面的Javascript库。它一开始就是为了高性能而设计的。在这篇文章中我会阐述React中的差分算法和渲染的工作原理以便于你优化你自己的应用。

差分算法(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是怎样的。理解渲染的结果并不是真实的DOM节点是非常重要的。那些都只是轻量的JavaScript对象。我们叫它们虚拟DOM(virtual DOM)。
React总是试着找到从上次渲染到下次渲染的最小步骤。举个例子,如果我们挂载了<MyComponent first={true} />, 用<MyComponent first={false} /> 替换,然后卸载它,下面是 DOM指令的结果:

None to first

  • Create node: <div className="first"><span>A Span</span></div>

First to second

  • Replace attribute: className="first" by className="second"
  • Replace node: <span>A Span</span> by <p>A Paragraph</p>

Second to none

  • Remove node: <div className="second"><p>A Paragraph</p></div>
逐级调和(Level by Level)

找到两个随机树之间的最小的修改次数是一个 O(n^3)问题。如你所想,这对我们用例来说是不好处理的。React使用简单且强大的启发式方式去找到一个O(n)的非常好的近似值。
React只会试着逐级调和树。这大大地减少了复杂度并且没有很大的损失,因为在web应用中把一个组件移动到不同级别的情况是非常罕见的。它们通常只是在子项中横向移动。
逐级调和

列表(List)

假设我们有一个组件要在一个迭代中渲染5个组件,接下来要在这个列表中间插入一个新的组件。只有这些信息的情况下,想要知道怎么样给这两个列表中的组件做映射是非常困难的。
默认的,React会将先前列表的第一个组件与下一个列表的第一个组件关联起来,以此类推。你可以创建一个属性 key 来帮助React完成映射。事实上,在子项中找到一个唯一的key通常是很容易的。
列表

组件(Component)

一个React应用通常是由许多用户定义的组件组成的,这最终会变成一个主要由div组成的树。这些额外的信息也会被差分算法考虑在内因为React只会将拥有相同类的组件配对在一起。
举个例子,如果一个 <Header>组件被 <ExampleBlock>替换了,React会直接移除掉header并且创建一个example block。我们不需要浪费宝贵的时间在尝试匹配两个不太有可能由多少相似的组件上。
component

事件委托(Event Delegation)

给DOM节点附加事件监听是非常慢并且消耗内存的。相应的,React使用了一种非常流行的技术——事件委托。React更进一步并重新实现了符合W3C标准的事件系统。这意味着IE8的事件处理错误成为了过去式并且所有的事件名称在浏览器间保持了一致。
让我来解释一下它是怎么实现的。单个事件监听器被附加在文档的根节点上,当一个事件被触发,浏览器会告诉我们目标DOM节点。为了在DOM层级结构中传播事件,React不会在虚拟DOM中再次迭代。
相对的我们使用每个React组件具有编码层次结构且每个组件拥有唯一id的事实。我们能够用简单的字符操作来获取所有父组件的id。通过将事件监听器存储在哈希映射中,我们发现这比把它们附加到虚拟DOM中要表现得更好。下面是一个当一个事件在虚拟DOM中派发了之后会发生什么的例子。

// dispatchEvent('click', 'a.b.c', event)
clickCaptureListeners['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的组件并重新渲染它们。
这种批处理方式意味着在一次事件循环中,DOM只会有且仅有一次机会更新。这个属性是构建高性能应用的关键,但是在常规的JavaScript书写中很难获得。而在React应用中,你默认就获得这了能力。
batching

子树渲染(Sub-tree Rendering)

当setState被调用,组件会重建它自组建的虚拟DOM。如果你调用的是根元素的setState,那么接下来整个React应用都会被重新渲染。所有的组件,即使它们没有改变,它们的render方法都会被调用。这听起来有些吓人并且很低效,但是实际上,这运行得很好因为我们没有触及真实的DOM。
首先,我们在谈论用户界面的展示。因为屏幕空间的限制,所以你通常按照顺序短时间显示成百上千的元素。为了整个界面是可管理的,JavaScript需要拥有足够快的业务逻辑。
另一个重点是当在书写React代码时,你通常不会调用在每次有变化的时候在根节点上调用setState。你会在接收到改变事件或者这些组件之上的组件上调用它。你基本不会在最顶层调用。这意味着变化会定位到用户交互的位置。
子树渲染

选择性子树渲染(Selective Sub-tree Rendering)

最后,你有能力去阻止一些子树的重渲染。如果你在组件上次重现下面的方法:

boolean shouldComponentUpdate(object nextProps, object nextState)

基于前一个和后一个组件的属性或状态,你可以告诉React这个组件没有改变并且不需要重新渲染它。当这个方法恰当的实施时,它会给你的应用带来巨大的性能提升。
为了能够使用它,你必须有能力比较JavaScript对象。这会带来很多问题比如比较是要浅还是要深;如果要深对比,我要用不可改变的数据结构还是用深拷贝;
同时你要将这个方法将会一直被调用牢记在心,所以你要确保它花费在计算上的时间要比启发式与它渲染组件的时间要少,即使重渲染不是被严格需要。
选择性子树渲染

结语

让React快速运行的技术不是新技术。我们知道DOM操作是很昂贵的,所以你应该批量进行写入和读取操作,事件委托会快些。。。
由于在实践中,这些技术在常规JavaScript代码中非常难以实现,所以人们仍在谈论。由于这些优化在React中都是默认的所以成就了React的优秀。这让它很难使自己的处境变得更糟或者让应用运行变慢。
React的性能消耗模型也很容易理解,每个setState重渲染整个子树,如果你想要压榨性能,尽量慢的调用setState并用shouldComponentUpdate来阻止渲染大的子树。
原文连接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值