美文网首页
05|React组件的性能优化

05|React组件的性能优化

作者: 井润 | 来源:发表于2019-12-13 18:02 被阅读0次

    本文主要是针对性能优化做介绍,对应的React本身性能就比较不错!要介绍的点如下:

    • 单个React组件的性能优化
    • 多个React组件的性能优化
    • 利用reselect提高数据选取的性能

    因为现代化的Web应用的开发都是 组件化开发模式 ,因此我们不可能站在全局的角度去优化,而是针对Web应用的核心部分,也就是 组件 针对性的进行优化操作!

    01|单个React组件性能优化

    • Virtual DOM帮助React组件提高渲染性能

      • 虽然每次都是重新渲染,但是利用React最核心的功能,DIFF机制进行diff之后 渲染该渲染的部分!
      • 虽然说每次的DOM操作量比较少,但是计算和比较的时候依然是一个比较复杂的过程! 这样一来就可以知道,在使用Virtual DOM之前就确认渲染结果不会有变化! 提升性能的方法不是diff而是不渲染甚至是不进行Diff!
    • 对性能进行检测,对对应的部分进行针对性的优化!

    • 避免过早优化,我们应该对性能影响最关键的代码进行优化!

    • 使用提供的 shouldComponentUpdate 手动操作组件是否渲染! 更加细粒度的控制组件的渲染逻辑!

    • react-redux提供的 shouldComponentUpdate 对比prop和上一次使用到的prop上面,用的尽量简单的方法! 也只是对应的浅层比较,如果说对应的porp值是复杂的对象,只看是不是同一对象的引用,如果不是,哪怕这两个对象的内容完全一样,也会被认为两个不同的prop!

    为什么不使用深层比较呢?

    • 一个对象到底有多少层是无法预料的,如果使用递归对每个字段进行深层比较,让代码复杂的同时,也让性能比较低下!

    • 对应的props如果说是对象类型的话, 想让react-redux认为前后的对象类型prop是相同的,就必须要保证只想的同一对象!

      • 比如说是style属性 可以使用一个对象对样式进行设置,那么就可以将对应的 样式进行抽象成一个变量进行使用!

    对应的 关于事件的处理,如果说每次都是以箭头函数的方式进行处理的话, 每次都是产生匿名函数! 匿名函数也就是每次都是新的函数 因此如果涉及到组件被应用到 多个页面中内存的占用就非常高!

    对应的我们通过具体的代码可以很清楚的复现这个知识点:

    import React,{PropTypes} from "react";
    import {toogleTodo,removeTodo} from "../actions";
    const TodoList = ({todos,onToggleTodo,onRemoveTodo})=>{
        return (
            <ul className='todo-list'>
                todos.map((item)=>{
                    <TodoItem key={item.id} text={item.text} completed={item.completed} 
                        onToggle={()=>onToogleTodo(item.id)}
                        onRemove={()=>onRemoveTodo(item.id)} />
                })
            </ul>
        );
    }
    

    对应的TodoItem点击了对应的按钮,调用父类的回调函数,父类产生一个新的TodoItem 对应的每次更新都躲不过重新渲染的命运!

    02|多个React组件的性能优化

    多个React组件的优化,首先就需要学会从React的生命周期入手,从对应的生命周期阶段针对性的去优化!

    render=>start: render method
    op1=>operation: VDOM Tree
    op2=>operation: RC or DOM
    end=>end: View
    action=>inputoutput: Action
    render->op1->op2->end->action->op1
    
    • render方法在内存中产生了树形的结构(Virtual DOM)
    • 树上的每一个节点表示: React组件或者说是原生的DOM元素
    • React 根据 Virtual DOM渲染浏览器中的DOM树!

    如果说用户的交互出发了页面的更新,网页中需要更新页面的话,React 依然通过render方法获得了一个树形结构VirtualDOM 这个时候不能完全和装载过程一样直接使用VirtualDOm去产生DOM树!

    • 这个阶段(更新阶段)巧妙的对比原有的Virtual DOM和新生成的Virtual DOM找出两者的不同之处,根据不同来更新DOM! 只做必要的最小的改动!

    • 对应的这个找不同的过程叫做 Reconciliation 调和!

    对照计算机科学目前的算法研究成果,比对N个节点的树形结构的算法,时间复杂度是O(N^3) 如果说比对两个100节点的DOM树需要计算 100*100*100 次 其中的算法复杂度 N表示节点数,

    但是对应的React实际采用的算法需要的时间复杂度为O(N) ,对比两个树形怎么着都要比对两个树形上的节点!

    也不存在比O(N)复杂度更低的算法

    对应的调和过程:

    • 节点类型不同的情况
      • React会直接丢弃原来树形结构,然后重建DOM树,对应的React组件也会经历卸载的生命周期
      • 面对不同的应用场景,可能会引发树结构某些组件的卸载和装载过程!
    • 节点类型相同的情况
      • 如果两个数形结构的根节点类型相同,React就认为原来的根节点只需要更新过程,不会将其卸载,也不会引发根节点的重新装载!
        • 对应的节点类型分为两种:一种是DOM元素对应的也就是所谓的HTML元素,另一种则是 React的组件,也就是利用React库定制的类型!

    对应的如果说属性结构的根节点不是DOM元素,那就只可能是React组件类型,那么React做的工作类似,React此时也不知道如何更新DOM树,因此逻辑还在React组件之中,React能做的也就是通过新节点的props更新原来的组件实例,引发组件实例更新的过程! 按照顺序触发:

    1. shouldComponentUpdate 如果不需要更新的话,可以在函数中直接返回false来保持最大的性能!
    2. componentWillReceiveProps
    3. componentWillUpdate
    4. render
    5. componentDidUpdate
    01|如果说多个子组件的情况是怎么样的呢?

    我们那一个最简单的TodoItem来举例吧:

    <ul>
        <TodoItem text="first" completed={false}  />
        <TodoItem text="second" completed={false} />
    </ul>
    

    在更新之后,用JSX表示是这样:

    <ul>
        <TodoItem text="first" completed={false}  />
        <TodoItem text="second" completed={false} />
        <TodoItem text="Thrid" completed={false} />
    </ul>
    

    对应的结果就是,react检查多出了一个TodoItem,创建一个新的TodoItem组件实例,该实例需要经历装载的过程,但是对于前面两个TodoItem实例,React会引发他们的更新过程! 但是如果说对应的shouldComponentUpdate函数实现恰当,props检查之后就返回false之后,可以避免实质的更新操作!

    • 刚刚那样是在后面加了一个TodoItem实例,如果说在前面加的话又会出现什么问题呢?
    <ul>
        <TodoItem text="zero" completed={false}  />
        <TodoItem text="first" completed={false} />
        <TodoItem text="second" completed={false} />
    </ul>
    

    和之前的代码实例相比,此时的React会这么处理:

    • 首先认为把text从first改为了zero
    • second改为了first
    • 最后多出了一个TodoItem实例内容为second

    现存(first,second)的两个实例的属性被改变了,强迫他们完成了一个更新! 虽然这种情况只是改变了2个组件的属性,如果说有一百个TodoItem实例的话,明显就是一个浪费!

    后面React提供了方法来克服这种浪费,于是有了key

    02|Key的用法

    React中,需要确定每一个组件在组件序列中的唯一标识就是它的位置! 因此React本身也不懂哪些子组件实质上面没有改变! key就是每一个组件的唯一标识!

    <ul>
        <TodoItem key={1} text="zero" completed={false}  />
        <TodoItem key={2} text="first" completed={false} />
        <TodoItem key={3} text="second" completed={false} />
    </ul>
    
    • key的值需要保证唯一性
    • 通过key的使用配合shouldComponentUpdate就能够一定程度上面提高性能!
    03|使用reselect提高数据获取性能

    对应的除了通过优化渲染过程来提高性能,既然React和Redux都是通过数据驱动渲染过程,除了渲染过程,获取数据的过程也是一个需要考虑的优化点!

    通过mapStateToProps函数从Redux store提供的state中产生渲染需要的数据,对应的代码如下所示:

    const selectVisibleTodos = (todo,filter)=>{
        switch(filter){
            case FilterType.ALL:
                return todos;
            case FilterType.COMPLETED:
                return todos.filter(item=>item.completed);
            case FilterTypes.UNCOMPPLETED:
                return todos.filter(item=>!item.completed);
            default:
                throw new Error("unsupport filter!");
        }
    }
    const mapStateToProps = state=>{
        return {todos:selectVisibleTodos(state.todos,state.filter)};
    }
    

    Redux Store上获取数据的重要一环,mapStateToProps函数一定要快,从代码来看,运算本身没有什么课优化的空间,

    获取对应的待办事项,需要通过对应的todos和filter两个字段的值计算出来! 计算过程需要遍历todos字段上的数组,数组比较大的时候,TodoList组件的每一次重新渲染都需要重新计算一遍,负担就会过重!

    • 两阶段选择过程

    对应的selectVisibleTodos函数的计算必不可少,那么对应的如何优化呢?

    并不是每一次TodoList的渲染都需要执行selectVisibleTodos中的计算过程,如果对应的Redux Store状态树上的待办事项的todos字段没有变化,而代表当前过滤器的filter字段也没有变化,实在没有必要重新渲染todos数组来计算一个新的结果! 如果说上一次的结果能够被缓存过来的话,那么就重用缓存就行了!

    reselect库的工作原理就是,只要相关状态没有改变的话,那就直接重用上一次的缓存!

    reselect库被用来创造 选择器:接受一个state作为参数,并且通过选择器返回的函数的数据就是我们某个mapStateToProps需要的结果! 但是选择器不是纯函数,一种有记忆力的函数,运行选择器函数会有副作用!

    • 通过输入参数state抽取第一层结果,降低一层结果与之前的结果进行比对,如果完全相同没必要进行比对,这一部分的比较,就是JavaScript中的全等操作符比较! 如果是对象且是同一个对象才会被认为是相同! 否则进入到下一步
    • 接下来的就是确定选择器步骤一和步骤二分别进行什么计算,原则很简单:
      • 步骤一:尽量快,运算非常简单的,最好就是一个映射运算 通常是state参数中某个字段的引用!
      • 之后的活交给第二步去计算!

    对上面的代码进行改造就需要使用 reselect 库:

    import {createSelector} from "reselect";
    import {FilterTypes} from "../constants.js";
    const selectVisibleTodos = createSelector([getFilter,getTodos],(filter,todos)=>{
        switch(filter){
            case FilterTypes.ALL:
                return todos;
            case FilterTypes.COMPLETED:
                return todos.filter(item=>item.completed);
            case FilterTypes.UNCOMPLETED:
                return todos.filter(item=>!item.completed);
            default:
                throw new Error("unsupport filter!");
        }
    })
    const getFilter = state=> state.filter;
    const getTodos = state=> state.todos;
    
    import {selectVisibleTodos}  from "../selector.js";
    const mapStateToProps = state=>{
        return {todos:selectVisibleTodos(state)};
    }
    

    这样一来虽然说createSelector接受的所有函数都是为纯函数,但是选择器有记忆的副作用,只要对应的state没有变化自然输出也没有变化!

    只要是Redux store状态树上的filter和todos字段不变的话,怎么触发TodoList的渲染过程,都不会触发遍历todos字段的计算,性能自然更快!

    • 状态树的设计尽量的范式化,按照一定的设计规约he关系型数据库的设计原则,减少数据的冗余!(数据冗余造成的后果就是难以保证数据的一致性!)

    相关文章

      网友评论

          本文标题:05|React组件的性能优化

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