React渲染問題研究以及Immutable的應用

寫在前面

這裏主要介紹本身在React開發中的一些總結,關於react的渲染問題的一點研究。javascript

另外本人一直但願在React項目中嘗試使用,所以在以前已經介紹過immutable的API,能夠參看這裏Immutable平常操做之深刻API,算是對其的一個補充。css

本文全部代碼請參看github倉庫:https://github.com/Rynxiao/immutable-reacthtml

渲染房間列表

這個例子主要是寫了同時渲染1000個房間,若是我添加一個房間或者修改一個房間,在react中不一樣的實現方式下render函數將會表現出什麼樣的結果?以及針對不一樣結果的一些思考和優化。大體的列表例子以下:生成1000個不一樣的房間盒子,顏色隨機。java

rooms

項目總體目錄結構大體是這樣的:react

fileTree

下面主要來看ListDetail.js中是如何寫的:webpack

  • 父組件List
  • 子組件RoomDetail,子組件的功能只是純粹的渲染功能,自身並無任何操做

子組件:css3

// 子組件
class RoomDetail extends React.Component {

    constructor(props) {
        super(props);
    }

    render() {
        let room = this.props.room;
        
        return (
            <li 
                className="list-item" 
                style={{ backgroundColor: room.backgroundColor }}>
                { room.number }
            </li>
        );
    }

}

父組件:git

// 父組件
export default class List extends React.Component {
    
    // ...
  
    constructor(props) {
        super(props);
        this.addRoom = this.addRoom.bind(this);
        this.modifyRoom = this.modifyRoom.bind(this);
        this.state = {
            roomList: this.generateRooms(),
            newRoom: 0
        };
    }
  
    // ...

    render() {
        return (
            <div>
                <h2 className="title">React的列表渲染問題</h2>
                <div><a className="back" href="#/">返回首頁</a></div>
                <div className="btn-operator">
                    <button onClick={ this.addRoom }>Add A Room</button>
                    <button onClick={ this.modifyRoom }>Modify A Room</button>
                </div>
                <ul className="list-wrapper">
                    {
                        this.state.roomList.map((room, index) => {
                            return <RoomDetail key={ `roomDetail${index}` } room={ room } />
                        })
                    }
                </ul>
            </div>
        );
    }
}

下面咱們來添加一個房間試試github

// 添加房間
addRoom() {
    let newRoom = { number: `newRoom${++this.state.newRoom}`, backgroundColor: '#f00' };
    let newList = this.state.roomList;
    newList.push(newRoom);
    this.setState({ roomList: newList });
}

這個操做主要是生成一個新的房間,而後從state中取出當前的房間列表,而後再當前的房間列表中添加一個新的房間,最後將整個列表重新設置到狀態中。web

很顯然,此時因爲父組件的狀態發生了變化,會引發自身的render函數執行,同時列表開始從新遍歷,而後將每個房間信息從新傳入到子組件中。是的,從新傳入,就表明了子組件將會從新渲染。咱們能夠來作一個測試,在子組件的render方法中加入以下打印:

render() {
    let room = this.props.room;
    console.log(`.No${room.number}`);
  
    return (
        // ...
    );
}

不出意外的發現了全部的子組件渲染的證據:

childrenAllRender

同時利用chormePerformance檢測的信息以下:

chromeTotalBefore

調用的方法堆棧以下:

chromeFunctionBefore

渲染子組件的時間達到764ms,同時在堆棧中能夠看到大量的receiveComponentupdateChildren方法的執行。那麼有沒有什麼辦法只渲染改變的部分呢?在react官網性能監控這一小節中有提到一個方法,將子組件繼承React.PureComponent能夠局部有效防止渲染。加上以後的代碼是這樣的:

class RoomDetail extends React.PureComponent {
    // ...
}

全部的東西都沒有變化,只是將Component換成了PureComponent。下面咱們再來測試一下:

childrenOneRender

性能檢測圖以下:

chromeTotalAfter

效果出奇的好,果真只是渲染了一次,而且速度提高了10幾倍之多。

其中的原理是在組件的shouldComponentUpdate方法中進行了propsstate的比較,若是認爲他們相等,則會返回false,不然則會返回true

// react/lib/ReactComponentWithPureRenderMixin.js
var ReactComponentWithPureRenderMixin = {
    shouldComponentUpdate: function (nextProps, nextState) {
        return shallowCompare(this, nextProps, nextState);
    }
};

同時官網也說了,這只是局部有效,爲何呢?由於這些值的比較都只是淺比較,也就是隻是第一層的比較。那麼會出現什麼問題,咱們來看下面的操做:

修改其中的一個房間:

