金沙官网线上React Native 组件性能优化

很多人在写React组件的时候没有太在意React组件的性能,使得React做了很多不必要的render,现在我就说说该怎么来编写搞性能的React组件。

前言

从以往的经验和实践中,我们知道影响性能最大的因素是界面的重绘,React 背后的 Virtual DOM 就是尽可能地减少重绘。
对于性能优化这个主题,我们往往会基于 "不信任" 的前提,即我们需要提高 React Virtual DOM 的效率。从 React 渲染过程看,如何防止不必要的渲染可能是最需要取解决的问题。针对这个问题,React 官方提供了一个便捷的方法来解决,那就是 PureRender。

首先我们来看一下下面两个组件

纯函数

要理解 PureRender 中的 Pure,要从函数式编程的的基本概念 "纯函数"讲起。纯函数由三大原则构成。

  • 给定相同的输入,他总是返回相同的输出

对于一个方法,只要我们传入的值是固定,无论做多少次调用,结果都是一样。有些方法不依赖于你传入的参数,如 Math.random(),不传任何参数到方法中,该方法依然总是会输出不同的结果,这种方法也是纯函数。还有常用的 slice 和 splice 方法,slice在参数一致的时候结果都是一致的,splice方法的执行结果会改变原来数组,对于程序来说,splice 的隐藏行为是危险的,所以 splice 不是纯函数。

  • 过程没有副作用

在纯函数中我们不能改变外部状态。

  • 没有额外的状态依赖

方法内的状态都只在方法的生命周期内存活,意味着不能在方法内使用共享变量,这会给方法带来不可知因素。

纯函数也是函数式编程的基础,他完全独立于外部状态,这样就避免了因为共享外部状态而导致的 bug。
纯函数非常方便方法级别的测试以及重构,可以让程序具有良好的扩展性及适应性。
React 就是函数式编程,React 组件本身就是纯函数,可以表示为 UI = f(data),data 包括props 和 state,改变 data,就能改变 UI,而不是像 JQuery 一样直接操纵 UI。
可以通过拆分组件为自组件,进而对组件做更颗粒度的控制。只是函数式编程的魅力之一,保持纯净状态,让方法或组件更加专注 (focused),体积更小 (small),更独立(independent),更具有复用性 (reustability)。

import React, {PureComponent,Component} from "react"

import PropTypes from "prop-types"

class A extends Component {

    constructor(props){
        super(props);
    }

    componentDidUpdate() {
        console.log("componentDidUpdate")
    }

    render (){
        return (
            <div />
        )
    }
}

class Test extends Component {

    constructor(props) {
        super(props);
        this.state={
            value:0
        };
    }

    static propTypes = {};

    static defaultProps = {};

    componentDidMount() {
        setTimeout(()=>{
            this.setState({value:this.state.value+1})
        },100);
    }

    render() {
        return (
            <A />
        )
    }
}

PureRender

pureRender 指的就是组件满足春寒书条件,即组件的渲染是被相同的 props 和 state 渲染进而得到相同的结果。

  1. PureRender 的本质

React 生命周期中有一个 shouldComponentUpdate 方法,如果返回 true,表示需要渲染,返回 false 表示不需要渲染。

  1. 运行 PureRender

React 新版本提供一个 PureComponent,PureComponent 内部重新实现 shouldComponentUpdate 方法,让当前传入的 props 和 state 与之前的作浅比较,,浅比较对 object 只作了引用比较,并没有做值比较,如果返回 false,那么组件就不会执行 render 方法。

  1. 优化 PureRender
  • 直接为props设置对象或数组
    每次调用 React 组件其实都会重新创建组件,就算传入的数组或对象的值没有改变,它们引用的地址也会发生改变。
<TestComponent 
    style={{background: 'white'}}
/>

我们可以把传入的数组或对象保存成一份引用。

<TestComponent 
    style={styles.container}
/>
const styles = StyleSheet.create({
    container: {
        background: 'white'
    },
});
  • 设置 props 方法并通过事件绑定在元素上
onPress() {
}
<TestComponent 
    onPress={this.onPress.bind(this)}
/>

这样写,每一次渲染都会重新绑定 onPress方法, 不要让方法每一次都绑定,因此把绑定移动到构造器内。

