react-native元件避免重複渲染
react-native若有效能問題,很可能是由於元件的重複渲染,研究下相關知識點。
轉載http://www.tuicool.com/articles/vY7Vjym
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。