day184-Diff Algorithm(译)

要点

本文为一篇经典的Diff算法文章的译文,原文写的是React的Diff算法。
这里只是摘取了后半部分的内容,并没有翻译开篇部分。

原文点此

level by level

找出两个任意二叉树的最小变化是一个O(n^3)问题。但是React用简单且强大的启发式来找一个O(n)的最优解。

React尝试逐层地去求解二叉树。这种方式大幅地减少了树的复杂度且在web应用中组件移动到不同层级时产生极少的(性能)损耗。通常这种移动都是横向的子节点间移动。

List

假设有在一个组件中某次迭代渲染5个组件之后要在List的中间位置插入一个新的组件。这里很难去根据这个信息去知道如何映射在两个list组件之间。(This would be really hard with just this information to know how to do the mapping between the two lists of components.)

默认情况下,React会将前一个list中的第一个组件和下一个list中的第一个组件相关联,等等。你可以加一个key属性来帮助React找出映射。在实践中,这样做通常能很容易找到唯一的key就存在子节点之间。

Components

一个React应用通常是由很多用户定义的组件组成并最终转换成一棵主要由div组成的树。其他的信息会被diff算法考虑在内,因为React只匹配具有相同类的组件。
例如,如果一个<Header>被一个<ExampleBlock>替代,React会移除header创建一个example block。不需要我们花宝贵的时间去尝试匹配两个组件而且是不太可能有任何的相似之处。

Event Delegation(事件委托)

添加事件监听到DOM节点上非常的慢而且还费内存。相反的是,React采用一种非常流行的技术叫“event delegation”。React则更进一步重新实现符合W3C标准的事件系统。这个意味着Internet Explorer 8事件处理的问题已经成为过去,事件名称在所有的浏览器中都是一致的。

让我解释下它是怎么实现的。一个单独的事件监听被添加到document的root中。当该事件被触发时,浏览器给我们提供目标DOM节点。为了通过DOM层级分发事件(propagate the even),React不会再virtual DOM层级上进行迭代。

相反,我们用的每个React组件实际上都有唯一的id来编码层级(encodes the hierarchy)。我们可以用简单的字符操作来获取所有父组件(原文为:parents)的id。通过将事件监听存储在hash map中,
我们发现性能要优于添加到virtual DOM。这里有个例子来解释当一个事件通过virtual DOM被调度(dispatched)会发生什么。

// dispatchEven('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);

浏览器创建一个新的事件对象给每个事件和每个监听者(listener)。这个性质的好处在于你可以保留事件对象的引用甚至可以修改它。然而,这意味着要进行大量的内存分配。React在启动时会分配这些对象一个池。
需要事件对象时,它会从池子里去取。这极大地减少了垃圾收集。

Rendering

Batching(批量)

当你调用在组件内的setState时,React会标记它为脏值。在事件循环的末尾,React会查看组件所有的脏值并重新渲染它们。
批量表示在一次事件循环期间,DOM只有一次的更新。这个性质是建立高性能应用的关键,但用常规的JavaScript非常难写这个。在React应用中,默认有这个性质。

Sub-tree Rendering

setState被调用时,组件给其子节点重新建立virtual DOM。如果你在根元素调用setState,整个React应用就会重新渲染。
所有的组件,即使它们没有改变,都会被他们的render方法调用。这可能听起来很可怕且在实际情况中低效,这很好用,因为我们没有接触到真实DOM。

首先,我们谈谈关于用户界面的展示。因为屏幕空间的限制,你可能通常一次性地展示成百上千的节点。JavaScript在整个可管理的界面上的业务逻辑已经够快了(JavaScript has gotten fast enough business logic for the whole interface is manageable.)另一个重要的点是写React代码时,你通常不会在根节点每次变化时都调用setState。你会在收到变动的事件或上层几个组件时在组件上调用。很少会直接到顶部。这表明改动会落实到用户的交互。

Selective Sub-tree Rendering

最后你有可能阻止一些子树重新渲染。如果你在组件上用下列方法:

boolean shouldComponentUpdate(object nextProps object nextState)

建立在前后(previous and next)的props/state组件上,你可以告诉React这个组件不要改动且没必要再重新渲染它。用的得当时,这个可以让性能极大地提升。
为了能够用它,必须能够比较JavaScript对象。如果比较的浅或深会有很多的问题产生;如果深的话我们是否要用不可变数据结构或者做深拷贝。
而你想要记住,这个功能会一直被调用,因此你想确认它计算所需的时间是否少于启发式少于渲染组件的花费,及时不是严格需要重新渲染。

Conclusion

这技术让React变得更快这并不新鲜。我们很早就知道,接触DOM的开销很大,应该批量的读写操作,事件委托更快…

人们依旧在讨论这些东西,因为在实践中,它们很难用于常规的JavaScript代码。让React脱颖而出的是所有这些优化都是默认。这让你很难搬起石头砸自己的脚,让你的应用变慢。
这些React的性能开销模型也非常容易理解:每个setState重新渲染整个子树。如果你想挤出性能,尽可能的少调用setState同时用shouldComponentUpdate来阻止一颗大子树的重新渲染。
(全文完)

文章作者: lmislm
文章链接: http://lmislm.com/2019/07/16/2019-07-16/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LMISLMのBlog