// 修改房間
modifyRoom() {
    let newList2 = this.state.roomList;
    newList2[0] = { number: 'HAHA111', backgroundColor: '#0f0' };
    this.setState({ roomList: newList2 });
}

很意外,當我添加了一個房間以後,發現第一個房間並無咱們想象中的發生變化。爲何?

緣由是我雖然修改了第一個房間的數據,當時我並無修改他的引用地址。相似下面這樣的:

var arr = [{ a: 1 }, { b: 2 }];
var arr2 = arr1;
arr2[0] = { c: 1 };
arr === arr2;   // true

所以在子組件中比較房間的時候,就會出現比較的值相等的狀況,此時將會返回false

那麼有沒有辦法改變這個問題,我找到了兩個辦法:

  • 從數據源頭入手
  • 從子組件是否渲染條件入手

從數據源頭入手,即爲改造數據,將數據進行深拷貝,使得原先的引用與新獲得的對象的引用不相同便可。關於深拷貝的實現方法有不少,我這裏貼一個,以後再仔細作研究。

// 這個函數能夠深拷貝 對象和數組
var deepCopy = function(obj){
    var str, newobj = obj.constructor === Array ? [] : {};
    if(typeof obj !== 'object'){
        return;
    } else if(window.JSON){
        str = JSON.stringify(obj), //系列化對象
        newobj = JSON.parse(str); //還原
    } else {
        for(var i in obj){
            newobj[i] = typeof obj[i] === 'object' ? 
            cloneObj(obj[i]) : obj[i]; 
        }
    }
    return newobj;
};

在ES6中提供了一種解構方式,這種方式也能夠實現數組的深層次拷貝。相似這樣的

let arr = [1, 2, 3, 4];
let arr1 = [...arr];
arr1 === arr;   // false

// caution
let arr = [{ a: 1 }, { b: 2 }];
let arr1 = [...arr];
arr1 === arr;           // false
arr1[0] = { c: 3 };
arr1[0] === arr[0];     // false
arr1[1] === arr[1];     // true

所以我把modifyRoom函數進行了如此改造:

// 修改房間
modifyRoom() {
    let newList2 = [...this.state.roomList];
    newList2[0] = { number: 'HAHA111', backgroundColor: '#0f0' };
    this.setState({ roomList: newList2 });
}

所以在比較第一個對象的時候,發現它們已經不相等了,則會從新渲染。

從子組件是否渲染條件入手,能夠不須要使用React.PureComponent,而直接在shouldComponentUpdate方法入手。由於兩次值改變以後,我清楚得能夠知道,改變的值只是第一個對象中的數值改變。那麼我能夠這麼寫來判斷:

class RoomDetail extends React.Component {

    constructor(props) {
        super(props);
    }
  
    shouldComponentUpdate(nextProps, nextState) {
        if (nextProps.room.number === this.props.room.number) {
            return false;
        } 
        return true;
    }

    render() {
        let room = this.props.room;
        
        return (
            <li 
                className="list-item" 
                style={{ backgroundColor: room.backgroundColor }}>
                { room.number }
            </li>
        );
    }

}

一樣得能夠達到效果。可是若是在shouldComponentUpdate中存在着多個propsstate中值改變的話,就會使得比較變得十分複雜。

應用Immutable.js來檢測React中值的變化問題

在官網上來講,immutable提供的數據具備不變性,被稱做爲Persistent data structure,又或者是functional data structure,很是適用於函數式編程,相同的輸入總會預期到相同的輸出。

2.1 immutable的性能

immutable官網以及在知乎中談到爲何要使用immutable的時候,會看到一個關鍵詞efficient。高效地,在知乎上看到說是性能十分好。在對象深複製、深比較上對比與Javascript的普通的深複製與比較上來講更加地節省空間、提高效率。我在這裏作出一個實驗(這裏我並不保證明驗的準確性,只是爲了驗證一下這個說法而已)。

實驗方法:我這裏會生成一個對象,對象有一個廣度與深度,廣度表明第一層對象中有多少個鍵值,深度表明每個鍵值對應的值會有多少層。相似這樣的:

{
  "width0": {"key3": {"key2": {"key1": {"key0":"val0"}}}},
  "width1": {"key3": {"key2": {"key1": {"key0":"val0"}}}},
  "width2": {"key3": {"key2": {"key1": {"key0":"val0"}}}},
  // ...
  "widthN": {"key3": {"key2": {"key1": {"key0":"val0"}}}}
}

所以實際上在javascript對象的複製和比較上,須要遍歷的次數實際上是width * deep

在複製的問題上,我作了三種比較。

  • deepCopy(obj)
  • JSON.parse(JSON.stringify(obj))
  • Immutable

最終獲得的數據爲:

