一、前言
对于react 高阶组件。大家基本只知道定义和用法,在实际开发中,使用的可能并不多。项目组下来一个任务,重写某个由Antd Table
渲染为主要功能的模块,这里发现一些问题,Antd Table
在渲染时,即使没有改变Table
的props
,只是改变包裹Table
的组件,Table
组件也会重新渲染。
二、问题产生
import {
Table} from 'antd';
class TestRoot extends Component {
change() {
this.setState({
self_state: 2
});
}
render() {
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
render: (text, record, index) => {
console.log('rerendered !!!')
return <div>{
text}</div>;
}
},
{
title: '年龄',
dataIndex: 'age',
key: 'age'
},
{
title: '住址',
dataIndex: 'address',
key: 'address'
}
];
return (
<div>
<h1>{
this.state.self_state}</h1>
<Table columns={
columns} dataSource={
this.state.data_arr} />
</div>
);
}
}
当我的TestRoot
组件自身状态发生改变时,会发现控制台不断的输出rerendered !!!
这样的提示。证明Table
表格内部做了重新渲染操作。
三、分析 & 解决
像Antd
这样的UI库,重复渲染问题是优化问题,一般来说库不会太过分做这些优化。我们要做的就是通过控制Table
组件的shouldComponentUpdate
来进行过滤非必要的状态渲染。这时候我们想到了利用高阶组件封装,采用反向继承的方式,注入到Table
组件的渲染逻辑中:
import {
Table } from 'antd';
const withPure = (Comp) => {
return class PureTable extends Comp {
shouldComponentUpdate(nextProps, nextState) {
//--在这里控制
return true;
}
render() {
return super.render();
}
};
};
export default withPure(Table)
使用方式,将原先直接使用Table
换成使用PureTable
。
return (
<div>
<h1>{
this.state.self_state}</h1>
<PureTable columns={
columns} dataSource={
this.state.data_arr} />
</div>
);
大家注意,这是高阶组件的第二种实现方式。采用的是继承源组件的形式,这样我们可以通过覆写源组件的一些方法,来实现控制渲染流程,是一种深度攻击注入。
1. 通过shallowCompare 进行第一层浅比较
有关shallowCompare
的用法,大家可以自行百度,就是一层浅比较。可以判断出state和props第一层的变化。但是无法判断深层次的引用对象。
npm i react-addons-shallow-compare --save
import {
Table } from 'antd';
let shallowCompare = require('react-addons-shallow-compare');
const withPure = (Comp) => {
return class PureTable extends Comp {
shouldComponentUpdate(nextProps, nextState) {
return !shallowCompare(this, nextProps, nextState);
}
render() {
return super.render();
}
};
};
export default withPure(Table)
2.通过lodash _.isEqual进行深比较
上述代码测试后发现,改变self_state后,确实表格没有再渲染。但是很明显,当我的dataSource进行变化时,shallowCompare并不会判断出来,禁止了表格的渲染。例如下面这个直接修改dataSource的例子:
change3() {
const {
data_arr } = this.state;
data_arr[0].name = '胡二狗';
this.setState({
data_arr
});
}
这里大家有2个分支,第一个是根据业务情况,自行判断需要的Table的深props和state,比如dataSource属性,或者直接使用深比较,判断状态和props。第二种方式明显效率更低,但是可以保证表格的渲染不会受到影响。
import {
Table } from 'antd';
let shallowCompare = require('react-addons-shallow-compare');
const withPure = (Comp) => {
return class PureTable extends Comp {
shouldComponentUpdate(nextProps, nextState) {
const is_sample = shallowCompare(this, nextProps, nextState);
if(!is_sample) {
return true;}
const state_sample = _.isEqual(nextState, this.state);
if(!state_sample) {
return true;}
const props_sample = _.isEqual(nextProps, this.props);
if(!props_sample) {
return true;}
return false;
}
render() {
return super.render();
}
};
};
export default withPure(Table)
到这里,理论上当我只改变Root组件的自身状态时,不会在触发表格的渲染了。可是在lodash进行深比较时,还是有一个属性判断不一致,深度调试之后发现是两次props传递的columns属性中的render函数不同。这里发现一个表格写法问题。
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
render: (text, record, index) => {
console.log('rerendered !!!')
return <div>{
text}</div>;
}
},
{
title: '年龄',
dataIndex: 'age',
key: 'age'
},
{
title: '住址',
dataIndex: 'address',
key: 'address'
}
];
return (
<div>
<h1>{
this.state.self_state}</h1>
<Table columns={
columns} dataSource={
this.state.data_arr} />
</div>
);
大家注意姓名那一栏的render
函数。这种写法会导致每次都创建一个新的render
函数引用,导致lodash深比较的时候,判断不一致,导致表格重新渲染。
将render方法定义在类中,修改定义方式如下:
//--省略
render_column_name = (text, record, index) => {
console.log('column rendered!');
return <TestColumnChild name={
text} ></TestColumnChild>;
}
render() {
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
render: this.render_column_name
},
{
title: '年龄',
dataIndex: 'age',
key: 'age'
},
{
title: '住址',
dataIndex: 'address',
key: 'address'
}
];
)
经过测试,一切正常。设置Root本身的状态不会再导致表格的重新渲染。
四、升华
其实我们发现,我们只是通过包装器,自己实现了一个pureComponent
。这个包装器,如果按照上面深浅比较的通用写法,其实可以包装任何组件,不仅仅限于Table。其实这就是高阶组件的精髓所在。这里引入一个库,recompose
这是react中的lodash。这里仅仅抛砖引玉,给出我上面的写法的一种recompose实现。后续章节会就recompose做更详细的分享。
import {
compose, pure } from 'recompose';
import {
Table} from 'antd';
const composeHoc = compose(pure);
export default composeHoc(Table);
是不是很简单?
这里的实现方式,就是通过recompose原生的pure包装器去进行组件包装。只不过这里的pure是浅比较。recompose内提供了大量的短小精美的包装器,可以通过compose进行层层包装,增强我们的组件。