React 技巧和最佳實踐
不管如何,你都應該去嘗試一下當下炙手可熱的 React。這篇文章我會總結一些關于 React 的技巧和最佳實踐。
使用 PureComponent
React 在 15.3 版本中被引入了類 PureComponent
,表明之前社區中獎 shouldComponentUpdate
和 PureRenderMixin
作為一種最佳實踐是一種共識。
當然兩點需要簡單說明的地方:
首先,你需要了解的是當 state 改變時,React 是如何更新我們的組件的。簡單的說,當一個組件的狀態改變時,它以及它所有后代的組件的 render 方法都會被調用一次,然后生成一個虛擬的 DOM 樹再將它與內存中的虛擬的 DOM 樹進行比較,然后采用最小代價的操作來更新瀏覽器中真實 DOM。因此,當我們通過 setState
方法更新狀態的時候,React 看起來像是更新了整個 DOM 樹。shouldComponentUpdate
提供了一種我們可以侵入到這個過程的能力,當該方法返回 false 的時候,該組件的 render 方法不會被調用,不會產生新的虛擬 DOM,該組件自然也沒有后續虛擬 DOM 的 diff 和真實 DOM 更新。
其次,PureRenderMixin 只是淺比較 state 和 props ,只意味著你的 render 在相同的 state 和 props 的情況下必須要返回一致的輸出,當你使用了復雜的數據類型來作為 state 和 prop 的時候,你可能需要引入 Immutable.js 這樣的不可變數據類型的庫。
通常情況下,它能夠大幅提高你的應用的性能,尤其是你在構建一個復雜的應用的時候。當你使用類似于 Redux 這樣的庫用于集中管理你應用的 state 時,數據流總是從頂部單向流向各個層級的組件,因此意味著每一次 store 的變化,都會導致整個組件樹中所有組件的 render 方法被調用一次,這也意味著引入 PureComponent 能夠避免那些不必要的 render 方法被調用。
狀態設計和無狀態組件
關于狀態的設計有些時候并不是一目了然,尤其是對新手來說。但是總結起來只有一條核心的原則,那就是盡量保證你的組件無狀態 。這會讓你的組件更容易理解和維護,也會讓你整個應用的數據流更加清晰。
具體可以拆分為兩條具體的原則,狀態最小子集 和 狀態集中管理 。狀態最小集要求我們找到影響視圖變化的狀態的最小集合,而狀態集中管理則要求我們使用類似 redux 這樣的狀態容器來集中管理我們的狀態。實踐中具體的技巧大概有以下 3 條:
消除組件內重復的狀態
其核心是:
任何可以被計算或推導的數據不應該作為狀態。
如果將可以被計算和推導出來的數據作為狀態,那么就意味著你要維護和保持這些狀態之間的同步,這通常是 bug 產生的地方,而且排查起來也很不容易。
消除組件間重復的狀態
其核心是:
_ 任何除自身以及后代以外的組件會關注的數據不應該作為自身的狀態_。
也就是通常提到的「向上頂」的原則。
這一條沒有上一條那么直觀,但卻是很容易犯錯的地方。任何除自身以及后代以外的組件通常意外著父組件或者其兄弟組件。
我舉一個簡單的例子,現在你要設計一個篩選器組件,它會涉及到很多篩選項,然后當用戶提交的時候,頁面下方的列表會更新。我們會很自然的將當前用戶篩選的值作為該組件內部的一個狀態,由該組件內部跟新和維護,然后在用戶點擊的確定的時候,通過暴露一個 onSubmit
事件,將參數傳遞給父組件,如組件更新列表。類似如下:
class Filter extends React.Component {
constructor(props) {
super(props)
this.state = {
foo: props.defaultFoo,
bar: props.defaultBar
}
}
handleBarChange(newBar) {
this.setState({
bar: newBar
})
}
handleFooChange(newFoo) {
this.setState({
foo: newFoo
})
}
handleSubmit() {
const { bar, foo } = this.state
this.props.onSubmit({ bar, foo })
}
render () {
//...
}
}
這樣做會帶來很多問題,比如如組件如果還接受其他的篩選條件來更新列表,比如翻頁,那么意味這父組件必須要在 onSubmit 事件中不得不保存該篩選條件,可能將它存到 state 里面,那就意味同一份數據在父組件中也保存了一份相同的引用,同時意味著你要時刻小心的保持兩者的同步。再比如,我現在用一個彈窗或者下拉框的形式來展現這些篩選條件,也就是說,但用戶篩選完成之后,如果并沒有點擊提交,而是直接關閉了彈窗。那么當用戶再次點開彈窗的時候,你需要重置會已生效的篩選條件,這個時候你又不得不將 props 同步給 state。
接下來我們進行改進,篩選項的值父組件是會關注的,所以「向上頂」放到父組件里面去。大概如下:
class Filter extends React.Component {
constructor(props) {
super(props)
}
handleBarChange(newBar) {
const {value, onChange} = this.props
onChange(Object.assign({}, value, {bar: newBar}))
}
handleFooChange(newFoo) {
const {value, onChange} = this.props
onChange(Object.assign({}, value, {foo: newFoo}))
}
handleSubmit() {
this.props.onSubmit()
}
render () {
//...
}
}
然后 Filter 組件變得很清晰明了,僅僅負責將父組件傳入的篩選項信息 value 正確的渲染,同時暴露 onChange 和 onSubmit 父組件,將篩選狀態的維護工作拆分給了父組件。而我上面提到的 2 個問題也就迎刃而解了。
通過以上 2 條技巧,我們很容易消除那些重復的狀態,保證我們應用處于最小狀態集合中。同時能夠衍生出一條簡單的準則幫助你發現問題,那就是當你發現你在維護兩個狀態的同步時,通常意味著你的組件設計有問題,如果兩個狀態在同一個組件內,參考技巧 1,否則參考技巧 2。
現在保證了狀態最小集合,但要達到無狀態組件,還需要我們將這些狀態移除我們的組件,也就是接下來第 3 條技巧。
使用狀態容器管理狀態
這一條技巧很簡單,就是使用例如 redux 這樣的狀態容器來集中管理我們應用中的狀態,尤其是在構建復雜的應用的時候。關于 redux 的使用,社區里面有很多相關的教程,這里不再贅述。
最后,值得一提的是,我們是否應該將所有的狀態都移除組件,達到真正的無狀態組件。答案是否定的,因為這樣不僅將會導致實踐中流程極其繁瑣,甚至某些時候也是不必要的。
但某些時候,又是什么時候?
簡單的想一下我們瀏覽器自帶的 select
元素,選擇器是否展開必然應該作為一個狀態。但是我們沒有需要將其作為一個屬性傳給元素來控制元素是否展開,而且 select
元素確實也沒有這個屬性。因此,這個原則就是:
當某個狀態之后該組件自身會關注,其余組件(父類和子類等等)都不關注的時候,那么你可以將它作為狀態保留到組件內部。
關注 ComponentDidUpdate
最后,我想說明一下 ComponentDidUpdate 這個方法,這個方法經常被大家忽略,但其實應該有著更大的用武之地。
對于 React 而言,理想情況下應該是,我們設計好狀態以及狀態和視圖的關聯關系以后,在隨后時間推移和用戶交互的過程中我們只需要更改我們的狀態就可以了。但實際情況是,我們很多時候在更改狀態(例如,篩選項等)之后還需要去服務器上請求相應的數據。我們最常見的做法是在 setState
的回調函數里,如果使用了 類似于 redux 的狀態管理容器時,我們可能派發一個異步 Action。
// setState 回調
function handlePageChange(newPage) {
this.setState(
{ page: newPage },
() => this.fetchData()
);
}
// dipatch 異步的 action
function fetchDataWithPageChange(page) {
return async (dispatch, getState) => {
// 同步的 action
dispatch(pageChange(page));
const query = getState().query;
// 異步操作
let res = await get(url, Object.assign({ page }, query));
// 同步 action
dispatch(dataChange(res));
}
}
這樣做最大的問題是,我們將我們改變狀態的行為和數據請求耦合在了一起,而當其他同樣需要請求相同數據的狀態改變之后,我們也必須記著調用相同的數據請求的方法,需要小心翼翼,不能遺忘。
如果我們把狀態變化與數據請求的關系寫在 componentDidUpdate 里面,那么我們就能夠專注的更新我們的狀態,因為狀態更新會自動觸發數據請求。看到了嗎?這個我們處理狀態和視圖的關系如此相似,一旦指定了映射關系,那么只需要簡單更新狀態,就能自動觸發另一方相應的動作。
改進如果下:
class Com extends PureComponent {
// ...
handlePageChange(newPage) {
this.setState({
page: newPage
});
}
componentDidUpdate(preProps, preState) {
const { page } = preState;
if (page !== this.state.page) {
this.fetchData();
}
}
// ...
}
// 或者
class Com extends PureComponent {
// ...
handlePageChange(newPage) {
this.props.dispatch(changePage(newPage));
}
componentDidUpdate(preProps) {
const { page } = preProps;
if (page !== this.props.page) {
this.props.dispatch(fetchData());
}
}
// ...
}
function changePage(page) {
return {
type: 'CHANGE_PAGE',
index: page
};
}
總結
關于 React 的總結和最佳實踐暫時想到的就這些,從使用了 React 之后,在我需要構建一個在線的應用之前(不管是否使用 React ),我都會先首先梳理頁面視圖隨著時間推移和用戶的交互時是如何變化的,具體到 React 就是整個 app 的數據流。它們就像是一棟建筑的設計稿和結構圖,等到你深入其中的細枝末節的時候仍然給能夠對整體了然于胸,不會被一葉蔽目。我很享受這個過程,希望你們也能喜歡。