zoukankan      html  css  js  c++  java
  • React爬坑秘籍(一)——提升渲染性能

    React爬坑秘籍(一)——提升渲染性能

    ##前言

    来到腾讯实习后,有幸八月份开始了腾讯办公助手PC端的开发。因为办公助手主推的是移动端,所以导师也是大胆的让我们实习生来技术选型并开发,他来做code review。之前也学习过React,当然也是非常合适这一次的开发。

    我会梳理这一个月来,自己对架构的思考过程和踩过的坑。当然这一切都不一定是最佳的,所以希望能有更多的建议和讨论。

    例子所需库:Webpack、React、Immutable。其中Webpack用于前端构建,如果不清楚的同学可以看这里:webpack前端构建体验

    ##出现场景

    一般来说,React作为一个高效的UI Library,如果合理使用是很难出现性能问题的。它内部提供了虚拟DOM搭配上Diff算法,和子组件必要的key属性,都是非常优秀的优化了绝大部分的性能。

    但是我们来模拟一个场景,在一个数组里有10000个对象,我们把这个数组的数据渲染出来后,其中一个属性用于控制页面状态。

    在这里我希望大家知道有一点就是,当父组件的状态state发生变化时,传入state的子组件都会进行重新渲染。

    下面我们来模拟一下这种情况,一起来看看。

    /**
     * Created by YikaJ on 15/9/17.
     */
    'use strict';
    var React = require("react");
    
    var App = React.createClass({
    
        getInitialState(){
            return {
                list: this.props.dataArr
            }
        },
    
        // 对数据的状态进行变更
        toggleChecked(event){
            let checked = event.target.checked;
            let index = event.target.getAttribute("data-index");
            let list = this.state.list;
            list[index].checked = checked;
    
            this.setState({list});
        },
    
        render(){
            // 将数组的数据渲染出来
            return (
                <ul>
                    {this.state.list.map((data, index)=>{
                        return (
                            <ListItem data={data}
                                index={index} key={data.name}
                                toggleChecked={this.toggleChecked}
                            />
                        )
                    })}
                </ul>
            )
        }
    });
    
    // 代表每一个子组件
    var ListItem = React.createClass({
        render(){
            let data = this.props.data;
            let index = this.props.index;
            
            // checkbox选择框是一个受限组件,用数据来决定它是否选中
            return (
                <li>
                    <input type="checkbox" data-index={index} checked={data.checked} onChange={this.props.toggleChecked}/>
                    <span>{data.name}</span>
                </li>
            )
        }
    });
    
    // 构造一个2000个数据的数组
    let dataArr = [];
    for(let i = 0; i < 2000; i++){
        let checked = Math.random() < 0.5;
        dataArr.push({
            name: i,
            checked
        });
    }
    
    React.render(<App dataArr={dataArr}/>, document.body);

    这个就是我们的有性能问题的组件。当我们去点击选框时,因为父组件的state传到了子组件的props里,我们就会遇到10000个子组件重新渲染的情况。所以表现出来的情况就是,我点一下,等个一两秒那个框才真正被勾上。我相信用户在这一秒内肯定已经关掉页面了。

    如果对React很熟悉的人,肯定知道一个生命周期的Hook,就是shouldComponentUpdate(nextProps, nextState)。这个API就是用来决定该组件是否重新Render。所以我们肯定很开心的说,只要属性的checked值不变,就不渲染呗!

    // return true时,进行渲染;false时,不渲染
    shouldComponentUpdate(nextProps, nextState){
        if(this.props.data.checked !== nextProps.data.checked){
            return true;
        }
        return false;
    }

    就这么简单么~我保存编译JSX后,迫不及待的刷新浏览器看一看了。一按
    嗯,呵呵,组件都不会渲染了...那说明this.props.datanextProps.data的数据是一致的,这怎么可能?!我明明是通过父组件的函数修改了数组然后重新setState的呀!

    修改数组......嗯,当时我就意识到这肯定又和引用类型有关。我相信大家既然能看到这里,相信基础都是有的,就是数据的基本类型和引用类型的差别,但是我还是乐意再用代码展示一次。

    // 基本类型,number boolean string undefined null
    var a = 10;
    var b = a;
    a = 12;
    console.log(b) // => 10
    
    // 引用类型,Object Function Array
    var a = [{checked: false}, {checked: true}];
    var b = a;
    a[0].checked = true;
    console.log(b) // => [{checked: true}, {checked: true}]
    

    我们明显可以看到它们的差别,我们这里着重注意一下引用类型。因为变量不再直接存值,而是变成了存指针。所以我们的每一次都同一个指针所指内存进行修改时,都会影响到拥有该指针的变量。这里当然a和b都是指的同一个对象,所以他们修改的数据也同样是同步的。

    对,我们的this.props.datanextProps.data指的是同一个东西,所以任何修改都不会让它们区分开。那这样我们是不是就要开始考虑如何进行深拷贝?

    ## 深拷贝表示只是路过打个酱油

    我们在开发过程中,既可以享受到使用引用类型的特点带来的便利,但是同时也会忍受到非常多稀奇古怪的问题,总而言之,弊大于利。

    思路其实就是将一个引用类型通过递归的方式,逐层向下取最小的基本类型,然后拼装成一样的引用类型。一看就是耗性能的主啊!如果真有这个深拷贝需求的同学,这里推荐的是lodash库的_.cloneDeep方法,它是据我所知最完善的深拷贝方法。

    当然如果你的引用类型并不复杂,例如没有函数或正则,只包含扁平化的数据时,我这里推荐一个奇淫巧计。

    var newData = JSON.parse(JSON.stringify(data));

    其实在我们这次这个案例里,就非常适合这个JSON序列化后再反序列化的方法,因为我们的数据其实也就是扁平化的。我们把它放到函数内看一下效果。

    toggleChecked(event){
        let checked = event.target.checked;
        let index = event.target.getAttribute("data-index");
        let list = JSON.parse(JSON.stringify(this.state.list));
        list[index].checked = checked;
    
        this.setState({list});
    },

    这个世界瞬间清爽多了。但是我们知道,在真正的开发过程中,不一定可以用这种奇淫巧计的,那我们除了实在没办法耗性能的deepClone,我们还能怎么办?怎么办!?

    ## Immutable Data

    Facebook自家有一个专门处理不可变数据的库,immutable-js。我们知道,React其实是非常接近函数式编程的思想的,我们可以用下面这个式子来表示React的渲染。

    UI = fRender(state, props);

    Immutable Data(不可变数据)的思想就是,不存在指向同一地址的变量,所有对Immutable Data的改变,最终都会返回一份新复制的数据,各自的数据并不会互相影响。在构建大型应用时,应该非常注意这样的数据独立性,不然你连数据在哪儿被改了你或许都不知道。那说了这么多它的概念,实际使用的时候是怎么样的?

    // 这段代码可以直接在Immutable的文档页面的控制台执行
    var arr = Immutable.fromJS([1]);
    var arr1 = arr.push(2);
    console.log(arr.toJS(), arr1.toJS()); // => [1], [1,2]  

    我们执行后,确实原有的数据已经不可变了,又新生成了一个新的不可变数据,其实这里有个非常有趣的应用场景就是撤销。不用再担心引用类型数据的变化,因为一切数据都被你把控了。

    我相信有人肯定好奇说,我每一次操作数据时都deepClone一下,也可以达到这种效果呀,这里的实现有什么不一样吗?deepClone是通过递归对象进行数据的拷贝,而Immutable数据的实现则是仅仅拷贝父节点,而其他不受影响的数据节点都是共享的用同一份数据,以大大提升性能。我们需要做的仅仅是将原生的数据转化成Immutable数据。

    我知道仅仅通过语言是很难生动表现出来的,所以找到几幅图来进行解释。


    我们需要修改某个节点的数据,这个节点用黄色标了出来。

    按照我们刚才所说的,仅对父节点进行一次数据的拷贝,我们把全新的数据拉出来,拷贝的是绿色的节点。

    而其他的节点数据其实并不受影响,所以我们可以直接使用他们的内存地址,共享一份数据。共享的数据,我们用橙色标出。

    最后我们以最优的性能得到了一份全新的数据。


    当我们在shouldComponentUpdate里判断是否更新时,变化的数据是新的引用,而不变的数据是原来的引用,这样我们就可以非常轻松的判断新旧数据的差异,从而大大提升性能。那我们知道了这个Immutable可以很好的解决我们的痛点之后,我们该如何使用到我们的实际项目中呢?其实很简单的,就是数据初始化时,就让它变成Immutable数据,然后之后对数据的操作就可以参照一下文档了,这里我直接重写了demo,其实也就是把取值和赋值做个改变,我会用注释标识出来。

    /**
     * Created by YikaJ on 15/9/17.
     */
    'use strict';
    var React = require('react');
    var Immutable = require('immutable');
    
    var App = React.createClass({
    
        getInitialState(){
            return {
                // 这里将传入的数据转化成Immutable数据
                list: Immutable.fromJS(this.props.dataArr)
            }
        },
    
        // 对数据的状态进行变更
        toggleChecked(event){
            let checked = event.target.checked;
            let index = event.target.getAttribute("data-index");
            
            // 这里不再是直接修改对象的checked的值了,而是通过setIn,从而获得一个新的list数据
            let list = this.state.list.setIn([index, "checked"], checked);
    
            this.setState({list});
        },
    
        render(){
            return (
                <ul>
                    {this.state.list.map((data, index)=>{
                        return (
                            <ListItem data={data}
                                index={index} key={index}
                                toggleChecked={this.toggleChecked}
                            />
                        )
                    })}
                </ul>
            )
        }
    });
    
    // 代表每一个子组件
    var ListItem = React.createClass({
    
        shouldComponentUpdate(nextProps){
            // 这里直接对传入的data进行检测,因为只需要检测它们的引用是否一致即可,所以并不影响性能。
            return this.props.data !== nextProps.data;
        },
    
        render(){
            let data = this.props.data;
            let index = this.props.index;
    
            // 取值也不再是直接.出来,而是通过get或者getIn
            return (
                <li>
                    <input type="checkbox" data-index={index} checked={data.get("checked")} onChange={this.props.toggleChecked}/>
                    <span>{data.get("name")}</span>
                </li>
            )
        }
    });
    
    // 构造一个2000个数据的数组
    let dataArr = [];
    for(let i = 0; i < 2000; i++){
        let checked = Math.random() < 0.5;
        dataArr.push({
            name: i,
            checked
        });
    }
    
    React.render(<App dataArr={dataArr}/>, document.body);
    

    就这样,我们非常优雅的解决了引用类型带来的问题。其实Immutable的功能并不只这些。它内部提供了非常多种的数据结构以供使用,例如和ES6一致的Set,这种特殊的数组不会存有相同的值。相信利用好不同的数据结构,会非常有利于你构建复杂应用。

    ##PureRenderMixin表示也要来打个酱油

    这里插多个React.addons内添加的东西,在我一开始探索这些性能相关问题的时候,我就注意到了这个东西。它会自行为该组件增添shouldComponentUpdate,对现有的子组件的state和props进行判断。但是它只支持基本类型的浅度比较,所以实际开发时并不能直接拿来使用。但是!我们一旦使用了Immutable数据后,比较是否是同一指针这样的事情,自然就是浅比较,所以换句话而言,我们可以使用PureRenderMixin配合上Immutable,非常优雅的实现性能提升,而且我们也不用再手动去shouldComponentUpdate进行判断。

    var React = require("react/addons");
    
    var ListItem = React.createClass({
        mixins: [React.addons.PureRenderMixin],
        
        // .....以下代码省略
    });

    ##总结

    我相信这次提供的方法,已经可以非常优雅的解决绝大部分的性能问题了。但如果还不行,那么你可能要对你的业务逻辑代码进行优化了。下一篇,我将会介绍一下React-hot-loader这一开发神器,它可以利用webpack的模块热插拔的特性,实时对浏览器的js进行无刷新的更新,非常的酷炫!我在配置它的过程中也摸了一些坑,所以希望能帮助大家跳过这个坑。相信如果能好好使用它,将会大大提升大家的开发效率。

  • 相关阅读:
    Cocos2d-x教程(34)-三维物体OBB碰撞检測算法
    POJ 2485 Highways 最小生成树 (Kruskal)
    LintCode-分糖果
    云存储市场布局已定,怎样助力企业互联网转型
    HDU 1853 Cyclic Tour(最小费用最大流)
    windows下基于bat的每1分钟执行一次一个程序
    python中匹配中文,解决不匹配,乱码等问题
    bytes,packet区别 字节数据包
    wmic
    paramiko 模块封装
  • 原文地址:https://www.cnblogs.com/yasin-yx/p/5930731.html
Copyright © 2011-2022 走看看