React 將頁面元素拆分成元件,通過組裝展示資料。元件又有無狀態和有狀態之分,所謂狀態,可以簡單的認為是元件要展示的資料。React 有個特性或者說是限制單向資料流,元件的狀態資料只能在元件內部修改,對於其他元件是只讀的,想要修改只能通過元件提供的介面回撥。
隨著元件數量的增多,元件間狀態資料共享的複雜性也會隨之增加,如果僅使用 React 元件內的 State 可能會導致程式的流程混亂,程式碼難以維護。
原文連結:https://www.chuonye.com/archives/react-redux.html
1. 引入 Redux
為什麼這麼說呢,我們來看一個圖:
原始的 React,當最底層元件需要改變資料時,如果資料在父元件,回撥方法層層傳遞即可;如果在兄弟元件,那就需要藉助一箇中間元件,當然了也有辦法直接與兄弟元件通訊。可以想象,隨著應用複雜程度的提高,元件間通訊使用的各種直接或間接回撥,可能會導致程式碼亂成一團。
此時,Redux 就派上用場了,如上圖所示,它把應用的所有狀態-資料儲存在一個地方,並稱之為Store,元件間不直接通訊,而是把變化的資料推給 Store,需要根據狀態變化重新渲染的元件通過訂閱 Store 來實現。
Redux 在設計與實現時,遵循三大原則或者說規範限制。
1.1 唯一資料來源
整個應用程式的狀態資料僅儲存在一個 Store 中,資料就儲存在一個大的物件樹(object tree)中。
1.2 只讀的 Store
唯一的 Store 也是隻讀的,應用程式無法直接修改狀態資料。Store 物件本身的 API 非常少,僅有四個方法:
getState()
: 獲取當前狀態資料dispatch(action)
: 推送觸發變化subscribe(listener)
: 訂閱資料變化replaceReducer(nextReducer)
顯而易見,沒有提供設定狀態的方法。其中的 dispatch(action)
是唯一改變資料的方法,比如:
var action = {
type: 'ADD_USER',
user: {name: 'chuonye.com'}
};
store.dispatch(action);
dispatch
方法會將 action
傳遞給 Redux,action 就是一個普通物件,包含觸發的操作型別以及資料。
1.3 使用純函式更改資料
Redux 接收到 action
後,會使用一個純函式來處理,這些函式被稱為 Reducers:
var someReducer = function(state, action) {
...
return state;
}
Reducer 接收當前的 state 和 action 作為引數,它也不能直接修改原有的資料,而是通過返回一個新的 state 來修改。
總的來說,Redux 是一個幫助應用統一管理狀態資料的工具,它遵循嚴格的單向資料流(Store 只讀)設計,使得應用的行為變得可預測且容易理解。
2. React-Redux
Redux 一般和 React 這類框架搭配使用,為了方便與 React 整合,Redux 官方提供了一個 react-redux 繫結庫。react-redux 將元件劃分為容器元件,UI元件和其他元件,其中:
- 容器元件:與 Redux-Store 互動,分派 Action,監聽 state 變化,負責資料管理和業務邏輯
- UI 元件:無狀態,負責資料的展示,樣式,排版,資料來源於 props 屬性
- 其他元件:無法明確區分是容器還是 UI 的元件,或者本身比較簡單沒有拆分必要的元件
從整體來看,在使用上,應用程式碼分層設計,結構如下:
我們不妨結合著資料庫,來理解下每個層次的意思,從下往上:
- Store 可以理解成資料庫服務,其中的 State 是一個資料庫,State 物件樹的各個部分,可以理解為對應不同的表
- Reducer 就是實際對 State 增刪改的,它按邏輯拆分成多個 子reducer,分別對物件樹的不同部分或者說對不同的表進行操作,就如同一個 SQL 執行引擎
- Action 就是一系列觸發改變的動作以及資料,就如同一條條 SQL 語句
- 容器元件和 UI 元件就如同 Java 中設計的 DAO 和 Controller
這些設計怎麼有點熟悉,跟 Java 是越來越像了。下面簡單寫個例子,看看到底怎麼用。
3. 例項:Counter 計數器
接下來我們按照使用原生 React,使用 Redux 和使用 React-Redux的順序分別實現一個 Counter 計數器的功能。
3.1 原生 React 實現
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
constructor(props) {
super(props);
this.state = { value: 0 }; // 狀態資料
}
render() {
return (
<div>
<p>state: {this.state.value}</p>
<button onClick={() => this.handleIncrement()}>+1</button>
<button onClick={() => this.handleDecrement()}>-1</button>
</div>
);
}
// 處理方法
handleIncrement() {
let curVal = this.state.value + 1;
this.setState({value: curVal});
}
handleDecrement() {
let curVal = this.state.value - 1;
this.setState({value: curVal});
}
}
// 渲染
ReactDOM.render(<Counter />, document.getElementById('root'))
3.2 使用 Redux 實現
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
// 接收 action 並處理,簽名:(state, action) => newState
function reducer(state = {value: 0}, action) {
let curVal = state.value;
switch (action.type) {
case 'INCREMENT':
return {value: curVal+1};
case 'DECREMENT':
return {value: curVal-1};
default:
return state;
}
}
// 建立 Redux store
const store = createStore(reducer);
const render = () => ReactDOM.render(
<div>
<p>state: {store.getState().value}</p>
{/* dispatch 通知 store 改變資料 */}
<button onClick={()=>{store.dispatch({type: 'INCREMENT'})}}>+1</button>
<button onClick={()=>{store.dispatch({type: 'DECREMENT'})}}>-1</button>
</div>,
document.getElementById('root')
)
render();
// 訂閱 store,狀態改變時更新 UI
store.subscribe(render);
3.3 使用 React-Redux 實現
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';
// 接收 action 並處理
function reducer(state = {value: 0}, action) {
let curVal = state.value;
switch (action.type) {
case 'INCREMENT':
return {value: curVal+1};
case 'DECREMENT':
return {value: curVal-1};
default:
return state;
}
}
// 定義一個 UI 元件,用於展示資料
class CounterUI extends Component {
render() {
// 資料來源於 props
const {value, handleIncrement, handleDecrement} = this.props;
return (
<div>
<p>state: {value}</p>
<button onClick={handleIncrement}>+1</button>
<button onClick={handleDecrement}>-1</button>
</div>
);
}
}
// 定義一個 容器元件,用於與 Redux store 互動
// 將 Redux state 按需注入到 UI 元件的 props
function mapStateToProps(state) {
return { value: state.value }
}
// 將 Redux actions 按需注入到 UI 元件的 props
function mapDispatchToProps(dispatch) {
return {
handleIncrement: () => dispatch({type: 'INCREMENT'}),
handleDecrement: () => dispatch({type: 'DECREMENT'})
}
}
// 使用 connect 方法基於 UI元件 生成一個 容器元件
const CounterContainer = connect(mapStateToProps, mapDispatchToProps)(CounterUI);
const store = createStore(reducer);
// 使用 Provider 作為根元件,使得所有子元件都能訪問到 store
ReactDOM.render(
<Provider store={store}>
<CounterContainer />
</Provider>,
document.getElementById('root')
)
4. 小結
Redux 和 React-Redux 專門設計了約束和約定,導致程式碼量不見得減少,甚至還可能增加了。簡單的專案看不出有什麼優勢,但在構建一定規模的企業級專案時,這些約束和條條框框,會有利於專案的維護和管理。正如 Java,有些人認為過於囉嗦,但這正是它嚴謹的體現!
下一篇將會結合 antd UI框架模仿實現官方提供的購物車例項。