React总结篇之五_React组件的性能优化

一、单个React组件的性能优化
React利用Virtual DOM来提高渲染性能,虽然这能将每次DOM操作量减少到最小,计算和比较Virtual DOM依然是一个复杂的计算过程。如果能够在计算Virtual DOM之前就能判断渲染结果不会有变化,那样可以干脆不要进行Virtual DOM计算和比较,速度就会更快。

  1. 发现浪费的渲染时间
    在Chrome浏览器中安装React Perf扩展,步骤省略(属于操作部分)

  2. 性能优化的时机
    “我们应该忘记忽略很小的性能优化,可以说97%的情况下,过早的优化是万恶之源,而我们应该关心对性能影响最关键的那另外3%的代码” --高德纳
    对于合并多个字符串,怎样合并,使用什么方法合并不大可能对整个应用造成关键的性能影响,这就是高纳德所说的97%的情况,而选择用什么样的方式去定义组件的接口,如何定义state到prop的转变,使用什么样的算法来比对Virtual DOM,这些决定对性能和架构的影响是巨大的,就是那关键的3%。

  3. React-Redux的shouldComponentUpdate的实现
    使用React-Redux,一个典型的React组件代码文件最后一个语句代码是这样的:
    export default connect(mapStateToProps)(mapDispatchToProps)(Foo)
    以上,connect过程中实际上产生了一个无名的React组件类,这个类定制了shouldComponentUpdate的实现,实现逻辑是比对这次传递给内层傻瓜组件的props和上次的props,如果相同那就没必要重新渲染了,可以返回false,否则就要返回true。
    但是,我们需要了解一下shouldComponentUpdate的实现方式,shouldComponentUpdate在比对prop和上次渲染所用的prop方面,依然用的是尽量简单的方法,做的是所谓的“浅层比较”。简单来说就是用JavaScript的===的操作符来比较,如果prop的类型是字符串或者数字,只要值相同,那么“浅层比较”也会认为二者相同,但是,如果prop的类型是复杂对象,那么“浅层比较”的方式只看这两个prop是不是同一个对象的引用,如果不是,哪怕这两个对象中的内容完全一样,也会被认为是两个不同的prop。
    比如,在JSX中使用组件Foo的时候给名为style的prop赋值,代码如下:
    <Foo style={{color:"red"}} />
    像上面这样的使用方法,Foo组件利用React-Redux提供的shouldComponentUpdate函数实现,每一次渲染都会认为style这个prop发生了变化,因为每次都会产生一个新的对象给style,而在“浅层比较”中,只比较第一层,不会去比较对象里面是不是相等。那为什么不用深层比较呢?因为一个对象到底有多少层无法预料,如果递归对每个字段都进行“深层比较”,不光代码更复杂,也可能会造成性能问题。
    上面的例子应该改成下面这样:
    const fooStyle = {color:"red"} //确保这个初始化只执行一次,不要放在render中
    <Foo style={fooStyle} />
    同样的情况也存在与函数类型的prop,React-Redux无从知道两个不同的函数是不是做着同样的事,要想让它认为两个prop是相同的,就必须让这两个prop指向同样一个函数,如果每次传给prop的都是一个新创建的函数,那肯定就没法让prop指向同一个函数了。
    看TodoList传递给TodoItem的onToggle和onRemove,在JSX中代码如下:
    onToggle = {()=>onToggleTodo(item.id)}
    onRemove = {()=>onRemoveTodo(item.id)}
    这里赋值给onClick的是一个匿名的函数,而且是在赋值的时候产生的。也就是说,每次渲染一个TodoItem的时候,都会产生一个新的函数,这就是问题所在。办法就是不要让TodoList每次都传递新的函数给TodoItem。有两种解决方式。
    (1)第一种方式,TodoList保证传递给TodoItem的onToggle永远只能指向同一个函数对象,这是为了应对TodoItem的shouldComponentUpdate的检查,但是因为TodoItem可能有多个实例,所以这个函数要用某种方法区分什么TodoItem回调这个函数,区分的办法只能通过函数参数。
    在TodoList组件中,mapDispatchToProps产生的prop中onToggleTodo接受TodoItem的id作为参数,恰好胜任这个工作,所以,可以在JSX中代码改为下面这样:
    <TodoItem
    key=em.id
    id=em.id
    text=em.text
    completed=em.completed
    onToggle={onToggleTodo}
    onRemove={onRemoveTodo}
    />
    注意,除了onToggle和onRemove的值变了,还增加了一个新的prop名为id,这是让每个TodoItem知道自己的id,在回调onToggle和onRemove时可以区分不同的Todo-Item实例。
    TodoList的代码简化了,但是TodoItem组件也要做对应改变,对应TodoItem组件的mapDispatchToProps函数代码如下:
    const mapDispatchToProps = (dispatch,ownProps) =>({
    onToggleItem : () => ownProps.onToggle(ownProps.id)
    });
    mapDispatchToProps这个函数有两个参数dispatch和ownProps,也就是父组件渲染当前组件时传递过来的props,通过访问ownProps.id就能够得到父组件传递过来的名为id的prop值。
    上面的mapDispatchToProps这个函数给TodoItem组件增加了名为onToggleItem的prop,调用onToggle,传递当前实例的id作为参数,在TodoItem的JSX中就应该使用onToggleItem,而不是直接使用TodoList提供的onToggle。
    (2)第二种方式,干脆让TodoList不要给TodoItem传递任何函数类型prop,点击事件完全由TodoItem组件自己搞定。
    在TodoList组件的JSX中,渲染TodoItem组件的代码如下:
    <TodoItem
    key = em.id
    id = em.id
    text = em.text
    completed = em.completed
    />
    可以看到不需要onToggle和onRemove这些函数类型的prop,但依然有名为id的prop。
    在TodoItem组件中,需要自己通过react-redux派发action,需要改变的代码如下:
    const mapDispatchToprops = (dispatch,ownProps) = >{
    const id = ownProps.id;
    return {
    onToggle : () => dispatch(toggleTodo(id)),
    onRemove : () => dispatch(removeTodo(id))
    }
    };
    对比这两种方式,看一看到无论如何TodoItem都要使用react-redux,都需要定义产生定制prop的mapDispatchToProps,都需要TodoList传入一个id,区别只在于actions是由父组价导入还是组件自己导入。
    相比而言,没有多大必要让action在TodoList导入然后传递一个函数给TodoItem,第二种让TodoItem处理自己的一切事物,更符合高内聚的要求。

