4.2 react patterns
-
修改 Props
-
Immutable data representation
-
-
確定性
-
在 getInitialState 中使用 props
-
私有狀態和全域性事件
-
render 包含 side effects
-
jQuery 修改 DOM
-
使用無狀態元件
-
-
記憶體管理
-
componentWillUnmount 取消訂閱事件
-
判斷 isMounted
-
-
上層設計
-
使用 container component
-
使用 Composition 替代 mixins
-
Composability – Presenter Pattern
-
Composability – Decorator Pattern
-
Context 資料傳遞
-
4.2.1 關於
React 的框架設計是趨於函式式的,其中最主要的兩點也是為什麼會選擇 React 的兩點:
-
單向性:資料的流動是單向的
-
確定性:React(storeData) = view 相同資料總是渲染出相同的 view
這兩點即是特性也是設計 React 應用的基本原則,圍繞這兩個原則社群裡邊出現了一些 React 設計模式,即有好的設計模式也有應該要避免的反模式,理解這些設計模式能夠幫助我們寫出更優質的 React 應用,本節將圍繞 單向性、確定性、記憶體管理、上層設計 來討論這些設計模式。
anti 表示反模式,good 表示好模式
4.2.2 單向性
資料的流動是單向的
修改 Props (anti)
描述: 元件任何地方修改 props 的值
解釋:
React 的資料流動是單向性的,流動的方式是通過 props 傳遞到元件中,而在 Javascript 中物件是通過引用傳遞的,修改 props 等於直接修改了 store 中的資料,導致破壞資料的單向流動特性
使用不可變資料 (good)
描述: store data 使用不可變資料
解釋: Javascript 物件的特性是可以任意修改,而這個特性很容易破壞資料的單向性,因為人工無法永遠確保資料沒有被修改過,唯一的做法是使用不可變資料,用程式碼邏輯確保資料不能被任意修改,後面會有一個完整的小節介紹不可變資料在 React 中的應用
4.2.3 確定性
React(storeData) = view 相同資料總是渲染出相同的 view
在 getInitialState 中使用 props (anti)
描述: getInitialState 通過 props 來生成 state 資料
解釋:
官方文件 https://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html
在 getInitialState 中通過 props 來計算 state 破壞了確定性原則,“source of truth” 應該只是來自於一個地方,通過計算 state 過後增加了 truth source。這種做法的另外一個壞處是在元件更新的時候,還需要計算重新計算這部分 state。
舉例:
var MessageBox = React.createClass({
getInitialState: function() {
return {nameWithQualifier: `Mr. ` + this.props.name};
},
render: function() {
return <div>{this.state.nameWithQualifier}</div>;
}
});
ReactDOM.render(<MessageBox name="Rogers"/>, mountNode);
優化方式:
var MessageBox = React.createClass({
render: function() {
return <div>{`Mr. ` + this.props.name}</div>;
}
});
ReactDOM.render(<MessageBox name="Rogers"/>, mountNode);
需要注意的是以下這種做法並不會影響確定性
var Counter = React.createClass({
getInitialState: function() {
// naming it initialX clearly indicates that the only purpose
// of the passed down prop is to initialize something internally
return {count: this.props.initialCount};
},
handleClick: function() {
this.setState({count: this.state.count + 1});
},
render: function() {
return <div onClick={this.handleClick}>{this.state.count}</div>;
}
});
ReactDOM.render(<Counter initialCount={7}/>, mountNode);
私有狀態和全域性事件 (anti)
描述: 在元件中定義私有的狀態或者使用全域性事件
介紹: 元件中定義了私有狀態和全域性事件過後,元件的渲染可能會出現不一致,因為全域性事件和私有狀態都可以控制元件的狀態,這樣外部使用元件無法保證元件的渲染結果,影響了元件的確定性。另外一點是元件應該儘量保證獨立性,避免和外部的耦合,使用全域性事件造成了和外部事件的耦合。
render 函式包含 side effects (anti)
side effect 解釋: https://en.wikipedia.org/wiki/Side_effect_(computer_science)
描述: render 函式包含一些 side effects 的程式碼邏輯,這些邏輯包括如
-
修改 state 資料
-
修改 props 資料
-
修改全域性變數
-
呼叫其他導致 side effect 的函式
解釋: render 函式如果包含了 side effect ,渲染的結果不再可信,所以確保 render 函式為純函式
jQuery 修改 DOM (anti)
描述: 使用外部 DOM 框架修改或刪除了 DOM 節點、屬性、樣式
解釋: React 中 DOM 的結構和屬性都是由渲染函式確定的,如果使用了 Jquery 修改 DOM,那麼可能造成衝突,檢視的修改源頭增加,直接影響元件的確定性
使用無狀態元件 (good)
描述: 優先使用無狀態元件
解釋: 無狀態元件更符合函式式的特性,如果元件不需要額外的控制,只是渲染結構,那麼應該優先選擇無狀態元件
4.2.4 記憶體管理
componentWillUnmount 取消訂閱事件 (good)
描述: 如果元件需要註冊訂閱事件,可以在 componentDidMount 中註冊,且必須在 ComponentWillUnmount 中取消訂閱
解釋: 在元件 unmount 後如果沒有取消訂閱事件,訂閱事件可能仍然擁有元件例項的引用,這樣第一是元件記憶體無法釋放,第二是引起不必要的錯誤
判斷 isMounted (anti)
描述: 在元件中使用 isMounted 方法判斷元件是否未被登出
解釋:
React 中在一個元件 ummount 過後使用 setState 會出現warning提示(通常出現在一些事件註冊回撥函式中) ,避免 warning 的解決辦法是:
if(this.isMounted()) { // This is bad.
this.setState({...});
}
但這是個掩耳盜鈴的做法,因為如果出現了錯誤提示就表示在元件 unmount 的時候還有元件的引用,這個時候應該是已經導致了記憶體溢位。所以解決錯誤的正確方法是在 componentWillUnmount 函式中取消監聽:
class MyComponent extends React.Component {
componentDidMount() {
mydatastore.subscribe(this);
}
render() {
...
}
componentWillUnmount() {
mydatastore.unsubscribe(this);
}
}
4.2.5 上層設計
使用 container component (good)
描述: 將 React 元件分為兩類 container 、normal ,container 元件負責獲取狀態資料,然後傳遞給與之對應的 normal component,對應表示兩個元件的名稱對應,舉例:
TodoListContainer => TodoList
FooterContainer => Footer
解釋: 參看 redux 設計中的 container 元件,container 元件是 smart 元件,normal 元件是 dummy 元件,這樣的責任分離讓 normal 元件更加獨立,不需要知道狀態資料。明確的職責分配也增加了應用的確定性(明確只有 container 元件能夠知道狀態資料,且是對應部分的資料)。
使用 Composition 替代 mixins (good)
描述: 使用元件的組合的方式(高階元件)替代 mixins 實現為元件增加附加功能
解釋:
mixins 的設計主要目的是給元件提供外掛機制,大多數情況使用 mixin 是為了給元件增加額外的狀態。但是使用 mixins 會帶來一些額外的壞處:
-
mixins 通常需要依賴元件定義特定的方法,如 getSomeMixinState ,而這個是隱式的約束
-
多個 mixins 可能會導致衝突
-
mixins 通常增加了額外的狀態資料,而 react 的設計應該是要避免過多的內部狀態
-
mixins 可能會影響 shouldComponentUpdate 的邏輯, mixins 做了很多資料合併的邏輯
另外一點是在新版本的 React 中,mixins 將會是廢棄的 feature,在 es6 class 定義元件也不會支援 mixins。
舉個例子,一個訂閱 fluxstore 的 mixin 為:
function StoreMixin(store) {
var Mixin = {
getInitialState() {
return this.getStateFromStore(this.props);
},
componentDidMount() {
store.addChangeListener(this.handleStoreChanged)
this.setState(this.getStateFromStore(this.props));
},
componentWillUnmount() {
store.removeChangeListener(this.handleStoreChanged)
},
handleStoreChanged() {
if (this.isMounted()) {
this.setState(this.getStateFromStore(this.props));
}
}
};
return Mixin;
}
使用
const TodolistContainer = React.createClass({
mixins: [StoreMixin(AppStore)],
getStateFromStore(props) {
return {
todos: AppStore.get(`todos`);
}
}
})
轉換為元件的組合方式為:
function connectToStores(Component, store, getStateFromStore) {
const StoreConnection = React.createClass({
getInitialState() {
return getStateFromStore(this.props);
},
componentDidMount() {
store.addChangeListener(this.handleStoreChanged)
},
componentWillUnmount() {
store.removeChangeListener(this.handleStoreChanged)
},
handleStoreChanged() {
if (this.isMounted()) {
this.setState(getStateFromStore(this.props));
}
},
render() {
return <Component {...this.props} {...this.state} />;
}
});
return StoreConnection;
};
使用方式:
class Todolist extends React.Component {
render() {
// ....
}
}
TodolistContainer = connectToStore(Todolist, AppStore, props => {
todos: AppStore.get(`todos`)
})
Presenter Pattern
描述: 利用 children 可以作為函式的特性,將資料獲取和資料表現分離成為兩個不同的元件
如下例子:
class DataGetter extends React.Component {
render() {
const { children } = this.props
const data = [ 1,2,3,4,5 ]
return children(data)
}
}
class DataPresenter extends React.Component {
render() {
return (
<DataGetter>
{data =>
<ul>
{data.map((datum) => (
<li key={datum}>{datum}</li>
))}
</ul>
}
</DataGetter>
)
}
}
const App = React.createClass({
render() {
return (
<DataPresenter />
)
}
})
解釋: 將資料獲取和資料展現分離,同時利用元件的 children 可以作為函式的特性,讓資料獲取和資料展現都可以作為元件使用
Decorator Pattern
描述: 父元件通過 cloneElement 方法給子元件新增方法和屬性
cloneElement 方法:
ReactElement cloneElement(
ReactElement element,
[object props],
[children ...]
)
如下例子:
const CleverParent = React.createClass({
render() {
const children = React.Children.map(this.props.children, (child) => {
return React.cloneElement(child, {
// 新增 onClick 屬性
onClick: () => alert(JSON.stringify(child.props, 0, 2))
})
})
return <div>{children}</div>
}
})
const SimpleChild = React.createClass({
render() {
return (
<div onClick={this.props.onClick}>
{this.props.children}
</div>
)
}
})
const App = React.createClass({
render() {
return (
<CleverParent>
<SimpleChild>1</SimpleChild>
<SimpleChild>2</SimpleChild>
</CleverParent>
)
}
})
解釋: 通過這種設計模式,可以應用到一些自定義的元件設計,提供更簡潔的 API 給第三方使用,如 facebook 的 FixedDataTable 也是應用了這種設計模式
Context 資料傳遞
描述: 通過 Context 可以讓所有元件共享相同的上下文,避免資料的逐級傳遞, Context 是大多數 flux 庫共享 store 的基本方法。
使用方法:
/**
* 初始化定義 Context 的元件
*/
class Chan extends React.Component {
getChildContext() {
return {
environment: "grandma`s house"
}
}
}
// 設定 context 型別
Chan.childContextTypes = {
environment: React.PropTypes.string
};
/**
* 子元件獲取 context
*/
class ChildChan extends React.Component {
render() {
const ev = this.context.environment;
}
}
/**
* 需要設定 contextTypes 才能獲取
*/
ChildChan.contextTypes = {
environment: React.PropTypes.string
};
解釋: 通常情況下 Context 是為基礎元件提供的功能,一般情況應該避免使用,否則濫用 Context 會影響應用的確定性。