react-native若有性能問題蒂誉,很可能是由于組件的重復(fù)渲染扰路,研究下相關(guān)知識點醉锅。
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>
)
}
}
組件關(guān)系圖如下:
測試現(xiàn)象:
若A被渲染肌括,則C轧抗、D也會跟著被渲染匿刮,其他不變狼牺。
若根被渲染峰档,所有組件都重新渲染禁添。
若B被渲染撮胧,其他組件不變。
結(jié)論:某一組件被render老翘,只會導(dǎo)致本身和所有的子組件被重新render芹啥,其他的沒有影響锻离。
問題:例如當A被渲染時,C叁征、D的數(shù)據(jù)并未改變纳账,甚至C、D根本就是無狀態(tài)無屬性組件捺疼,但它們也被重新渲染了疏虫,浪費性能。
組件是否重新渲染的決定因素是組件的生命周期方法shouldComponentUpdate的返回值啤呼,而Component組件該方法的默認實現(xiàn)返回值總是true卧秘,所以會被重新渲染」倏郏可以重寫該方法讓其根據(jù)業(yè)務(wù)需求判斷是否返回true來決定是否刷新組件翅敌。但是每個組件都重寫該方法很麻煩。
PureComponent組件惕蹄,繼承自Component蚯涮,已經(jīng)實現(xiàn)了shouldComponentUpdate,大概實現(xiàn)如下
function shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}
shallowEqual實現(xiàn)在"node_modules/fbjs/lib/shallowEqual"處卖陵,高效判斷兩個對象是否相等遭顶,從而決定是否渲染頁面。
將上面所有組件的父類改為PureComponent泪蔫,再次測試棒旗,發(fā)現(xiàn)當A被渲染時,C撩荣、D也不會被渲染了铣揉,性能肯定變好。
此法雖好餐曹,但也有不少副作用逛拱,比如將B組件的實現(xiàn)改為如下
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的屬性發(fā)生了變化橘券,但是由于C組件的shouldComponentUpdate判斷方法shallowEqual只能對對象做淺比較,也就是判斷對象的地址是否一致卿吐,這里數(shù)組本身地址并未發(fā)生變化,僅僅是內(nèi)容有所變化锋华,該方法鑒別不出來嗡官,返回的是false,頁面不會重新被渲染毯焕,有大問題衍腥。
這里的解決方案有好些:
a磺樱、在C組件中重寫shouldComponentUpdate,判斷arr內(nèi)容是否變化婆咸,決定重新渲染竹捉。
b、B組件給C組件傳遞屬性前尚骄,將arr對象進行深拷貝(有性能問題)块差,重新生成對象
c、利用不可變對象倔丈,我這里用的是輕量級的seamless-immutable
不可變對象有諸多優(yōu)點就不說了憨闰,總之很好很強大,性能提升利器需五。使用之后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鹉动、導(dǎo)入頭文件,import Immutable from 'seamless-immutable'
2、數(shù)組或?qū)ο蟪跏蓟瘯r使用如Immutable([1,2,3])的方式
3宏邮、改變數(shù)組或?qū)ο髸r使用專門的api泽示,比如Immutable.asMutable、Immutable.flatMap
有些組件擁有繼承關(guān)系蜜氨,但是頂層父類又是繼承的Component械筛,這時可以采用pure-render-decorator,給該組件添加一個裝飾器擴展方法shouldComponentUpdate记劝,這個效果跟PureComponent基本一致变姨。
import pureRenderDecorator from 'pure-render-decorator';
@pureRenderDecorator
class ComponentA extends PureComponent {
還有個小問題,上面B組件傳遞到C組件的屬性arr厌丑,在C組件中并沒有在render方法中被使用定欧,理論上該組件是不需要不斷渲染的,但是因為shouldComponentUpdate方法返回true所以會反復(fù)渲染怒竿。當然既然B組件傳遞了屬性arr給C砍鸠,那么實際開發(fā)中arr肯定是必不可少的。我要說的是耕驰,如果在開發(fā)中C組件拿到arr不是為了渲染更新頁面的目的爷辱,而是其他的比如統(tǒng)計之類的跟頁面渲染無關(guān)的事,那么朦肘,還是需要自己重寫shouldComponentUpdate饭弓,避免不必要的渲染發(fā)生。
接下來簡單說下seamless-immutable中數(shù)組的實現(xiàn)方式:
Immutable([1,2,3])媒抠,會調(diào)用到下面這些方法
function makeImmutableArray(array) { // 方法A
for (var index in nonMutatingArrayMethods) {
if (nonMutatingArrayMethods.hasOwnProperty(index)) {
var methodName = nonMutatingArrayMethods[index];
makeMethodReturnImmutable(array, methodName); // 方法B
}
}
// 給數(shù)組添加上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);
}
// 讓數(shù)組中的每一項都變?yōu)椴豢勺儗ο? 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實現(xiàn)
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實現(xiàn)
for (var index in bannedMethods) {
if (bannedMethods.hasOwnProperty(index)) {
banProperty(obj, bannedMethods[index]);
}
}
Object.freeze(obj);
return obj;
}
B方法:
參數(shù)obj就是數(shù)組對象,methodName為"map", "filter", "slice", "concat", "reduce", "reduceRight等趴生。Object.defineProperty為數(shù)組重定義屬性和方法阀趴,writable為false變?yōu)椴豢蓪懟韬玻摲椒ǖ哪康木褪亲寯?shù)組的這些方法失去作用,外界調(diào)用不可變數(shù)組的map刘急、concat等方法后不再有效棚菊。
C方法:bannedMethods為"push", "pop", "sort", "splice", "shift", "unshift", "reverse"等,banProperty方法的實現(xiàn)也是用Object.defineProperty實現(xiàn)叔汁,作用是外界調(diào)用不可變數(shù)組的push统求、pop等方法時直接報錯。最后Object.freeze(obj)凍結(jié)整個數(shù)組對象攻柠,讓其本身無法被修改球订,變?yōu)椴豢勺儗ο蟆?/p>
A方法:包含B、C瑰钮,并且給數(shù)組添加上自定義的很多方法如遍歷flatMap冒滩、轉(zhuǎn)換為普通數(shù)組asMutable等。array[i] = Immutable(array[i])使數(shù)組內(nèi)部的每一個成員都變?yōu)椴豢勺儗ο罄饲矗谶@里开睡,若數(shù)組內(nèi)部元素又是個數(shù)組,則會遞歸到該方法再次執(zhí)行苟耻,直到數(shù)組內(nèi)部所有對象都變?yōu)榱瞬豢勺儗ο笃悖緮?shù)據(jù)類型不可變對象就是本身。
seamless-immutable使用Object.defineProperty擴展了JavaScript 的Array和Object對象來實現(xiàn)凶杖,只支持 Array和Object兩種數(shù)據(jù)類型胁艰,API 基于與 Array 和 Object ,因此許多不用改變自己的使用習慣智蝠,對代碼的入侵非常小腾么。同時,它的代碼庫也非常小杈湾,壓縮后下載只有 2K解虱。相比于笨重的教科書級別的Immutable無疑更適用于react-native。