deepCopy( μs ) JSON( μs ) Immutable( μs )
20 * 50 4000 9000 20
20 * 500 8000 10000 20
20 * 5000 10000 14000 20

在比較上,我只比較了兩種方式:

  • javascript deep compare
  • Immutable.is

代碼以下:

let startTime1 = new Date().getTime();
let result1 = Equals.equalsObject(gObj, deepCopyObj);
let endTime1 = new Date().getTime();
console.log(result1);
console.log(`deep equal time ${(endTime1-startTime1)*1000}μs`);

let startTime2 = new Date().getTime();
let result2 = is(this.state.immutableObj, this.state.aIObj);
let endTime2 = new Date().getTime();
console.log(result2);
console.log(`immutable equal time ${(endTime2-startTime2)*1000}μs`);

最終獲得的數據爲:

deepCompare( μs ) Immutable.is( μs )
20 * 5 0 7000
20 * 50 1000 27000
20 * 500 6000 24000
20 * 5000 84000 5000

數據的設計上可能太過單一,沒有涉及到複雜的數據,好比說對象中再次嵌套數組,而且在每個鍵值對應的值得廣度上設計得也太過單一,只是一條直線下來。可是當數據量達到必定的程度時,其實也說明了一些問題。

總結:

  1. 對象複製上來講,基本上Immutable能夠說是零消耗
  2. 對象比較上,當對象深層嵌套到必定規模,反而Immutable.is()所用的時間會更少
  3. 可是在數據方面來講,Immutable並快不了多少

固然只是測試,平時中的縱向嵌套達到三層以上都會認爲是比較恐怖的了。

因而我去google翻了翻,看看有沒有什麼更好的demo,下面我摘錄一些話。

What is the benefit of immutable.js?

Immutable.js makes sure that the "state" is not mutated outside of say redux. For smaller projects, personally i don't think it is worth it but for bigger projects with more developers, using the same set of API to create new state in reduce is quite a good idea

It was mentioned many times before that Immutable.js has some internal optimizations, such as storing lists as more complex tree structures which give better performance when searching for elements. It's also often pointed out that using Immutable.js enforces immutability, whereas using Object.assign or object spread and destructuring assignments relies to developers to take care of immutability. EDIT: I haven't yet seen a good benchmark of Immutable.js vs no-library immutability. If someone knows of one please share. Sharing is caring :)

Immutable.js adds two things: Code enforcement: by disallowing mutations, you avoid strange errors in redux and react. Code is substantially easier to reason about. Performance: Mutation operations for larger objects are substantially faster as the internals are a tree structure that does not have to copy the entirety of an object every assignment. In conclusion: it's a no brainer for decently scoped applications; but for playing around it's not necessary.

https://github.com/reactjs/redux/issues/1262

yes, obviously mutable is the fastest but it won't work with how redux expects the data, which is immutable

Performance Tweaking in React.js using Immutable.js

But wait… This is can get really ugly really fast. I can think of two general cases where your shouldComponentUpdate can get out of hand.

// Too many props and state to check!

  shouldComponentUpdate(nextProps, nextState) {
    return (
      this.props.message !== nextProps.message ||
      this.props.firstName !== nextProps.firstName ||
      this.props.lastName !== nextProps.lastName ||
      this.props.avatar !== nextProps.avatar ||
      this.props.address !== nextProps.address ||
      this.state.componentReady !== nextState.componentReady
      // etc...
    );
  }

是的,我並無得出Immutable在性能上必定會很快的真實數據。可是不得不提到的是他在配合Redux使用的時候的一個自然優點——數據是不變的。而且在最後一個連接中也提到,在配合React使用中經過控制shouldComponentUpdate來達到優化項目的目的。

however,Let's write some examples about immutable used in react to make sense.

2.2 房間列表加入Immutable

在父組件中的改變:

constructor(props) {
    super(props);
    this.addRoom = this.addRoom.bind(this);
    this.modifyRoom = this.modifyRoom.bind(this);
    this.state = {
        // roomList: this.generateRooms()
        roomList: fromJS(this.generateRooms()),
        newRoom: 0
    };
}

addRoom() {
    // let newRoom = { number: `newRoom${++this.state.newRoom}`, backgroundColor: '#f00' };
    // let newList = this.state.roomList;
    // newList.push(newRoom);
    let newRoom = Map({ number: `newRoom${++this.state.newRoom}`, backgroundColor: '#f00' });
    let newList = this.state.roomList.push(newRoom);
    this.setState({ roomList: newList });
}

modifyRoom() {
    // let newList = [...this.state.roomList];
    // newList[0] = { number: 'HAHA111', backgroundColor: '#0f0' };
    let list = this.state.roomList;
    let newList = list.update(0, () => {
        return Map({ number: 'HAHA111', backgroundColor: '#0f0' });
    });
    this.setState({ roomList: newList });
}

