參考文章:Redux中文文檔
1. Redux是什么?
是一種狀態(tài)管理容器,用來管理狀態(tài)。
Redux 是 JavaScript 狀態(tài)容器,提供可預(yù)測化的狀態(tài)管理。
可以讓你構(gòu)建一致化的應(yīng)用,運行于不同的環(huán)境(客戶端、服務(wù)器、原生應(yīng)用),并且易于測試。不僅于此,它還提供 超爽的開發(fā)體驗,比如有一個時間旅行調(diào)試器可以編輯后實時預(yù)覽。
Redux 除了和 React 一起用外,還支持其它界面庫。它體小精悍(只有2kB,包括依賴)。
不明白?繼續(xù)看下面
2. 為什么用Redux?
React中,數(shù)據(jù)的流向是單向的
props只能從父組件向下傳遞到子組件
而state只能在組件內(nèi)部使用
拋出一個問題,如果子組件想把數(shù)據(jù)傳遞給父組件,咋辦?
這簡單啊,可以讓父組件通過props給子組件傳遞一個函數(shù);子組件想傳數(shù)據(jù)的時候,只要調(diào)用父組件傳進來的函數(shù),把數(shù)據(jù)當參數(shù)傳進去并調(diào)用該函數(shù),這樣父組件就能收到了
那如果要傳遞給父組件的父組件的父組件呢?把函數(shù)一層一層傳進來么?
0.0
可以是可以,就是太麻煩了。。。
那如果一個組件不想和父組件通訊,想和其他的組件通訊,咋辦?
0.0
。。。。。。。
所以這就是問題所在了,完整版請看下方鏈接
引用知乎上的一段答案
作者:Wang Namelos
鏈接:https://www.zhihu.com/question/41312576/answer/90782136
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
解答這個問題并不困難:唯一的要求是你熟悉React。不要光聽別人描述名詞,理解起來是很困難的。從需求出發(fā),看看使用React需要什么:
- React有props和state: props意味著父級分發(fā)下來的屬性,state意味著組件內(nèi)部可以自行管理的狀態(tài),并且整個React沒有數(shù)據(jù)向上回溯的能力,也就是說數(shù)據(jù)只能單向向下分發(fā),或者自行內(nèi)部消化。理解這個是理解React和Redux的前提。
- 一般構(gòu)建的React組件內(nèi)部可能是一個完整的應(yīng)用,它自己工作良好,你可以通過屬性作為API控制它。但是更多的時候發(fā)現(xiàn)React根本無法讓兩個組件互相交流,使用對方的數(shù)據(jù)。然后這時候不通過DOM溝通(也就是React體制內(nèi))解決的唯一辦法就是提升state,將state放到共有的父組件中來管理,再作為props分發(fā)回子組件。
- 子組件改變父組件state的辦法只能是通過onClick觸發(fā)父組件聲明好的回調(diào),也就是父組件提前聲明好函數(shù)或方法作為契約描述自己的state將如何變化,再將它同樣作為屬性交給子組件使用。這樣就出現(xiàn)了一個模式:數(shù)據(jù)總是單向從頂層向下分發(fā)的,但是只有子組件回調(diào)在概念上可以回到state頂層影響數(shù)據(jù)。這樣state一定程度上是響應(yīng)式的。
- 為了面臨所有可能的擴展問題,最容易想到的辦法就是把所有state集中放到所有組件頂層,然后分發(fā)給所有組件。
- 為了有更好的state管理,就需要一個庫來作為更專業(yè)的頂層state分發(fā)給所有React應(yīng)用,這就是Redux。
3. Redux怎么用?
用之前首先要明白Redux的思想,使用方法倒是次要的
重要的是這種把所有數(shù)據(jù)的狀態(tài),都拿出來放到最頂層的思想
做個不恰到的比喻就相當于計劃經(jīng)濟。。。
簡單描述一下,具體還是要看官方文檔:
首先把一個應(yīng)用里所有的數(shù)據(jù)都拿出來,放到最頂層的一個叫store的容器里,這個里面的state變量存儲了整個App的數(shù)據(jù)狀態(tài)
Redux規(guī)定了一些規(guī)則,store里的數(shù)據(jù)不能隨便改,要改的話要發(fā)送一個action,這個action包含了一些操作信息和數(shù)據(jù)信息,比如有一個添加用戶的action,它的操作信息就是添加用戶,附帶了一些用戶的基本信息
發(fā)送了信息總得有人收到吧 這個接受的人就叫reducer,它會收到action,然后做處理,返回新的數(shù)據(jù)
React拿到了新的數(shù)據(jù)state就會變化,React檢測到了state變化就自動更新UI,這就是基本流程了
其實Redux單另拿出來用還是挺簡單的,使用教程戳這里https://github.com/react-guide/redux-tutorial-cn#redux-tutorial
主要是和React一起用,需要使用到react-redux
這個模塊
這個模塊封裝了幾個方法,在React項目里要按他這個套路來
,然而這個套路要寫的東西有點繁雜,就被繞進去了
官網(wǎng)的Demo就大概講了這么一個套路,然而他又直接使用了一種高級的設(shè)計思想 容器組件和展示組件相分離 ,分成很多個組件,所以看的云里霧里的
下面我們就不用他官方的Demo,自己按正常的邏輯順序把Redux接入React項目
可以下載我的Demo
GitHub:https://github.com/BadWaka/redux-demo
npm install
npm start
4. 代碼簡述
我要做一個習慣應(yīng)用,用戶可以添加習慣,點擊習慣可以改變習慣的完成狀態(tài),就是這么簡單的一個功能
我建立了一個文件夾redux
用來存放狀態(tài)管理的相關(guān)文件,redux/custom
和component/custom
相對應(yīng),代表是custom模塊的組件部分和狀態(tài)部分,方便管理
action
在redux/custom/
下建立一個actions.js
文件,用來存放custom習慣模塊的所有action
/**
* Action的集合
* Created by BadWaka on 2017/3/15.
*/
/**
* 修改習慣項完成狀態(tài)Action
* @param customItemId 習慣項的id
* @return {{type: string, id: *}}
*/
export const changeCustomItemCompletionStatusAction = (customItemId) => {
return {
type: 'CHANGE_CUSTOM_ITEM_COMPLETION_STATUS_ACTION',
id: customItemId
};
};
/**
* 添加習慣項Action
* @param customItemText 要添加的習慣項的文本
* @return {{type: string, text: *}}
*/
let nextId = 0;
export const addCustomItemAction = (customItemText) => {
return {
type: 'ADD_CUSTOM_ITEM_ACTION',
text: customItemText,
id: nextId++
};
};
該文件暴露出兩個函數(shù),這兩個函數(shù)用來生成對應(yīng)的action
這里的id是自己維護的一個id,講道理這都應(yīng)該去服務(wù)器獲得的,但是那有涉及到異步了。。更復雜,先了解Redux同步使用再說
reducer
可以看到有3個reducer
indexReducer
是總的reducer,因為一個store只能由一個reducer創(chuàng)建,而React中,我們只需要一個最上層的store就夠了;所以這時候就需要reducer的合并,indexReducer
就是干這個事情的
indexReducer.js:
/**
* 最終的Reducer
* Created by BadWaka on 2017/3/15.
*/
import {combineReducers} from 'redux'; // 需要調(diào)用redux中的combineReducers這個合并reducer的方法
import customListReducer from './custom/customListReducer'; // 引入習慣列表Reducer
// 合并Reducer
const appReducer = combineReducers({
customList: customListReducer // 這里把state中的customList字段與customListReducer對應(yīng)起來,相當于customListReducer就只在乎customList這個字段就行了
});
// 導出一個最終的appReducer
export default appReducer;
customListReducer.js
// 引入customItemReducer
import customItemReducer from './customItemReducer';
/**
* 習慣列表的Reducer
* @param state 這里收到的state是一個習慣項的數(shù)組
* @param action
* @return {*}
*/
const customListReducer = (state = [], action) => { // 這里使用ES6的語法,如果state是undefined的話,給state設(shè)置一個默認值;為什么要這樣?因為reducer和store創(chuàng)建的時候會默認的發(fā)送初始化的action和用于檢測的action;如果state是undefined,而reducer里又沒有處理,直接返回了,就會在控制臺里報錯
console.log('customListReducer state = ', state, ' action = ', action);
switch (action.type) {
/**
* 添加customItem
*/
case 'ADD_CUSTOM_ITEM_ACTION':
// 返回一個新對象
let newCustomItem = {
id: action.id, // id
text: action.text, // 文本內(nèi)容
completed: false // 是否完成
};
// 復制一份state的副本,永遠不要修改state,而是返回state的一份副本
let newState = [...state];
// console.log('newState', newState);
newState.push(newCustomItem); // 把新的習慣項添加到副本中
return newState; // 返回
/**
* 修改習慣項完成狀態(tài)Action
*/
case 'CHANGE_CUSTOM_ITEM_COMPLETION_STATUS_ACTION':
// 遍歷state(這里得到的state是customList,是一個數(shù)組),返回一個新數(shù)組
return state.map((customItem) => {
// 對每一項再調(diào)用customItemReducer
return customItemReducer(customItem, action);
});
default:
return state;
}
};
export default customListReducer;
customItemReducer.js
/**
* 習慣項Reducer
* Created by BadWaka on 2017/3/15.
* @param state 這里接收到的state只是單個的一個習慣項
* @param action
* @return {*}
*/
const customItemReducer = (state, action) => {
console.log('customItemReducer state = ', state, ' action = ', action);
switch (action.type) {
// 修改習慣項完成狀態(tài)Action
case 'CHANGE_CUSTOM_ITEM_COMPLETION_STATUS_ACTION':
// 因為上一層customReducer會傳所有的customItem進來,所以要判斷action里的id和傳入的state的id是不是相同
if (action.id !== state.id) {
return state; // 如果不相同,直接返回原state
}
// 注意,Redux官方文檔中嚴格指出不要直接修改state,所以下面這一句是錯誤的
// state.completed = !state.completed; // 將completed置反
// 需要使用Object.assign()新建副本,或者使用對象展開運算符
// 這里使用對象展開運算符,大概的意思就是將一個對象的可枚舉屬性拷貝至另一個對象,本例中是state中的可枚舉屬性給一個新對象,這個新對象就是state的副本;之后修改這個新對象的completed屬性,然后將這個新對象作為返回值;
return {
...state,
completed: !state.completed
};
// 別忘了返回一個默認的state
default:
return state;
}
};
export default customItemReducer;
為什么有customListReducer
又有customItemReducer
?
因為設(shè)計的時候想著是充分解耦,能在內(nèi)部處理的,就不要麻煩外部的父組件;
比如點擊習慣項的時候切換習慣完成狀態(tài),它不需要父組件,所以就讓customItemReducer來處理;
而添加習慣項的時候,就需要父組件來處理,讓customListReducer來處理;
然而,理想是美好的,現(xiàn)實是殘酷的
在indexReducer.js
中合并reducer的時候,需要為每個reducer指定對應(yīng)的數(shù)據(jù),like this:
這時候就犯愁了。。整個數(shù)組可以這樣綁定,單個數(shù)據(jù)咋辦?
于是只能曲線救國,通過customListReducer繼續(xù)向下分發(fā),于是就有了在customListReducer
中繼續(xù)調(diào)用customItemReducer
進行處理
所以這就是為啥有customListReducer
又有customItemReducer
的原因
好了,既然action
和reducer
都有了,那么該有store
了
store
index.js
是React的入口文件,在這個文件里,初始化全局的store
index.js
// 引入React
import React from 'react';
import ReactDOM from 'react-dom';
// 引入全局css
import './stylesheets/app.css';
// 引入組件
import App from './components/App';
// 引入Redux
import {createStore} from 'redux'; // 需要使用redux的createStore方法創(chuàng)建store
import {Provider} from 'react-redux'; // 需要使用react-redux封裝好的控件Provider包裹App組件
import indexReducer from './redux/indexReducer'; // 導入合并好的全局reducer
// 創(chuàng)建一個全局store用來保存全局狀態(tài)
const store = createStore(indexReducer);
// 循環(huán)顯示總的state,這個主要是為了方便在控制臺看store中的state
setInterval(() => {
console.log('total state', store.getState());
}, 3000);
ReactDOM.render(
// 使用Provider組件包裹App組件,把store作為props傳入
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
這時候,運行程序npm start
,就應(yīng)該可以在控制臺看到日志了
INIT是redux初始化自動發(fā)的action,PROBE_UNKNOWN_ACTION是測試的action,不用管它們
好了,下面就來觸發(fā)一個action,看一下狀態(tài)變化吧
觸發(fā)action,觀察現(xiàn)象
components/custom/Custom.js
/**
* 習慣頁
* Created by BadWaka on 2017/3/11.
*/
import React, {Component} from 'react';
// 引入組件
import AppBar from '../common/app-bar/AppBar';
import CustomItem from './custom-item/CustomItem';
// 引入用到的action
import {addCustomItemAction} from '../../redux/custom/actions';
// 引入connect函數(shù)用來生成Redux組件
import {connect} from 'react-redux';
class CustomList extends Component {
constructor(props) {
super(props);
// 這個state是本地state,它存在的意義是為了獲得用戶的輸入文本
this.state = {
text: ''
};
}
render() {
return (
<section className="custom">
{/*引入AppBar 自己造的一個React Component 輪子*/}
<AppBar title="習慣"/>
<ul>
{this.props.customList.map(customItem => // 遍歷customList渲染customItem
<CustomItem
key={customItem.id}
id={customItem.id}
completed={customItem.completed}
text={customItem.text}/>
)}
</ul>
<div>
<input
type="text"
value={this.state.text}
onChange={(e) => { // 監(jiān)聽用戶輸入
this.setState({
text: e.target.value
});
}}/>
<button
onClick={() => {
console.log('添加 click');
// 發(fā)送添加習慣項Action
this.props.dispatch(addCustomItemAction(this.state.text));
// 把輸入置為空
this.setState({
text: ''
});
}}>
添加
</button>
</div>
</section>
);
}
}
// 關(guān)聯(lián)習慣列表的props與state,不關(guān)聯(lián)的話全局的state.customList就沒法作為props屬性傳進來
const mapStateToProps = (state) => {
return {
customList: state.customList
};
};
// 這個方法是用來綁定dispatch的,這里直接在組件里調(diào)用dispatch了,所以就沒有用到這個方法
// const mapDispatchToProps = {};
// 使用connect函數(shù)包裹組件,從而獲得store上下文,可以在組件里使用this.props.dispatch訪問到dispatch方法
CustomList = connect(mapStateToProps)(CustomList);
export default CustomList;
components/custom/custom-item/CustomItem.js
/**
* 習慣Item頁
* Created by BadWaka on 2017/3/11.
*/
import React, {Component} from 'react';
import './custom-item.css';
// 引入另一個CheckBox輪子
import CheckBox from '../../common/check-box/CheckBox';
// 引入用到的action
import {changeCustomItemCompletionStatusAction} from '../../../redux/custom/actions';
// 引入connect函數(shù)用來生成Redux組件
import {connect} from 'react-redux';
class CustomItem extends Component {
render() {
return (
<li className="custom-item"
style={{
textDecoration: this.props.completed ? 'line-through' : 'none'
}}
onClick={() => {
console.log('習慣項點擊 customItem click id', this.props.id, 'completed', this.props.completed);
// 發(fā)送改變習慣項完成狀態(tài)Action
this.props.dispatch(changeCustomItemCompletionStatusAction(this.props.id));
}}>
<span>{this.props.text} {'' + this.props.completed}</span>
<CheckBox checked={this.props.completed}/>
</li>
);
}
}
// 不需要綁定state.customList作為props,也沒法綁。。因為不知道具體是哪個;所以讓父組件去綁就好了
// 也不需要dispatch,因為組件內(nèi)部已處理了
// 所以直接調(diào)用connect()方法把CustomItem組件變成Redux組件就好了
CustomItem = connect()(CustomItem);
export default CustomItem;
注釋都寫在代碼里了。。運行一下可以就看到現(xiàn)象了