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

寫在前面

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

另外本人一直希望在React項目中嘗試使用,因此在之前已經介紹過immutable的API,可以參看這裏Immutable日常操作之深入API,算是對其的一個補充。

本文所有代碼請參看github倉庫:https://github.com/Rynxiao/immutable-react

渲染房間列表

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

rooms

項目整體目錄結構大致是這樣的:

fileTree

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

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

子組件:

// 子組件
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>
        );
    }

}

父組件:

// 父組件
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>
        );
    }
}

下面我們來添加一個房間試試

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

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

很顯然,此時由於父組件的狀態發生了變化,會引起自身的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 * 500 8000 10000
20 * 5000 10000 14000

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

  • 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
20 * 50 1000
20 * 500 6000
20 * 5000 84000

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

總結:

  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即可,就可以保證數據的純淨性,每次返回的都是新的數據。與源數據的比較上就不可能會存在改變源數據相關部分之後,由於引用相等而導致數據不相等的問題。

三、在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中的深拷貝上來說提升會很多。

發佈了60 篇原創文章 · 獲贊 29 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章