constructor(props) {
    super(props);
    this.onPress = this.onPress.bind(this);
}

onPress() {
}

render() {
    <TestComponent 
        onPress={this.onPress}
    />
}

运行结果:

Immutable

在传递的数据的时候,可以直接使用 Immutable Data 来进一步提高组件的渲染性能。

  1. Immutable Data
    Immutable Data 就是一旦创建,就不能再更改的数据。对 Immutable 对象进行修改,添加或者删除操作,都会返回一个新的 Immutable 对象。Immutable 实现的原理是持久化的数据结构,也就是使用旧数据创建新数据,要保证就数据同时可用切不变。同时为了避免深拷贝把所有节点都复制一遍带来的性能损耗,Immtable 使用了结构共享,即如果对象树中一个节点发生变化,只修改这个节点和受他影响的父节点,其他节点进行共享。
  • Map:键值对集合,对应于Object。
  • List:有序可重复的列表,对应于Array。
  • ArraySet:无序切不可重复的列表。
  1. Immutable 的优点
  • 降低了 "可变" 带来的复杂度。
  • 节省内存。Immutable 使用结构共享尽量复用内存。没有被引用的对象会被垃圾回收。
  • 撤销/重做,复制/粘帖,甚至时间旅行这些功能做起来都变得简单。因为每次数据都是不一样的,那么只要把这些数据都放到一个数组里存储起来,想回退到哪里,就拿出对应的数据。
  • 并发安全。Immutable 数据天生不可变,但 js 是单线程运行,现在并没有什么用。
  • 拥抱函数式编程。只要输入一致,那么输出必然一致,这样开发的组件更易于调试和组装。
  1. 金沙官网线上,使用 Immutable 的缺点
    容易与原生对象混淆。
  2. Immutable.is
    为了直接比较对象的值,Immutable提供了Immutable.is 来作"值比较"
const map1 = Immutable.Map({a: 1, b: 1})
const map2 = Immutable.Map({a: 1, b: 1})
map1 === map2; // => false
Immutable.is(map1, map2); // => true

Immutable.is比较的是两个对象的hashCode 或 valueOf (对于JS 对象),由于Immutable 内部使用 trie 数据结构来存储,只要两个对象 hashCode 相等,值就是一样的。
5.Immutable 和 PureRender
React 做性能优化最常用的就是 shouldComponentUpdate 方法,但他默认返回true,即始终会执行 render 方法,然后做 Virtual DOM比较,并得出是否需要做真实 DOM 更新,这里往往带来不必要的渲染,
当然,我们可以在 shouldComponentUpdate 中使用深拷贝和深比较来避免无必要的 render,但是深拷贝和深比较一般都是非常昂贵的选择。
Immutable.js 提供了简洁,高效的判断数据是否变化的方法,只需要 is 比较就能知道是否需要执行 render,而这个操作几乎零成本,所以可以极大提高性能。修改后的 shouldComponentUpdate 是这样的。

import React, {
    Component
} from 'react';
import Immutable from 'immutable';

class BaseComponent extends Component {

    shouldComponentUpdate(nextProps, nextState = {}) {
        return !Immutable.is(Immutable.fromJS(this.props), Immutable.fromJS(nextProps))
        || !Immutable.is(Immutable.fromJS(this.state), Immutable.fromJS(nextState));
    }
}

export default BaseComponent;

而在自定义组件你只需要继承BaseComponent,就可以拦截不必要的渲染。

class App extends BaseComponent {

}
  1. key
    写动态组件的时候,需要给动态子组件添加 key props,否则会报一个警告。Key 要保持惟一,稳定。
Test state change.
A componentDidUpdate

我们发现上面代码中只要执行了Test组件的中的setState,无论Test组件里面包含的子组件A是否需要这个state里面的值,A componentDidUpdate始终会输出

试想下如果子组件下面还有很多子组件,组件又嵌套子组件,子子孙孙无穷尽也,这是不是个很可怕的性能消耗?

当然,针对这样的一个问题最初的解决方案是通过shouldComponentUpdate方法做判断更新,我们来改写下组件A

class A extends Component {

    constructor(props){
        super(props);
    }

