react本身能夠完成動態數據的監聽和更新,如果不是必要可以不適用redux。 安裝redux: cnpm install redux --save,或者yarn add redux。 一、react基本用法 redux是獨立的用於狀態管理的第三方包,它建立狀態機來對單項數據進行管理。 上圖是個人粗 ...
react本身能夠完成動態數據的監聽和更新,如果不是必要可以不適用redux。
安裝redux: cnpm install redux --save,或者yarn add redux。
一、react基本用法
redux是獨立的用於狀態管理的第三方包,它建立狀態機來對單項數據進行管理。
上圖是個人粗淺的理解。用代碼驗證一下:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from "redux";
function reducer(state={name: "monkey", age: 5000}, action){
switch (action.type){
case "add":
state.age ++;
return state;
case "dec":
if (state.age <= 4995){
state.name = "small monkey";
}
state.age --;
return state;
default:
return state;
}
}
const store = createStore(reducer);
const add = {type: "add"}, dec={type: "dec"};
class App extends React.Component{
render (){
const style={
display: "inline-block",
width: "150px",
height: "40px",
backgroundColor: "rgb(173, 173, 173)",
color: "white",
marginRight: "20px"
};
const store = this.props.store;
// console.log(store.getState());
const obj = store.getState();
// console.log(obj);
return (
<div>
<h2>this { obj.name } is { obj.age } years old.</h2>
<button style={style} onClick={()=>store.dispatch(this.props.add)}>增加一歲</button>
<button style={style} onClick={()=>store.dispatch(this.props.dec)}>減少一歲</button>
</div>
)
}
}
function render() {
ReactDOM.render(<App store={store} add={ add } dec={ dec } />, document.getElementById('root'));
}
render();
store.subscribe(render);
因為action必須是個對象,所以只能寫成add = {type: "add"}的形式,而不能直接寫參數"add"。同樣地,在reducer中寫switch時將action.type作為參數。
action和state一一對應,要使用action必須要在reducer里聲明。
redux沒有用state來實現動態數據更新,而是通過props來傳遞數據,因此在組件內部只能通過props獲取store,以及store.getState()獲取state。
redux將ReactDOM.render進行了一次封裝來設置監聽。
redux對數據和組件進行瞭解耦,因而可以進行文件拆分。
把action寫成函數的形式:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from "redux";
const Add = "add", Dec="dec";
function reducer(state={name: "monkey", age: 5000}, action){
switch (action.type){
case Add:
state.age ++;
if (state.age > 5005){
state.name = "old monkey";
}
return state;
case Dec:
if (state.age <= 4995){
state.name = "small monkey";
}
state.age --;
return state;
default:
return state;
}
}
const store = createStore(reducer);
class App extends React.Component{
render (){
const style={
display: "inline-block",
width: "150px",
height: "40px",
backgroundColor: "rgb(173, 173, 173)",
color: "white",
marginRight: "20px"
};
const store = this.props.store;
const state = store.getState();
return (
<div>
<h2>this { state.name } is { state.age } years old.</h2>
<button style={style} onClick={()=>store.dispatch(add())}>增加一歲</button>
<button style={style} onClick={()=>store.dispatch(dec())}>減少一歲</button>
</div>
)
}
}
function render() {
ReactDOM.render(<App store={store} add={ add } dec={ dec } />, document.getElementById('root'));
}
render();
store.subscribe(render);
或者也可以寫成原始的:
// reducer.js const defaultState = { inputValue: '', list: [1, 2, 3] }; // reducer可以接受state,但是無法修改state,所以只能拷貝修改 export default (state = defaultState, action) => { if(action.type === 'change_input_value'){ const newState = JSON.parse(JSON.stringify(state)); // 深拷貝原來的數據 newState.inputValue = action.value; return newState; }; if(action.type === 'add_item'){ const newState = JSON.parse(JSON.stringify(state)); newState.list.push(newState.inputValue); return newState; } if(action.type === 'delete_item'){ const newState = JSON.parse(JSON.stringify(state)); newState.list.splice(action.value, 1); return newState; } return state; }
// store.js import {createStore} from 'redux'; import reducer from './reducer'; const store = createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); export default store;
// ReduxDemo.js import React, {Fragment} from 'react'; import 'antd/dist/antd.css'; import { Input, Button, Row, Col, List } from 'antd'; import store from './store'; class ReduxDemo extends React.Component{ constructor(props){ super(props); this.state = store.getState(); this.handleBtnClick = this.handleBtnClick.bind(this); this.handleInpChange = this.handleInpChange.bind(this); this.handleStoreChange = this.handleStoreChange.bind(this); this.handleItemDelete = this.handleItemDelete.bind(this); // 使用subscribe監聽函數時一定要事先綁定到this上 store.subscribe(this.handleStoreChange); } render(){ return ( <Fragment> <Row gutter={24} size="large"> <Col span={12} offset={4} style={{padding: "0px"}}> <Input defaultValue='todo info' style={{height:"50px"}} value={this.state.inputValue} onChange={this.handleInpChange} /> </Col> <Col span={2} style={{padding:"0px"}}> <Button type='primary' style={{width:"100%", height:"50px"}} onClick={this.handleBtnClick} >submit</Button> </Col> </Row> <List bordered={1} dataSource={this.state.list} renderItem={ (item, index) => ( <List.Item column={12} onClick={this.handleItemDelete.bind(this,index)} >{item}</List.Item> ) } /> </Fragment> ) } handleBtnClick(){ const action = { type: 'add_item' }; store.dispatch(action); } handleInpChange(e) { const action = { type: 'change_input_value', value: e.target.value }; store.dispatch(action); } handleStoreChange(){ this.setState(()=>store.getState()); }; handleItemDelete(index){ const action = { type: 'delete_item', value: index }; store.dispatch(action); // dispatch被監聽函數調用,並將action提交給store,store將上一次的數據狀態state和本次的action一起交給reducer去執行邏輯處理 } } export default ReduxDemo;
或者改為:
// actioncreators export const CHANGE_INPUT_VALUE = 'change_input_value'; export const ADD_ITEM = 'add_item'; export const DELETE_ITEM = 'delete_item'; export const getInpChangeAction = (value)=>({ type: CHANGE_INPUT_VALUE, value }); export const getAddItemAction = ()=>({ type: ADD_ITEM, }); export const getDeleteItemAction = (index)=>({ type: DELETE_ITEM, value: index });
// reducers import {CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM} from './actionCreators' const defaultState = { inputValue: '', list: [1, 2, 3] }; // reducer可以接受state,但是無法修改state,所以只能拷貝修改 export default (state = defaultState, action) => { if(action.type === CHANGE_INPUT_VALUE){ const newState = JSON.parse(JSON.stringify(state)); // 深拷貝原來的數據 newState.inputValue = action.value; return newState; }; if(action.type === ADD_ITEM){ const newState = JSON.parse(JSON.stringify(state)); newState.list.push(newState.inputValue); return newState; } if(action.type === DELETE_ITEM){ const newState = JSON.parse(JSON.stringify(state)); newState.list.splice(action.value, 1); return newState; } return state; }
// store import {createStore} from 'redux'; import reducer from './reducer'; const store = createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); export default store;
// ReduxDemo import React, {Fragment} from 'react'; import 'antd/dist/antd.css'; import { Input, Button, Row, Col, List } from 'antd'; import store from './store'; import {getAddItemAction, getDeleteItemAction, getInpChangeAction } from './actionCreators'; class ReduxDemo extends React.Component{ constructor(props){ super(props); this.state = store.getState(); this.handleBtnClick = this.handleBtnClick.bind(this); this.handleInpChange = this.handleInpChange.bind(this); this.handleStoreChange = this.handleStoreChange.bind(this); this.handleItemDelete = this.handleItemDelete.bind(this); // 使用subscribe監聽函數時一定要事先綁定到this上 store.subscribe(this.handleStoreChange); } render(){ return ( <Fragment> <Row gutter={24} size="large"> <Col span={12} offset={4} style={{padding: "0px"}}> <Input defaultValue='todo info' style={{height:"50px"}} value={this.state.inputValue} onChange={this.handleInpChange} /> </Col> <Col span={2} style={{padding:"0px"}}> <Button type='primary' style={{width:"100%", height:"50px"}} onClick={this.handleBtnClick} >submit</Button> </Col> </Row> <List bordered={1} dataSource={this.state.list} renderItem={ (item, index) => ( <List.Item column={12} onClick={this.handleItemDelete.bind(this,index)} >{item}</List.Item> ) } /> </Fragment> ) } handleBtnClick(){ const action = getAddItemAction(); store.dispatch(action); } handleInpChange(e) { const action = getInpChangeAction(e.target.value); store.dispatch(action); } handleStoreChange(){ this.setState(()=>store.getState()); }; handleItemDelete(index){ const action = getDeleteItemAction(index); store.dispatch(action); } } export default ReduxDemo;
二、UI組件與容器組件拆解
容器組件負責邏輯處理,UI組件只負責頁面顯示。基於這樣的邏輯,可以將上述的ReduxDemo進行拆解。
// 容器組件
import React from 'react';
import store from './store';
import ReduxDemoUI from './ReduxDemoUI';
import {getAddItemAction, getDeleteItemAction, getInpChangeAction } from './actionCreators';
class ReduxDemo extends React.Component{
constructor(props){
super(props);
this.state = store.getState();
this.handleBtnClick = this.handleBtnClick.bind(this);
this.handleInpChange = this.handleInpChange.bind(this);
this.handleStoreChange = this.handleStoreChange.bind(this);
this.handleItemDelete = this.handleItemDelete.bind(this);
// 使用subscribe監聽函數時一定要事先綁定到this上
store.subscribe(this.handleStoreChange);
}
render(){
return (
<ReduxDemoUI
inputValue={this.state.inputValue}
list={this.state.list}
handleBtnClick={this.handleBtnClick}
handleInpChange={this.handleInpChange}
handleStoreChange={this.handleStoreChange}
handleItemDelete={this.handleItemDelete}
/>)
}
handleBtnClick(){
const action = getAddItemAction();
store.dispatch(action);
}
handleInpChange(e) {
const action = getInpChangeAction(e.target.value);
store.dispatch(action);
}
handleStoreChange(){
this.setState(()=>store.getState());
};
handleItemDelete(index){
const action = getDeleteItemAction(index);
store.dispatch(action);
}
}
export default ReduxDemo;
// UI組件
import React, {Fragment} from 'react';
import 'antd/dist/antd.css';
import { Input, Button, Row, Col, List } from 'antd';
class ReduxDemoUI extends React.Component{
render(){
return (
<Fragment>
<Row gutter={24} size="large">
<Col span={12} offset={4} style={{padding: "0px"}}>
<Input defaultValue='todo info' style={{height:"50px"}} value={this.props.inputValue}
onChange={this.props.handleInpChange}
/>
</Col>
<Col span={2} style={{padding:"0px"}}>
<Button
type='primary'
style={{width:"100%", height:"50px"}}
onClick={this.props.handleBtnClick}
>submit</Button>
</Col>
</Row>
<List
bordered={1}
dataSource={this.props.list}
renderItem={
(item, index) => (
<List.Item column={12}
onClick={(index) => {this.props.handleItemDelete(index)}}
>{item}</List.Item>
)
}
/>
</Fragment>
)
}
}
export default ReduxDemoUI;
當一個組件只有render函數時,可以改寫成無狀態組件,無狀態組件只是一個函數,因此性能要比UI組件高。
// 將ReduxDemoUI改寫 import React, {Fragment} from 'react'; import 'antd/dist/antd.css'; import { Input, Button, Row, Col, List } from 'antd'; export const ReduxDemoUI = (props) => { return (<Fragment> <Row gutter={24} size="large"> <Col span={12} offset={4} style={{padding: "0px"}}> <Input defaultValue='todo info' style={{height: "50px"}} value={props.inputValue} onChange={props.handleInpChange} /> </Col> <Col span={2} style={{padding: "0px"}}> <Button type='primary' style={{width: "100%", height: "50px"}} onClick={props.handleBtnClick} >submit</Button> </Col> </Row> <List bordered={1} dataSource={props.list} renderItem={ (item, index) => ( <List.Item column={12} onClick={() => { props.handleItemDelete(index) }} >{item}</List.Item> ) } /> </Fragment>)};
// ReduxDemo只修改導入方式 // import ReduxDemoUI from './ReduxDemoUI'; import {ReduxDemoUI} from './ReduxDemoUI';
三、請求數據渲染
利用redux+axios發送數據,獲取數據並將數據渲染。
// actionCreators export const CHANGE_INPUT_VALUE = 'change_input_value'; export const ADD_ITEM = 'add_item'; export const DELETE_ITEM = 'delete_item'; export const AXIO_LIST = 'axios_list'; // 添加狀態 export const getInpChangeAction = (value)=>({ type: CHANGE_INPUT_VALUE, value }); export const getAddItemAction = ()=>({ type: ADD_ITEM, }); export const getDeleteItemAction = (index)=>({ type: DELETE_ITEM, value: index });
// 添加action export const getAxiosListAction = (list) => ({ type: AXIO_LIST, list });
// reducers import {CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM, AXIO_LIST} from './actionCreators' const defaultState = { inputValue: '', list: [] }; export default (state = defaultState, action) => { if(action.type === CHANGE_INPUT_VALUE){ const newState = JSON.parse(JSON.stringify(state)); // 深拷貝原來的數據 newState.inputValue = action.value; return newState; }; if(action.type === ADD_ITEM){ const newState = JSON.parse(JSON.stringify(state)); newState.list.push(newState.inputValue); return newState; } if(action.type === DELETE_ITEM){ const newState = JSON.parse(JSON.stringify(state)); newState.list.splice(action.value, 1); return newState; }
// 添加狀態處理 if(action.type === AXIO_LIST){ const newState = JSON.parse(JSON.stringify(state)); newState.list = action.list; return newState; } return state; }
// ReduxDemo import React from 'react'; import store from './store'; import axios from 'axios'; import {ReduxDemoUI} from './ReduxDemoUI'; import {getAddItemAction, getDeleteItemAction, getInpChangeAction, getAxiosListAction } from './actionCreators'; class ReduxDemo extends React.Component{ constructor(props){ super(props); this.state = store.getState(); this.handleBtnClick = this.handleBtnClick.bind(this); this.handleInpChange = this.handleInpChange.bind(this); this.handleStoreChange = this.handleStoreChange.bind(this); this.handleItemDelete = this.handleItemDelete.bind(this); // 使用subscribe監聽函數時一定要事先綁定到this上 store.subscribe(this.handleStoreChange); } render(){ return ( <ReduxDemoUI inputValue={this.state.inputValue} list={this.state.list} handleBtnClick={this.handleBtnClick} handleInpChange={this.handleInpChange} handleStoreChange={this.handleStoreChange} handleItemDelete={this.handleItemDelete} />) } handleBtnClick(){ const action = getAddItemAction(); store.dispatch(action); } handleInpChange(e) { const action = getInpChangeAction(e.target.value); store.dispatch(action); } handleStoreChange(){ this.setState(()=>store.getState()); }; handleItemDelete(index){ const action = getDeleteItemAction(index); store.dispatch(action); }
// 發送請求,獲取list,並交給store,store將action和list交給reducer處理,然後store在constructor中監聽返回
// 然後storec調用store.dispatch將reducer更新後的數據更新到組件的setState中,隨後render方法開始渲染 componentDidMount() { axios({ method: 'get', url: 'http://localhost:8080/ccts/node/test', responseType: 'json' }).then((response) => { // 請求成功時 const action = getAxiosListAction(response.data.list); store.dispatch(action); }).catch((error) => { console.log(error); }); } } export default ReduxDemo;
store.js和ReduxDemoUI.js保持不變。啟動一個後臺服務,這裡用java起了一個後臺來傳遞list。註意在package.json中設置proxy為"http://localhost:8080"。
@CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/node") public class Service { @RequestMapping(value = {"/test"}, method = {RequestMethod.GET}) @ResponseBody public String test(HttpServletRequest request){ // consumes = "application/json") Map<String, ArrayList> map = new HashMap<String, ArrayList>(1); ArrayList<String> strings = new ArrayList<String>(); strings.add("張三"); strings.add("李四"); strings.add("王二"); strings.add("麻子"); map.put("list", strings); return JSON.toJSONString(map); } }
四、redux非同步處理
當一個父組件中有多個子組件時,如果每一個組件發生狀態變化,store都要重新渲染一遍。使用非同步可以只重新渲染髮生狀態變化的組件。
安裝redux和thunk的中間件redux-thunk:cnpm install redux-thunk --save。引入兩個函數,並修改createStore如下,其餘代碼保持不變:
import { createStore, applyMiddleware } from "redux";
import thunk from 'redux-thunk';
const store = createStore(reducer, applyMiddleware(thunk));
當然還可以使用compose在控制台添加調試功能,前提是這需要在瀏覽器中安裝redux插件,百度下載redux-devtools.crx,並直接拖動到谷歌瀏覽器擴展程式頁面(這就是安裝了)。
import { createStore, applyMiddleware, compose } from "redux";
const store = createStore(
reducer,
compose(
applyMiddleware(thunk),
window.devToolsExtension?window.devToolsExtension():f=>f
)
);
最終代碼如下:
import {createStore, applyMiddleware, compose} from 'redux'; import thunk from 'redux-thunk'; import reducer from './reducer'; const store = createStore( reducer, compose( applyMiddleware(thunk), window.devToolsExtension?window.devToolsExtension():f=>f ) ); export default store;
thunk支持非同步請求,它的action可以是一個函數。為了啟用非同步請求,對請求進行改寫。
thunk中間件指的是store和action之間的中間件。當store.dispatch被執行時,會檢測action是一個對象或是一個函數,當是一個函數時,會自動執行這個函數並將結果再交給store。然後和對象那種處理方式一樣,store將action交給reducer處理。
// actionCreators添加如下方法 export const getAxiosListThunk = () => { // 返回一個函數,它可以自動接收一個名為dispatch的方法,處理非同步 return (dispatch) => { axios({ method: 'get', url: 'http://localhost:8080/ccts/node/test', responseType: 'json' }).then((response) => { // 請求成功時 const action = getAxiosListAction(response.data.list); dispatch(action); // 只將store.dispatch()改寫成了dispatch() }).catch((error) => { console.log(error); }); }; };
// ReduxDemo中修改componnetDidMount componentDidMount() { const action = getAxiosListThunk(); store.dispatch(action); // dispatch會自動調用和執行action函數 // axios({ // method: 'get', // url: 'http://localhost:8080/ccts/node/test', // responseType: 'json' // }).then((response) => { // // 請求成功時 // const action = getAxiosListAction(response.data.list); // store.dispatch(action); // }).catch((error) => { // console.log(error); // }); }
五、組件間與狀態機、狀態機與狀態機
內層的組件調用dispatch,那麼它的父組件一直到最外層組件都需要使用store屬性一層一層地傳遞狀態機。
狀態機與狀態機之間需要使用redux中的combineReducers合併成一個對象。並由dispatch調用的action來查找其對應的reducer一級返回的state。
舉個例子:
在src目錄下新建index.redux.js文件,建立兩個reducer以及各自的action。
// src/index.redux.js
const Add = "add", Dec="dec";
export function reducerOne(state={name: "monkey", age: 5000}, action){
switch (action.type){
case Add:
state.age ++;
if (state.age > 5005){
state.name = "old monkey";
}
return state;
case Dec:
if (state.age <= 4995){
state.name = "small monkey";
}
state.age --;
return state;
default:
return state;
}
}
export function add() {
return {type: Add}
}
export function dec() {
return {type: Dec}
}
export function reducerTwo(state="孫悟空", action){
if(action.type >= 5005){
state = "鬥戰勝佛";
}else if(action.type <= 4995){
state = "小獼猴";
}
return state;
}
export function monkey(number) {
return {type: number}
}
在src/index.js中寫入如下代碼:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createSto