Diff算法
开发中我们知道,数据的更新:例如DOM节点、组件属性的增加、删除、修改等引起界面的变化。在React中,每次组件的状态或属性更新,组件的render方法都会返回一个新的虚拟DOM对象,用来展示新的UI结构。
❝
UI = render(data)
❞
虚拟DOM(Virtual DOM)
与虚拟DOM相对应的是真实DOM,真实DOM即DOM对象,其是对 结构化文本 的抽象表达,它定义了访问HTML文档对象的一套属性、方法和事件。
页面中的每一个HTML元素对应一个DOM节点,例如常见的p元素、div元素,HTML元素的层级嵌套关系也会体现在DOM节点的层级上,所有DOM节点构成了DOM树。如下图:
DOM树 图片来自百度
在传统的前端开发中(如下图),直接对DOM进行增删改查的操作时,每一次修改都会引起浏览器对网页的 重排 和 重绘。
而这个过程是很耗时且消耗性能的。因此在开发中性能优化方面应该 「尽量减少DOM操作」。
虚拟DOM则是对真实DOM的抽象,其表现为普通的Javascript对象,当需要操作DOM时,可以操作虚拟DOM,访问Javascript对象远比访问真实DOM快,因此避免了真实DOM效率低下的情况。
dom-diff
当界面上某一节点发生变化时,React会通过比较两次虚拟DOM结构的变化找出差异部分,更新到真实DOM上,从而减少最终要在真实DOM上执行的操作,提高程序执行效率。这一过程称为React的调和过程(Reconciliation),其中的关键是比较两个树形结构的Diff算法。
值得注意的是:「在Diff算法中,比较的两方是新的虚拟DOM和旧的虚拟DOM,而不是虚拟DOM和真实DOM」,只是最终Diff的结果会更新到真实DOM上。
两个假设
React比较两棵树的差异基于以下两个假设:
- Two elements of different types will produce different trees.(两个不同类型的元素会生成不同类型的树)
- The developer can hint at which child elements may be stable across different renders with a key prop.(同一层次的一组节点,可以通过唯一的key属性来进行区分在render过程中是否发生了变化。)
React对DOM树进行了分层,只会对同一层的节点进行数据比较(如下图)。另外,React认为在Web UI中DOM节点的跨层级移动操作比较少,甚至可以忽略不计。
比较两棵树的差异是从树的根节点开始,根节点的类型不同,则React执行的操作也不同。
根节点是相同DOM元素类型
如果两个根节点是相同类型的DOM元素,React会保留根节点对应的DOM元素,对树形结构根节点上的属性和内容比对,然后只更新虚拟DOM树和真实DOM树中对应节点的属性和内容即可。
根节点是相同组件类型
如果两个根节点是相同类型的组件,节点发生变化时,对应的组件实例不会被销毁,只是会执行更新操作,同步变化的属性到虚拟DOM树上,此时组件实例的componentWillReceiveProps() 和 componentWillUpdate() 会被调用。
开发人员可以通过shouldComponentUpdate() 进行优化,如果发现根本没有必要重新渲染,那就可以直接返回false,减少不必要的更新。
根节点类型不同
当根节点类型发生变化时包括两种:DOM元素的变化和Reac组件的变化。
当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。
当根节点为不同类型的组件时,React会认为这两个DOM树结构不同,将之前的组件直接删除,然后创建新组件。
组件类型发生变化时,在旧的虚拟DOM树被拆除的过程中,旧的DOM元素类型的节点会被销毁,旧的React组件实例的componentWillUnmount() 会被调用;
在重建的过程中,新的DOM元素会被插入DOM树中,新的组件实例的componentWillMount() 和 componentDidMount() 方法会被调用。重建后的新的虚拟DOM树又会被映射更新到真实DOM树中。
性能优化
避免不必要的组件渲染
当组件的 props 或 state 发生变化时,组件的 render 方法会被重新调用,返回一个新的虚拟DOM对象。
但在一些情况下,组件是没有必要重新调用 render 方法的。例如:如果只修改了父组件的数据, 并没有修改子组件的数据, 并且子组件中也没有用到父组件中的数据,那么子组件还是会重新渲染, 子组件的render方法还是会重新执行。
shouldComponentUpdate() 这个方法的默认返回值是true,如果返回false,组件此次的更新将会停止,也就是后续的 componentWillUpdate() 、render() 等方法都不会再被执行。我们可以根据组件自身的业务逻辑决定返回true还是false,从而避免组件不必要的渲染。
当然也建议使用 PureComponent 类来代替 shouldComponentUpdate()
shouldComponentUpdate(nextProps, nextState, nextContext) {
// return true;
// return false;
if(this.state.age !== nextState.age){
return true;
}
return false;
}
另外,要尽可能少在 render() 减少变量的新建和绑定函数,例如实现单击事件的时候,如果在 render() 函数里面绑定 this ,则每次渲染的时候都会重新执行一遍函数。所以提前绑定函数也是一种比较好的开发习惯,可以起到部分优化作用。
使用key
React会根据key索引元素,在 render 前后,判定拥有相同key值的元素是否为同一个元素。
给列表元素添加key,告诉React除了和同层同位置比, 还需要和同层其它位置对比。注意添加key属性时,需要保持其“唯一性”且“稳定性”。
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
不推荐使用数组下标作为key,看起来key值是唯一的,但是却不是稳定不变的,随着数组值的不同,同样一个实例在不同的更新过程中在数组中的下标完全可能不同,把下标当做key就让React完全混乱了。
使用性能检测工具
1.React Developer Tools for Chrome
主要用来检测页面使用的React代码是否是生产环境版本。当访问网页时,如果插件图标的背景色是黑色的,就表示当前网页使用的是生产环境版本的React;
如果插件图标的背景色是红色的,就表示当前网页使用的是开发环境版本的React
在项目中上线时尽量使用 「生产环境版本的库」 也能优化一部分性能,以 npm run start 启动时,使用的React是开发环境版本的React库,包含大量警告消息,以帮助我们在开发过程中避免一些常见的错误。开发环境版本的库不仅体积更大,而且执行速度也更慢,不适合在生产环境中使用。
对于create-react-app脚手架创建的项目,执行 npm run build ,就会构建生产环境版本的React库。
2.Chrome Performance Tab
在开发模式下,通过Chrome浏览器提供的Performance工具观察组件的挂载、更新、卸载过程及各阶段使用的时间。
3.React Perf/Jest/Enzyme等测试库
尽可能多地使用小组件
例如使用MobX和React结合时,@observer 包装的组件会追踪render方法中使用的所有可观测对象,组件越小,组件追踪的对象越少,引起组件重新渲染的可能性也越小。且在层级越低的小组件中解引用对象属性时,由这个属性的变化导致的重新渲染的组件的数量也越少。
慎用{...this.props}
只传递组件需要的props,传得太多或者层次传得太深时,都会加重shouldComponentUpdate 里的数据的比较负担,因此慎用spread attributes(形如 <Component{...props}/> )。
小结:性能优化本身涉及多方面因素,建议不要将性能优化的精力浪费在对整体性能提高不大的代码上,且不要过早优化。在实际开发中,也有其他途径值得实践,例如:使用CDN处理静态资源、启用Gzip压缩、DNS预解析等等