二、多个React组件的性能优化
和单个组件的生命周期一样,React组件也要考虑3个阶段:装载阶段、更新阶段、卸载阶段。其中,装载阶段基本没什么可以优化的空间,因为这部分工作没有什么可以省略的。而卸载阶段,只有一个生命周期函数componentWillUnmount,这个函数做的事情只是清理componentDidMount添加的事件处理监听等收尾工作,做的事情要比装载过程少很多,所以也没什么可以优化的空间。所以值得关注的过程,只剩下更新过程。

  1. React的调和过程
    React在更新阶段,很巧妙的对比原有的Virtual DOM和新生成的Virtual DOM(存在于内存中),找出两者的不同,根据不同修改DOM树,这样只需做最小的必要改动。
    React在更新中找不同的过程,就叫做调和(Reconciliation)。
    React实际采用的算法的时间复杂度是O(N)。React的Reconciliation算法并不复杂,当React要对比两个Virtual DOM的树形结构的时候,从根节点开始递归往下对比,在树形结构上,每个节点都可以看做这个节点以下子树部分的根节点,所以其实这个对比算法可以从Virtual DOM上的任何一个节点开始执行。
    React首先检查两个根节点的类型是否相同,根据相同或者不同有不同处理方式。
    (1)节点类型不同的情况
    这时可以直接认为原来的树形结构已经没用,需要重新构建新的DOM树,原有树形上的React组件会经历“卸载”的生命周期。这时,componentWillUnmount的方法会被调用,取而代之的组件则会经历装载过程的生命周期,组件的componentWillMount、render和componentDidMount方法会被依次调用。
    (2)节点类型相同的情况
    这时React就会认为原来的根节点只需要更新,不必将其卸载,也不会引发根节点的重新装载。
    这时,有必要区分一下节点的类型,节点的类型可以分为两类:一类是DOM元素类型,对应的就是HTML直接支持的元素类型,比如<div />,<span />和<p />;另一类是React组件,也就是利用React库定制的类型。
    • 对于DOM元素类型,React会保留节点对应的DOM元素,只对树形结构根节点上的属性和内容做一下对比,然后只更新修改的部分。
    • 对于React组件类型,React会根据新节点的props去更新原来根节点的props实例,引发这个组件实例的更新过程,也就是按照顺序引发下列函数:
      shouldComponentUpdate
      componentWillReceiveProps
      componentWillUpdate
      render
      componentDidUpdate
      在处理完根节点的对比之后,React的算法会对根节点的每个子节点重复一样的动作,这时候每个子节点就会成为它所覆盖部分的根节点,处理方式和它的父节点完全一样。
      (3)多个子组件的情况
      当一个组件包含多个子组件,React的处理方式也非常的简单直接。
      React总结篇之五_React组件的性能优化
      React发现多了一个TodoItem,会创建一个新的TodoItem组件实例,这个TodoItem组件实例需要经历装载过程,对于前两个TodoItem实例,React会引发它们的更新过程。
      上面的例子是TodoItem序列后面增加了一个新的TodoItem实例,接下来在TodoItem序列前面增加一个TodoItem实例,代码如下:
      React总结篇之五_React组件的性能优化
      像上面新的TodoItem实例插入在第一位的例子中,React会首先认为把text为First的TodoItem组件实例的text改成了Zero,text为Second的TodoItem组件实例的text改成了First,在后面多出了一个TodoItem组件实例,text内容为Second。这样操作的后果就是,现存的两个TodoItem实例的text属性被改变了,强迫它们完成了一个更新过程。React提供了方法来克服这种浪费,但需要开发人员在写代码的时候提供一点帮助,这就是key的作用。

猜你喜欢

转载自blog.51cto.com/chengyanli/2313096