这篇文章是基于 React 官方文档对于 Virtual DOM 的理念和 Diffing 算法的策略的整合。html
Virtual DOM 是一种编程理念。UI 信息被特定语言描述并保存到内存中,再经过特定的库,例如 ReactDOM 与真实的 DOM 同步信息。这一过程成为 协调 (Reconciliation)
。node
Virtual DOM 反映到实际的数据结构上,就是每个 React 的 fiber nodereact
// UI 组件描述 const Span = (props) => <span></span> // 实际的 Fiber node structure { stateNode: new HTMLSpanElement, type: "span", alternate: null, key: null, updateQueue: null, memoizedState: null, pendingProps: {}, memoizedProps: {}, tag: 1, effectTag: 0, nextEffect: null }
这一抽离结构有点像 React 版本的 AST 抽象语法树。算法
在 Virtual DOM -> Real DOM 之间的转换过程当中,须要高效率的算法来支撑。因为某个时刻调用 React render() 方法生成的 React 元素组成的树,与下一次 state 或 props 变化时调用同一个 render 返回的树是不同的,React 须要根据这两个不一样的树来决定如何高效地让最新的 Virtual DOM 反应到真实 DOM 中。编程
Diffing 算法就是解决如何更有效率地更新 UI 的关键。数组
React 采起了一个复杂度为 O(n) 的比较策略,这个策略有两个假设数据结构
若是为不一样类型,React 将会把原有的树拆卸并从新创建新的树。例如 <div>
-> <span>
。ide
在根节点如下的组件也会被卸载,它们的状态会被销毁。例如:函数
<div> <Counter /> </div> <span> <Counter /> </span>
当对比两个同类型的 React 元素时,React 会保留 DOM 节点,仅对比以及更新有变化的属性性能
<div className="before" title="stuff" /> <div className="after" title="stuff" />
经过对比两个元素,React 得知 className
变化,因此只须要更新 DOM 对应元素上的 class
。
当处理完当前节点时,React 将会对子节点进行递归。
当一个 React 组件须要更新时(例如 props
有变化),组件实例保持不变,实例中的 state 能在不一样渲染时保持一致。React 将更新该组件实例的 props
以保持与最新的元素的一致。并调用 该实例的原型 上的函数 getDerivedStateFromProps
(官方文档是 componentWillReceiveProps 和 componentWillUpdate,但这将会被弃用)。
下一步是调用该实例的 render
方法,diffing 算法将在以前的结果和最新的结果中进行递归。
在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表,当发现两个子元素有差别时,将生成一个「变种(mutation)」。
例如在子元素列表末尾新增元素时,更变开销比较小。好比:
// before <ul> <li>first</li> <li>second</li> </ul> // after <ul> <li>first</li> <li>second</li> <li>third</li> </ul>
React 会匹配两个 <li>first</li>
对应的树、两个 <li>second</li>
对应的树,而后插入 <li>third</li>
树。
但若是就这样简单实现的话,那么在列表头部插入会很影响性能,更变的开销会比较大。好比:
<ul> <li>Duke</li> <li>Villanova</li> </ul> <ul> <li>Connecticut</li> <li>Duke</li> <li>Villanova</li> </ul>
React 会认为每一个子元素都「改变(mutate)」了,而不会认为能够保持 <li>Duke</li>
和 <li>Villanova</li>
子树不变,从而致使从新渲染。这种状况下的低效可能会带来性能问题。
为了解决以上问题,React 支持 key 属性。当子传入 key 到子元素时,React 经过 key 来匹配比较原有树上的子元素
以及最新树上的子元素
的差别。如下例子在新增 key 以后使得以前的低效转换变得高效:
<ul> <li key="2015">Duke</li> <li key="2016">Villanova</li> </ul> <ul> <li key="2014">Connecticut</li> <li key="2015">Duke</li> <li key="2016">Villanova</li> </ul>
如今 React 知道只有带着 '2014' key 的元素是新元素,带着 '2015' 以及 '2016' key 的元素仅仅移动了位置。
因此通常在开发的时候最好使用一个有惟一属性的 id 来做为 key
<li key={item.id}>{item.name}</li>
在开发者本身肯定数组数据不会轻易改变
的状况下才能够用数组下表来做为 key。
上述只是 协调算法(reconciliation algorithm)的实现细节而已。React 能够响应每一次 action 后从新渲染整个应用,最终结果也会是同样的。
须要明确知道的是,在当前上下文(this context)
中从新渲染(rerender)
意味着会调用全部的 component
的 render()
,但并不意味着 React 会卸载(unmount)
或重载(remount)
它们。它(协调算法)只会用上述规则在其过程当中找出不一样。