React是由facebook开发,用于构建用户界面的js类库,以提升性能为设计理念。在本文中,我将为大家介绍在React中的diff算法,以及它的渲染机制,以便于你能够更好的优化你的程序。
Diff算法
在深入了解实现细节之前,了解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。
React将使用此表示法来尝试找到从上一个渲染到下一个渲染的最小步数,比如,如果我们要用 <MyComponent first={false} />
去替代<MyComponent first={true} />
,插入真实DOM, 然后移除它,下面是DOM指令的结果:
从无到有
创建DOM节点:<div className="first"><span>A Span</span></div>
从一到二
用className="second"
去替换属性 className="first"
用 <p>A Paragraph</p>
去替换节点<span>A Span</span
>
从二到无
移除节点:<div className="second"><p>A Paragraph</p></div>
逐级比较
寻找两个任意树之间最小的修改数需要执行n3次,你可以想象,这对于我们来说是难以接受的。而React用了一个简单而且目前来说都非常强大的计算方法去找到他们的变化,而执行的次数仅仅为n次。
React通过逐级的去比较两颗节点树的差异,这大大降低了复杂性,而且精准度上损失也不大,因为在Web应用程序中将组件树不同级的移动比较是非常罕见的。 组件通常只能在子组件横向移动。
列表List
假设我们有一个组件,它在一个迭代中渲染了5个组件,而下一次渲染的时候在组件列表的中间插入一个新的组件。 只是通过这个信息真的很难知道如何在两个组件列表之间进行映射。
默认情况下,React将先前列表的第一个组件与下一个列表的第一个组件相关联,等等。您可以提供一个Key属性,以帮助React去找到他们的映射关系。 在实际中,这通常很容易把刚刚插入的组件从他们当中找出来。
组件
一个React应用程序通常由许多用户定义的组件组成,最终会变成一个主要由div组成的树。 通过diff算法考虑了这些附加信息,因为React只匹配具有相同类的组件。
例如,如果<Header>
被<ExampleBlock>
替换,则React将删除<Header>
并创建一个<ExampleBlock>
。 我们不需要花费宝贵的时间尝试匹配不太可能有相似之处的两个组件。
事件委托
将事件监听器附加到DOM节点是痛苦的缓慢而且消耗内存的事情。但是,React实现了一种称为“事件委托”的流行技术。 React更进一步,并重新实现了符合W3C标准的事件系统。这意味着Internet Explorer 8事件处理兼容问题将成为过去的事情,所有的事件名称在浏览器之间是一致的。
让我解释一下它的实现。单个事件监听器附加到文档的根节点上。当事件被触发时,浏览器告诉我们触发事件的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在一开始会为这些对象分配内存池。 每当需要事件对象时,它将从内存池中被重新使用,这大大减少了内存垃圾回收。
渲染
批量处理:
每当您在组件上调用setState时,React会将其标记为脏。 在事件循环结束时,React会查看所有脏组件并重新渲染它们。如果是批处理也就意味着在事件循环期间,正在更新渲染DOM的时间正好是一次。 这个属性是构建一个应用程序的性能好坏的关键,但是写常用的JavaScript非常难以获得。 在React应用程序中,默认情况下可以获得。
子树渲染:
调用setState时,组件将重建其子项的虚拟DOM。如果您在根元素上调用setState,则会重新渲染整个React应用程序。所有的组件,即使它们没有改变,也会使用它们的渲染方法。这可能听起来很可怕,效率低下,但在实践中,这样可以正常工作,因为我们没有碰到实际的DOM。
首先,我们讨论一下显示用户界面。由于屏幕空间有限,您通常一次按顺序的显示数百到数千个元素。 JavaScript已经可以足够快的速度处理业务逻辑和整个接口管理。
另一个重要的一点,当编写React代码时,每次发生变化通常不会都在根节点上调用setState。你可以在接收到事件变化的组件或父组件上调用它。我们很少走到顶端,通常用户的交互都是发生在对应的组件上变化。
选择子树渲染:
最后,您可以防止一些子树重新渲染。 如果在组件上使用以下方法:boolean shouldComponentUpdate(object nextProps, object nextState)
基于组件的上一个和下一个props/state,您可以告诉React该组件没有更改,并且不需要重新渲染。 正确使用该方法可以大大提高性能。为了能够使用它,您需要比较JavaScript对象。这里面还是有很多问题的,比如js对象的比较是深点还是浅点,如果深度比较,我是用不可变数据结构还是做深拷贝。而且你还要记住,这个函数将一直被调用,所以你想要确保计算时间要比渲染组件所用的时间要少,即使渲染是多余的。
到底什么情况下使用shouldComponentUpdate?
按照React团队的说法,shouldComponentUpdate是保证性能的紧急出口
http://jamesknelson.com/should-i-use-shouldcomponentupdate
http://www.infoq.com/cn/news/2016/07/react-shouldComponentUpdate复制代码
结论
让React变得如此快的技术已经不是什么新鲜的事情,而且我们很久之前就知道,操作DOM是昂贵的,所以你应该对DOM进行批量的读写、使用事件委托,这些都能使你的程序变得更快。
大家还一直在讨论React, 因为事实上,使用常规的Javascript代码很难去实现这些优化的方法,而React默认就能实现,这也是为什么React为什么能脱颖而出的原因。
React的性能成本模型也很容易理解:每次setState都会重新呈现整个子树。 如果要提高性能,请尽可能少调用setState,并使用shouldComponentUpdate来防止重新所有子树。