react-native若有性能问题,很可能是由于组件的重复渲染,研究下相关知识点。
export default class Home1 extends Component {
render() {
console.log('渲染根');
return (
<View style={{backgroundColor: 'white', marginTop: 100}}>
<ComponentA/>
<ComponentB/>
</View>
);
}
}
class ComponentA extends Component {
render() {
console.log('渲染A');
return (
<Text>组件A</Text>
)
}
}
class ComponentB extends Component {
render() {
console.log('渲染B');
return (
<View>
<Text>组件B</Text>
<ComponentC/>
</View>
)
}
}
class ComponentC extends Component {
render() {
console.log('渲染C');
return (
<View>
<Text>组件C</Text>
<ComponentD/>
</View>
)
}
}
class ComponentD extends Component {
render() {
console.log('渲染D');
return (
<View>
<Text>组件D</Text>
</View>
)
}
}
组件关系图如下:
测试现象:
若A被渲染,则C、D也会跟着被渲染,其他不变。
若根被渲染,所有组件都重新渲染。
若B被渲染,其他组件不变。
结论:某一组件被render,只会导致本身和所有的子组件被重新render,其他的没有影响。
问题:例如当A被渲染时,C、D的数据并未改变,甚至C、D根本就是无状态无属性组件,但它们也被重新渲染了,浪费性能。
组件是否重新渲染的决定因素是组件的生命周期方法shouldComponentUpdate的返回值,而Component组件该方法的默认实现返回值总是true,所以会被重新渲染。可以重写该方法让其根据业务需求判断是否返回true来决定是否刷新组件。但是每个组件都重写该方法很麻烦。
PureComponent组件,继承自Component,已经实现了shouldComponentUpdate,大概实现如下
function shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}
shallowEqual实现在"node_modules/fbjs/lib/shallowEqual"处,高效判断两个对象是否相等,从而决定是否渲染页面。
将上面所有组件的父类改为PureComponent,再次测试,发现当A被渲染时,C、D也不会被渲染了,性能肯定变好。
此法虽好,但也有不少副作用,比如将B组件的实现改为如下
class ComponentB extends PureComponent {
constructor(props) {
super(props);
this.state = {
num: 4,
arr: [1,2,3]
};
}
componentDidMount() {
setInterval(() => {
this.state.arr.push(this.state.num);
this.setState({
num: this.state.num + 1,
arr: this.state.arr
})
}, 2000)
}
render() {
console.log('渲染B');
return (
<View>
<ComponentC arr={this.state.arr}/>
<Text>{`组件B: ${this.state.num}`}</Text>
</View>
)
}
}
开个定时器,不断改变arr的值,组件C的属性发生了变化,但是由于C组件的shouldComponentUpdate判断方法shallowEqual只能对对象做浅比较,也就是判断对象的地址是否一致,这里数组本身地址并未发生变化,仅仅是内容有所变化,该方法鉴别不出来,返回的是false,页面不会重新被渲染,有大问题。
这里的解决方案有好些:
a、在C组件中重写shouldComponentUpdate,判断arr内容是否变化,决定重新渲染。
b、B组件给C组件传递属性前,将arr对象进行深拷贝(有性能问题),重新生成对象
c、利用不可变对象,我这里用的是轻量级的seamless-immutable
不可变对象有诸多优点就不说了,总之很好很强大,性能提升利器。使用之后B组件代码如下
class ComponentB extends PureComponent {
constructor(props) {
super(props);
this.state = {
num: 0,
arr: Immutable([1,2,3])
};
}
componentDidMount() {
setInterval(() => {
let arr = Immutable.asMutable(this.state.arr);
arr.push(5);
let newArr = Immutable(arr);
this.setState({
num: this.state.num + 1,
arr: newArr
})
}, 2000)
}
render() {
console.log('渲染B');
return (
<View>
<ComponentC arr={this.state.arr}/>
<Text>{`组件B: ${this.state.num}`}</Text>
</View>
)
}
}
使用就三步:
1、导入头文件,import Immutable from 'seamless-immutable'
2、数组或对象初始化时使用如Immutable([1,2,3])的方式
3、改变数组或对象时使用专门的api,比如Immutable.asMutable、Immutable.flatMap
有些组件拥有继承关系,但是顶层父类又是继承的Component,这时可以采用pure-render-decorator,给该组件添加一个装饰器扩展方法shouldComponentUpdate,这个效果跟PureComponent基本一致。
import pureRenderDecorator from 'pure-render-decorator';
@pureRenderDecorator
class ComponentA extends PureComponent {
还有个小问题,上面B组件传递到C组件的属性arr,在C组件中并没有在render方法中被使用,理论上该组件是不需要不断渲染的,但是因为shouldComponentUpdate方法返回true所以会反复渲染。当然既然B组件传递了属性arr给C,那么实际开发中arr肯定是必不可少的。我要说的是,如果在开发中C组件拿到arr不是为了渲染更新页面的目的,而是其他的比如统计之类的跟页面渲染无关的事,那么,还是需要自己重写shouldComponentUpdate,避免不必要的渲染发生。
接下来简单说下seamless-immutable中数组的实现方式:
Immutable([1,2,3]),会调用到下面这些方法
function makeImmutableArray(array) { // 方法A
for (var index in nonMutatingArrayMethods) {
if (nonMutatingArrayMethods.hasOwnProperty(index)) {
var methodName = nonMutatingArrayMethods[index];
makeMethodReturnImmutable(array, methodName); // 方法B
}
}
// 给数组添加上flatMap、asObject等一系列自定义方法
if (!globalConfig.use_static) {
addPropertyTo(array, "flatMap", flatMap);
addPropertyTo(array, "asObject", asObject);
addPropertyTo(array, "asMutable", asMutableArray);
addPropertyTo(array, "set", arraySet);
addPropertyTo(array, "setIn", arraySetIn);
addPropertyTo(array, "update", update);
addPropertyTo(array, "updateIn", updateIn);
addPropertyTo(array, "getIn", getIn);
}
// 让数组中的每一项都变为不可变对象
for (var i = 0, length = array.length; i < length; i++) {
array[i] = Immutable(array[i]);
}
return makeImmutable(array, mutatingArrayMethods); // 方法C
}
function makeMethodReturnImmutable(obj, methodName) { // 方法B实现
var currentMethod = obj[methodName];
Object.defineProperty(obj, methodName, {
enumerable: false,
configurable: false,
writable: false,
value: Immutable(currentMethod.apply(obj, arguments))
})
}
function makeImmutable(obj, bannedMethods) { // 方法C实现
for (var index in bannedMethods) {
if (bannedMethods.hasOwnProperty(index)) {
banProperty(obj, bannedMethods[index]);
}
}
Object.freeze(obj);
return obj;
}
B方法:
参数obj就是数组对象,methodName为"map", "filter", "slice", "concat", "reduce", "reduceRight等。Object.defineProperty为数组重定义属性和方法,writable为false变为不可写,该方法的目的就是让数组的这些方法失去作用,外界调用不可变数组的map、concat等方法后不再有效。
C方法:bannedMethods为"push", "pop", "sort", "splice", "shift", "unshift", "reverse"等,banProperty方法的实现也是用Object.defineProperty实现,作用是外界调用不可变数组的push、pop等方法时直接报错。最后Object.freeze(obj)冻结整个数组对象,让其本身无法被修改,变为不可变对象。
A方法:包含B、C,并且给数组添加上自定义的很多方法如遍历flatMap、转换为普通数组asMutable等。array[i] = Immutable(array[i])使数组内部的每一个成员都变为不可变对象,在这里,若数组内部元素又是个数组,则会递归到该方法再次执行,直到数组内部所有对象都变为了不可变对象,基本数据类型不可变对象就是本身。
seamless-immutable使用Object.defineProperty扩展了JavaScript 的Array和Object对象来实现,只支持 Array和Object两种数据类型,API 基于与 Array 和 Object ,因此许多不用改变自己的使用习惯,对代码的入侵非常小。同时,它的代码库也非常小,压缩后下载只有 2K。相比于笨重的教科书级别的Immutable无疑更适用于react-native。
网友评论