子組件中:

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

將數據源用Immutable初始化以後,以後再進行的數據改變都只要遵照ImmutableJS的相關API便可,就能夠保證數據的純淨性,每次返回的都是新的數據。與源數據的比較上就不可能會存在改變源數據相關部分以後,因爲引用相等而致使數據不相等的問題。

3、在Redux中運用immutable

我在項目底下新建了一個項目目錄redux-src,同時在項目中增長了熱更新。新建了webpack.config.redux.js,專門用來處理新加的redux模塊。具體代碼能夠上github上面去看。所以新的目錄結構以下:

redux-tree

webpack.config.redux.js文件以下:

'use strict';
var webpack = require("webpack");
var ExtractTextPlugin = require("extract-text-webpack-plugin");  //css單獨打包

module.exports = {
    devtool: 'eval-source-map',

    entry: [
        __dirname + '/redux-src/entry.js', //惟一入口文件
        "webpack-dev-server/client?http://localhost:8888",
        "webpack/hot/dev-server"
    ],
    
    output: {
        path: __dirname + '/build', //打包後的文件存放的地方
        filename: 'bundle.js',      //打包後輸出文件的文件名
        publicPath: '/build/'
    },

    module: {
        loaders: [
            { test: /\.js$/, loader: "react-hot!jsx!babel", include: /src/},
            { test: /\.css$/, loader: ExtractTextPlugin.extract("style", "css!postcss")},
            { test: /\.scss$/, loader: ExtractTextPlugin.extract("style", "css!postcss!sass")},
            { test: /\.(png|jpg)$/, loader: 'url?limit=8192'}
        ]
    },

    postcss: [
        require('autoprefixer')    //調用autoprefixer插件,css3自動補全
    ],

    plugins: [
        new ExtractTextPlugin('main.css'),
        new webpack.HotModuleReplacementPlugin()
    ]
}

在項目中運行npm run redux,在瀏覽器輸入localhost:8888便可看到最新的模塊。

這裏關於如何在react中使用redux,這裏就很少說了,若是不明白,能夠去看 http://cn.redux.js.org/或者到我以前寫的 redux的一個小demo中去看。

重點說說如何在reducer中使用Immutable,以及在List.js中如何經過發送Action來改變store

redux-src/redux/reducers/index.js

import { fromJS } from 'immutable';
import { combineReducers } from 'redux';

import { ADD_ROOM, MODIFY_ROOM, MODIFY_NEWROOM_NUM } from '../const';
import { addRoom, modifyRoom, modifyNewRoomNum } from '../actions';

// ... generateRooms()

const initialState = fromJS({
    roomList: generateRooms(),
    newRoom: 0
});

function rooms(state = initialState, action) {
    switch(action.type) {
        case ADD_ROOM: 
            return state.updateIn(['roomList'], list => list.push(action.room));
        case MODIFY_ROOM:
            return state.updateIn(['roomList', 0], room => action.room);
        case MODIFY_NEWROOM_NUM:
            return state.updateIn(['newRoom'], num => ++num);
        default:
            return state;
    }
}

export default combineReducers({
    rooms
});

跟以前List.js中的state中聲明的最開始狀態同樣。這裏依舊維持一個最開始的房間列表以及一個新增房間的序號數。只不過這裏的最初狀態是經過Immutable.js處理過的,因此在reducer中的全部操做都必須按照其API來。

redux-src/components/List.js

其實這個文件也沒有做多處修改,基本能夠看引入了immutablestate管理的Detail.js。只是在操做上顯得更加簡單了。

addRoom() {
    let { newRoom, onAddRoom, onModifyRoomNum } = this.props;
    let room = Map({ number: `newRoom${newRoom}`, backgroundColor: '#f00' });
    onAddRoom(room);
    onModifyRoomNum();
}

modifyRoom() {
    let { onModifyRoom } = this.props;
    let room = Map({ number: 'HAHA111', backgroundColor: '#0f0' });
    onModifyRoom(room);
}

監控圖

運用Redux-DevTools工具能夠清楚地看出當前redux中的數據變化,以及操做。

日誌模式:

reduxDevToolsLog
監控模式:

reduxDevToolsInspector

總結

運用redux的好處就是全局數據可控。在redux中運用immutable data也是redux所提倡的,咱們再也不會由於值沒有深拷貝而找不到值在何處什麼時候發生了變化的狀況,接而引起的就是組件莫名其妙地不會re-render,同時因爲immutable.js在值複製上的高效性,所以在性能上來講,會比用傳統javascript中的深拷貝上來講提高會不少。

相關文章
相關標籤/搜索