React經常使用的繼承實現方式有兩種,React.Component和 React.PureComponent。javascript
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
在上面的例子中,咱們使用了Object.assign來實現不可變性。其實徹底能夠不適用immutable。react
let _b = Object.assign([], b);
_b[0] = Object.assign({}, _b[0], {x: (_b[0].x + 1) });
複製代碼
可是他有缺點:
redux
immutable.js 使用樹的方式實現了 持久化數據結構
,保證了對象是都是不可變的。任何添加,刪除,修改等都是對生成一個新的對象。而且經過結構共享
等方式大幅提升性能。例以下面的結構,但願在g
節點下增長一個h節點
後端
數據的存儲(重點Vector Trie, 空間換時間), juejin.cn/post/684490…
如何壓縮空間作優化(樹的高度壓縮, BitMap節點內部壓縮). juejin.cn/post/684490…
api
使用緩存中間件,例如reselect
複製代碼
經過使用Immutable 的 Record 和Map,以及在項目中的改造,讓開發過程當中不被immutable的api所幹擾
複製代碼
在項目中儘可能不適用toJS(),及時使用,定義在某種特定類型下才容許(例如向後端提交數據時),這樣不會致使重複渲染的問題
複製代碼
調試錯誤困難(Immutable.js Object Formatter)
數組
使用繼承 Record 的方式來定義數據類型,該方式的優勢在於類型名既能表達一個「值」,又能表達「類型」。
緩存
有效處理後臺接口返回大量無用數據,而record只會記錄最初定義的字段數據。不會拋出異常。
有效解決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;
複製代碼
redux中有三個核心元素:store,reducer和action。
我在項目中主要採用了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 };
複製代碼
傳遞到Action中的數據分爲三種:
zyh
是不須要轉換的能夠看到,下面沒有出現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
複製代碼
/** * 用於集中處理 原始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,
},
};
};
複製代碼
// 每當新增一個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;
複製代碼
因爲存儲的全部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,
};
};
}
複製代碼
這一段小結時我將此次的調研使用到了一個全新的項目中,項目的性能很好,但願你們也能夠參考一下。主要是數組部分還想提一點:
對於immutable類型的數組,通過這段時間的開發,其實List()數組類型並不須要 toArray(), 應爲對於map,filter ,push 等經常使用方法是同樣的。所以儘可能我通常使用與原生數組的同樣的方法。也能夠減小immutable數據類型的侵入。但願你們也找到適合本身的開發模式,附上一個如今正在開發的項目性能測試截圖: