react-native组件避免重复渲染

作者: 请叫我啊亮 | 来源:发表于2017-09-13 17:56 被阅读1231次

    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。

    相关文章

      网友评论

        本文标题:react-native组件避免重复渲染

        本文链接:https://www.haomeiwen.com/subject/sfqlsxtx.html