    static propTypes = {
        value:PropTypes.number
    };

    static defaultProps = {
        value:0
    };

    shouldComponentUpdate(nextProps, nextState) {
        return nextProps.value !== this.props.value;
    }

    componentDidUpdate() {
        console.log("A componentDidUpdate");
    }

    render (){
        return (
            <div />
        )
    }
}

这里增加了shouldComponentUpdate方法来对传入的value属性进行对面,虽然这里没有传,但是不影响,运行结果:

Test state change.

好了,这次结果就是我们所需要的了,但是如果每一个组件都这样做一次判断是否太过于麻烦?

那么React 15.3.1版本中增加了 PureComponent ,我们来改写一下A组件

class A extends PureComponent {

    constructor(props){
        super(props);
    }

    static propTypes = {
        value:PropTypes.number
    };

    static defaultProps = {
        value:0
    };

    componentDidUpdate() {
        console.log("A componentDidUpdate");
    }

    render (){
        return (
            <div />
        )
    }
}

这次我们去掉了shouldComponentUpdate,继承基类我们改成了PureComponent,输出结果:

Test state change.

很好,达到了我们想要的效果,而且代码量也减小了,但是真的可以做到完全的防止组件无畏的render吗?让我们来看看PureComponent的实现原理

最重要的代码在下面的文件里面,当然这个是React 16.2.0版本的引用

/node_modules/fbjs/libs/shallowEqual

大致的比较步骤是:

1.比较两个Obj对象是否完全相等用===判断

2.判断两个Obj的键数量是否一致

3.判断具体的每个值是否一致

不过你们发现没有,他只是比对了第一层次的结构,如果对于再多层级的结构的话就会有很大的问题

来让我们修改源代码再来尝试:

class A extends PureComponent {

    constructor(props){
        super(props);
    }

    static propTypes = {
        value:PropTypes.number,
        obj:PropTypes.object
    };

    static defaultProps = {
        value:0,
        obj:{}
    };

    componentDidUpdate() {
        console.log("A componentDidUpdate");
    }

    render (){
        return (
            <div />
        )
    }
}

class Test extends Component {

    constructor(props) {
        super(props);
        this.state={
            value:0,
            obj:{a:{b:123}}
        };
    }

    static propTypes = {};

    static defaultProps = {};

    componentDidMount() {
        setTimeout(()=>{
            console.log("Test state change.");
            let {obj,value} = this.state;
            //这里修改了里面a.b的值  
            obj.a.b=456;
            this.setState({
                value:value+1,
                obj:obj
            })
        },100);
    }

    render() {
        let {
            state
        } = this;

        let {
            value,
            obj
        } = state;

        return (
            <A obj={obj} />
        )
    }
}

输出结果:

Test state change.

这里不可思议吧!这也是很多人对引用类型理解理解不深入所造成的,对于引用类型来说可能出现引用变了但是值没有变,值变了但是引用没有变,当然这里就暂时不去讨论js的数据可变性问题,要不然又是一大堆,大家可自行百度这些

那么怎么样做才能真正的处理这样的问题呢?我先增加一个基类:

import React ,{Component} from 'react';

import {is} from 'immutable';

class BaseComponent extends Component {

    constructor(props, context, updater) {
        super(props, context, updater);
    }

    shouldComponentUpdate(nextProps, nextState) {
        const thisProps = this.props || {};
        const thisState = this.state || {};
        nextState = nextState || {};
        nextProps = nextProps || {};
        if (Object.keys(thisProps).length !== Object.keys(nextProps).length ||
            Object.keys(thisState).length !== Object.keys(nextState).length) {
            return true;
        }

        for (const key in nextProps) {
            if (!is(thisProps[key], nextProps[key])) {
                return true;
            }
        }

        for (const key in nextState) {
            if (!is(thisState[key], nextState[key])) {
                return true;
            }
        }
        return false;
    }
}

export default BaseComponent

大家可能看到了一个新的东西Immutable,不了解的可以自行百度或者 Immutable 常用API简介  , Immutable 详解

本文由金沙官网线上发布于Web前端,转载请注明出处:金沙官网线上React Native 组件性能优化

您可能还会对下面的文章感兴趣: