React 性能優化 之 React + Redux + immutable 最佳實踐(避免重複渲染)

1. 爲何要在React中使用immutable

React經常使用的繼承實現方式有兩種,React.Component和 React.PureComponent。javascript

  • PureComponent類幫咱們以淺比較的方式對比props和state,實現了shouldComponentUpdate()函數,提高性能。
  • Component:只要值改變,組件就會從新渲染,致使了無效的渲染。下降了性能
import * as React from 'react';
type State = {
    a: {x: number, y: number};
    b: Array<{x: number, y: number}>;
};
class Test extends React.Component<{}, State>{
    constructor(props: any) {
        super(props);
        this.state = {
            a: {x: 1, y: 2},
            b: [{x: 1, y: 2}, { x: 1, y: 2}],
        };
    }
    changeArr = () => {
        const { b } = this.state;
        b[0] = Object.assign({}, b[0], {x: (b[0].x + 1) });
        this.setState({
            b: b,
        });
    };
render() {
    const { b, a } = this.state;
    return (
        <div> <div>一個對象的內容顯示:</div> <RenderAComponent a={a}/><br/><br/><br/> <div>一個List(能夠考慮爲table的一條一條的數據)的內容顯示</div> <button onClick={this.changeArr}>pop</button> <RenderListComponent b={b}/> </div>
    );
}
}
function RenderList({b}: {b: State['b']}) {
    return (
        <ul> { b.map((item: {x: number, y: number}, key) => <ListItemComponent key={key} item={item}/>) } </ul>
    );
}

function ListItem({item}: {item: {x: number, y: number}}) {
    return <li >{item.x} --- {item.y}</li>;
}
function RenderA({a}: {a: State['a']}) {
    const { x, y } = a;
    return <div>{x} ----- <span>{y}</span></div>;
}
const RenderListComponent = RenderList;
const ListItemComponent = ListItem;
const RenderAComponent = RenderA;
export default Test;
複製代碼

咱們使用React-dev-tools工具,咱們期待的是RenderList被修改, 可是RenderA都修改了。
html


經過下面修改部分代碼, 便避免重複渲染。前端

class Test extends React.PureComponent<{}, State>{
  // .....
}

changeArr = () => {
    const { b } = this.state;
    let _b = Object.assign([], b);
    _b[0] = Object.assign({}, _b[0], {x: (_b[0].x + 1) });
    this.setState({
        b: _b,
    });
};
const RenderListComponent = React.memo(RenderList);
const ListItemComponent = React.memo(ListItem);
const RenderAComponent = React.memo(RenderA);
複製代碼

RenderA 沒有渲染,RenderList的值被從新渲染。
java


2. Object.assign 實現不可變

在上面的例子中,咱們使用了Object.assign來實現不可變性。其實徹底能夠不適用immutable。react

let _b = Object.assign([], b);
_b[0] = Object.assign({}, _b[0], {x: (_b[0].x + 1) });
複製代碼

可是他有缺點:
redux

  • 當對象屬性龐大時效率較低。好比擁有 100,000 個屬性的對象,這個操做耗費了 134ms。性能損失主要緣由是 「結構共享」 操做須要遍歷近10萬個屬性,而這些引用操做耗費了100ms以上的時間。(實際咱們前端數據容量不會達到這個級別)
  • 深層次對象的賦值書寫起來很麻煩, 咱們在處理數據時須要一遍一遍去拷貝(這是使用immutable的最主要的緣由)

3. 什麼是immutable

immutable.js 使用樹的方式實現了 持久化數據結構,保證了對象是都是不可變的。任何添加,刪除,修改等都是對生成一個新的對象。而且經過結構共享等方式大幅提升性能。例以下面的結構,但願在g節點下增長一個h節點
後端

  • 第一種方式:從新生成一棵樹,每一個節點都是新的,浪費空間和時間。


  • immutable: 新生成根節點,而後對修改的部分進行父節點追蹤,全部相關節點都從新生成。而不相關的節點保留,實現告終構共享。


4. Immutable作了哪些優化

數據的存儲(重點Vector Trie, 空間換時間), juejin.cn/post/684490…
如何壓縮空間作優化(樹的高度壓縮, BitMap節點內部壓縮). juejin.cn/post/684490…
api



5. Immutable 缺點如何避免

  • 問題:因爲實現了完整的不可變數據,immutable.js的體積過於龐大,尤爲在移動端這個狀況被凸顯出來。
使用緩存中間件,例如reselect
複製代碼
  • 問題:全新的api+不友好的文檔。
經過使用Immutable 的 Record 和Map,以及在項目中的改造,讓開發過程當中不被immutable的api所幹擾
複製代碼
  • 問題: 極易引發濫用, 在項目中,許多開發者很喜歡使用toJS, 讓本能夠大幅度提升性能的immutable反而致使不斷地重複渲染
在項目中儘可能不適用toJS(),及時使用,定義在某種特定類型下才容許(例如向後端提交數據時),這樣不會致使重複渲染的問題
複製代碼

調試錯誤困難(Immutable.js Object Formatter)
數組

  • 插件

6 使用 Reacord 解決Immutable API 的泛濫

使用繼承 Record 的方式來定義數據類型,該方式的優勢在於類型名既能表達一個「值」,又能表達「類型」。
緩存

6.1 一旦構造好就不能再增長更多的屬性

有效處理後臺接口返回大量無用數據,而record只會記錄最初定義的字段數據。不會拋出異常。

6.2 Reacod實例能夠像普通js對象訪問

有效解決Immutable的API的侵入式氾濫。

const plugin: PluginType = {
    id: 0,
    name: '',
    version: '',
    createTime: new Date().valueOf(),
    status: 0,
    owns: null,
};

class Plugin extends Record({...plugin, owns: new User()}) {
    static fromJS(obj: PluginType): Plugin {
        return new Plugin({...obj, owns: User.fromJS(obj.owns)});
    }
}

// 使用
let plugin = new Plugin();
const { id, name , version, createTime, state, owns } = plugin;
複製代碼

6.3 Other

  • 能夠給Record設置默認值
  • 對Record添加命名,能夠更好地進行debugg和錯誤處理
  • 能夠對Record進行extend,以從Record中提供派生數據。

7. 在項目中的react, redux,immutable的使用

redux中有三個核心元素:store,reducer和action。

  • store做爲應用的惟一數據源,用於存儲應用在某一時刻的狀態數據,store是隻讀的,且只能經過action來改變。
  • reducer用於定義狀態轉換的具體細節,並與action相對應。
  • 應用的UI部分能夠經過store提供的subscribe方法來監聽store的改變,當狀態1下的store轉換爲狀態2下的store時,全部經過subscribe方法註冊的監聽器(listener)都會被調用。

7.1 Model

我在項目中主要採用了Record, List這兩種數據類型。

// 用於JS數據類型,例如給後臺傳遞的值使用類型,
interface PluginType {
    id: number;
    name: string;
    version: string;
    createTime: number;
    status: number;
    owns: UserType;
}


const plugin: PluginType = {
    id: 0,
    name: '',
    version: '',
    createTime: new Date().valueOf(),
    status: 0,
    owns: null,
};

// 用於Store存放的數據類型
class Plugin extends Record({...plugin, owns: new User()}) {
    // fromJS 很重要,用於轉換 js數據=> immutable數據。 每一個Record數據都有本身的formJS
    static fromJS(obj: PluginType): Plugin {
        return new Plugin({...obj, owns: User.fromJS(obj.owns)});
    }
}

export { plugin };
複製代碼

7.2 通用Action

傳遞到Action中的數據分爲三種:

  • 原始JS數據(不須要轉換爲immutable類型,例如 username:'zyh'),傳遞的值爲zyh是不須要轉換的
  • 做爲屬性的JS對象。 例如 上面Plugin 的 user屬性,須要將user轉爲immutable(recod)數據
  • 做爲model初始化的JS數據。直接轉爲immutable數據

在 組件和頁面中,對sotre發起action

能夠看到,下面沒有出現immutable數據結構的參數

actions.createAction('UPDATE_PLUGIN_NAME', 'plugin''change plugin name'); // name屬性,不須要修改成immutable數據
actions.createAction('UPDATE_INIT_PROPERTY_PLUGIN_USER', 'plugin', { id: 1, username: 'changeName', password: 'changepassword'}); // 存儲在Store中須要是Immutable數據
actions.createAction('INIT_PLUGIN', 'plugin', {id: 1, name: 'plugin', ....}); // 須要被轉爲immutabledata
複製代碼

通用Action提取

/** * 用於集中處理 原始JS數據 =》 immutable數據 * @param value JS 數據 * @param className JS 數據對應的immutable class 類型 */
const convertDataToRecord = (value: OriginStateType, className: string) => {
    let InitCls = initClasses[className];
        if (Array.isArray(value)) {
            return List(value).map((item) => (InitCls.fromJS(item)));
        }
        return InitCls.fromJS(value);
};
/** * * @param type 命名規則: INIT_XXXX, UPDATE_XXXX * @param key 命名規則:type===INIT_XXXX: 對象傳入對象名稱(例如alert), 數組傳入item類型名稱(Array<Plugin> 傳入 plugin) * type===UPDATE_XXXX, 傳遞修改值的key集合 * type===UPDATE_INIT_PROPERTY_XXXX : 將某個屬性總體替換爲一個對象 * @param value 能夠是原始類型,也能夠是Record List類型 */
const createAction = (type: string, key: string | Array<string>, value: OriginStateType | RecordStateType) => {

    return {
        type: type.replace('INIT_PROPERTY_', ''),
        payload: {
            key,
            value: type.includes('INIT_') ? convertDataToRecord(value, key as string) : value,
        },
    };
};
複製代碼

7.3 Store數據(reducer存儲通用函數)

// 每當新增一個Model模型,只須要在initClasses中添加一個key值 便可
export const initClasses = {
    alert: Alert,
    pluginQuery: PluginQuery,
    loading: Loading,
    user: User,
    plugin: Plugin,
    pluginList: List,
};

class Root {
    rootReducer = {};

    constructor() {
        this.initReducer();
    }
    private initReducer() {
        for (const key in initClasses) {
            if (initClasses.hasOwnProperty(key)) {
                let keyUpper = key.toUpperCase();
               
                this.rootReducer[key] = (state: RecordStateType = new initClasses[key], action: Action) => {
                    switch(action.type) {
                        case `INIT_${keyUpper}`:
                            return action.payload.value;
                        case `UPDATE_${keyUpper}`:
                            return this.basePropertyChange(state, action);
                        default:
                            return state;
                    }
                };
            }
        }
    }
    private basePropertyChange(state: RecordStateType, action: Action) {
        let {
            key,
            value,
        } : Payload = action.payload;
        // // change store based on string(eg: 'isShow'=>false, 'message':'id' => 10)
        if (typeof key === 'string') {
            let index = key.includes(':') ? key.split(':') : [key];
            return state.setIn(index, value);
        }

        // // chnage store bases on array, ['isShow', 'message:name'] => ['true', 'i am a message.']
        if (Array.isArray(key) && Array.isArray(value)) {
            let tempState = state;
            key.forEach((ck: string, index: number) => {
                let cIndex = ck.includes(':') ? ck.split(':') : [ck]; 
                tempState = tempState.setIn(cIndex, value[index]);
            });
            return tempState;
        } 

        return state;
    }
}
export default new Root().rootReducer;
複製代碼

7.4 在組件中屬性的使用

因爲存儲的全部Model都是immutable的(Record)類型,所以在使用的時候就是和普通js對象同樣。

const { id, name, user } = plugin;
複製代碼

可是我依然遇到了一個沒注意的坑,下面是數組的轉換,因爲store的subscribe方法註冊的監聽器(listener)都會被調用,若是咱們修改pluginQuery的值,對致使 mapStateToProps方法調用,則pluginList.toArray()被執行,那麼在我組件中 pluginList的地址就被改變,則相關的組件都會被從新渲染。所以請不要在mapStateToProps中轉換,仍是在使用的地方轉換比較好。

// as is
class Test {
    render() {
      const { pluginList } = this.props;
      // xxxx
    }
    const mapStateToProps = (state: MapState) => {
      let  { pluginQuery, pluginList }: Partial<InitState> = state.toObject();
      return {
          pluginQuery,
          pluginList,
      }; 
  };
}

// to be
class Test {
    render() {
      const { pluginList } = this.props;
     
      return(
      <div> { pluginList.map(() => { return xxxx }) } </div>)
    }

    const mapStateToProps = (state: MapState) => {
      let  { pluginQuery, pluginList }: Partial<InitState> = state.toObject();
      return {
          pluginQuery,
          pluginList,
      }; 
  };
}
複製代碼

7.5 小結

這一段小結時我將此次的調研使用到了一個全新的項目中,項目的性能很好,但願你們也能夠參考一下。主要是數組部分還想提一點:
對於immutable類型的數組,通過這段時間的開發,其實List()數組類型並不須要 toArray(), 應爲對於map,filter ,push 等經常使用方法是同樣的。所以儘可能我通常使用與原生數組的同樣的方法。也能夠減小immutable數據類型的侵入。但願你們也找到適合本身的開發模式,附上一個如今正在開發的項目性能測試截圖:

相關文章
相關標籤/搜索