書籍完整目錄
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 會影響應用的確定性。
參考鏈接
-
https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750#.hvbsii4zd
-
http://www.zhubert.com/blog/2016/02/05/react-composability-patterns/
-
https://medium.com/@learnreact/context-f932a9abab0e#.wn00ktlde