Redux 入门
5.1 Redux 概念简述
单单靠组件间传值太麻烦了,因此 Redux 应运而生。Redux 是数据层框架,把数据放在 store 这个共用存储空间里。其他组件取数据也在 store 里取。
Redux = Reducer + Flux
5.2 Redux 的工作流程
下面一个形象的比喻来解释 Redux Flow 工作流程:图书馆借书
React Component: 借书的用户
Action Creators: 借书所说的话
Store: 图书馆管理员
Reducers: 管理员的记录本
转化成代码理解:
组件要获取 Store 里的数据,Action Creator 创建语句后告诉 Store,Store 接收到了但是不知道要什么数据,所以传给了 Reducer,然后 Reducer 告诉 Store 应该给哪些数据。
5.3 实战:使用 antd 编写 TodoList 页面布局
Ant Design of React 是一个基于 React 的 UI 库,可以快速搭建有着漂亮样式的前端页面。
import React, { Component } from "react";
// 引入样式
import "antd/dist/antd.css";
// 引入组件
import { Button, Input, List } from "antd";
export default class TodoList extends Component {
render() {
const data = [
"Racing car sprays burning fuel into crowd.",
"Japanese princess to wed commoner.",
"Australian walks 100km after outback crash.",
"Man charged over missing wedding girl.",
"Los Angeles battles huge wildfires.",
];
return (
<div style={{ marginTop: "10px", marginLeft: "10px" }}>
<Input
placeholder="todo info"
style={{ width: "300px", marginRight: "10px" }}
/>
<Button type="primary">提交</Button>
<List
style={{ marginTop: "10px", width: "300px" }}
bordered
dataSource={data}
renderItem={(item) => <List.Item>{item}</List.Item>}
/>
</div>
);
}
}
效果图:
5.4 Store 的创建
安装 Redux 包后,在 src 目录下创建 store 文件夹,并添加 index.js 文件。
-
创建 store:
import { createStore } from "redux"; export const store = createStore();
-
同时创建一个文件,名字叫 reducer.js
// 有两个默认数据 const defaultState = { inputValue: "hhhh", list: [1, 2], }; // state 为 store 仓库里存储的数据 const reducer = (state = defaultState, action) => { return state; } export default reducer;
-
把数据传递给 Store
import { createStore } from "redux"; import reducer from "./reducer"; // reducer 让 store 知道了存储的数据 export default createStore(reducer);
-
组件从 store 里获取数据
... import store from "./store"; export default class TodoList extends Component { constructor(props) { super(props); // 使用这个方法便可获取存储的 State this.state = store.getState(); } render() { return ( <div style={{ marginTop: "10px", marginLeft: "10px" }}> <Input value={this.state.inputValue} placeholder="todo info" style={{ width: "300px", marginRight: "10px" }} /> <Button type="primary">提交</Button> <List style={{ marginTop: "10px", width: "300px" }} bordered dataSource={this.state.list} renderItem={(item) => <List.Item>{item}</List.Item>} /> </div> ); } }
-
显示效果
5.5 Action 和 Reducer 的编写
目标:实现功能,当 input 框内容发生改变的时候,Store 里的数据也跟着改变。
-
先安装 chrome 插件 redux -dev-tools
-
在 Store 里添加配置
import { createStore } from "redux"; import reducer from "./reducer"; export default createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() );
-
打开开发者工具,一目了然
-
Input 绑定事件
<Input // 输入结果绑定 value={this.state.inputValue} placeholder="todo info" style={{ width: "300px", marginRight: "10px" }} // 绑定事件 onChange={this.handleInputChange} />
-
事件代码如下,成功把语句传输给 Store
handleInputChange(e) { // 声明 action 语句 const action = { type: "change_input_value", value: e.target.value, }; // action 传给 store store.dispatch(action); }
-
Store 把之前存储的 state 和 action 转发给 Reducers
// reducers.js const reducer = (state = defaultState, action) => { console.log(state, action); return state; };
-
Reducers 返回新的 state 数据给 Store。Store 接收到 newState 后,原本的 state 被替换
// reducers.js const defaultState = { inputValue: "hhhh", list: [1, 2], }; // Reducer 可以接受 state,但是决不能修改 state const reducer = (state = defaultState, action) => { if (action.type === "change_input_value") { // 对之前的 state 进行深拷贝 const newState = JSON.parse(JSON.stringify(state)); // 把 state 的数据改成新的值 newState.inputValue = action.value; return newState; } return state; }; export default reducer;
-
虽然 Store 里的 state 改变了,但是组件的数据并没有更新,因为还需要组件订阅 Store 的步骤。
订阅后,用订阅触发的方法来获取新数据即可。
constructor(props) { super(props); this.state = store.getState(); this.handleInputChange = this.handleInputChange.bind(this); this.handleStoreChange = this.handleStoreChange.bind(this); // 该方法订阅了 store 的改变。store 改变一次,该方法将会执行一次。 store.subscribe(this.handleStoreChange); } handleStoreChange() { // 用 store 里获取的数据来替换原本的数据 this.setState(store.getState()); }
-
继续实现添加按钮添加 item 的方法
// TodoList.js import React, { Component } from "react"; import "antd/dist/antd.css"; import { Button, Input, List } from "antd"; import store from "./store"; export default class TodoList extends Component { constructor(props) { super(props); this.state = store.getState(); this.handleInputChange = this.handleInputChange.bind(this); this.handleStoreChange = this.handleStoreChange.bind(this); this.handleBtnClick = this.handleBtnClick.bind(this); // 订阅了 store 的改变。store 改变一次,该方法将会执行一次。 store.subscribe(this.handleStoreChange); } handleStoreChange() { // 用 store 里获取的数据来替换原本的数据 this.setState(store.getState()); } handleInputChange(e) { // 声明 action 语句 const action = { type: "change_input_value", value: e.target.value, }; // action 传给 store store.dispatch(action); } handleBtnClick() { const action = { type: "add_todo_item", }; store.dispatch(action); } render() { return ( <div style={{ marginTop: "10px", marginLeft: "10px" }}> <Input value={this.state.inputValue} placeholder="todo info" style={{ width: "300px", marginRight: "10px" }} onChange={this.handleInputChange} /> <Button type="primary" onClick={this.handleBtnClick}>提交</Button> <List style={{ marginTop: "10px", width: "300px" }} bordered dataSource={this.state.list} renderItem={(item) => <List.Item>{item}</List.Item>} /> </div> ); } }
// reducer.js const defaultState = { inputValue: "", list: [], }; // Reducer 可以接受 state,但是决不能修改 state const reducer = (state = defaultState, action) => { if (action.type === "change_input_value") { // 对之前的 state 进行深拷贝 const newState = JSON.parse(JSON.stringify(state)); // 把 state 的数据改成新的值 newState.inputValue = action.value; return newState; } if (action.type === "add_todo_item") { const newState = JSON.parse(JSON.stringify(state)); newState.list.push(newState.inputValue); newState.inputValue = ""; return newState; } return state; }; export default reducer;
5.6 使用 Redux 完成 TodoList 删除功能
// method
handleItemDelete(index) {
const action = {
type: "delete_todo_item",
index
}
store.dispatch(action);
}
// render
<List
style={{ marginTop: "10px", width: "300px" }}
bordered
dataSource={this.state.list}
renderItem={(item, index) => <List.Item onClick={this.handleItemDelete.bind(this, index)}>{item}</List.Item>}
/>
// reducer
if (action.type === "delete_todo_item") {
const newState = JSON.parse(JSON.stringify(state));
newState.list.splice(action.index, 1);
return newState;
}
5.7 ActionTypes 的拆分
上面的方法使用的时候肯定发现了有一个问题,type 是长字符串,一旦拼写出现一丁点错误,则 redux 功能就不好使。
因此 ActionTypes 应运而生。
store 文件夹里新建 actionTypes.js 文件,声明一系列的 actions 名称
// actionTypes.js
export const CHANGE_INPUT_VALUE = "change_input_value";
export const ADD_TODO_ITEM = "add_todo_item";
export const DELETE_TODO_ITEM = "delete_todo_item";
后边其他文件需要用的话,直接导入即可
// TodoList.js && reducer.js
import {
CHANGE_INPUT_VALUE,
ADD_TODO_ITEM,
DELETE_TODO_ITEM,
} from "./actionTypes";
5.8 使用 actionCreator 统一创建 action
上面的 action 的创建方法比较简陋,如果 action 太多,将会到处都是,不方便管理。因此需要 actionCreators 统一创建和管理 action,提高代码的可维护性。
修改前
handleInputChange(e) {
const action = {
type: CHANGE_INPUT_VALUE,
value: e.target.value,
};
store.dispatch(action);
}
优化
创建 actionCreators.js 并在里边定义 action
// actionCreators.js
import { CHANGE_INPUT_VALUE } from "./actionTypes";
// 返回 action
export const getInputChangeAction = (value) => ({
type: CHANGE_INPUT_VALUE,
value,
});
在组件代码里修改后:
handleInputChange(e) {
store.dispatch(getInputChangeAction(e.target.value));
}
完整代码
TodoList.js
import React, { Component } from "react";
import "antd/dist/antd.css";
import { Button, Input, List } from "antd";
import store from "./store";
import {
getInputChangeAction,
getBtnClickAction,
getItemDeleteAction,
} from "./store/actionCreators";
export default class TodoList extends Component {
constructor(props) {
super(props);
this.state = store.getState();
this.handleInputChange = this.handleInputChange.bind(this);
this.handleStoreChange = this.handleStoreChange.bind(this);
this.handleBtnClick = this.handleBtnClick.bind(this);
// 订阅了 store 的改变。store 改变一次,该方法将会执行一次。
store.subscribe(this.handleStoreChange);
}
handleStoreChange() {
// 用 store 里获取的数据来替换原本的数据
this.setState(store.getState());
}
handleInputChange(e) {
store.dispatch(getInputChangeAction(e.target.value));
}
handleBtnClick() {
store.dispatch(getBtnClickAction());
}
handleItemDelete(index) {
store.dispatch(getItemDeleteAction(index));
}
render() {
return (
<div style={{ marginTop: "10px", marginLeft: "10px" }}>
<Input
value={this.state.inputValue}
placeholder="todo info"
style={{ width: "300px", marginRight: "10px" }}
onChange={this.handleInputChange}
/>
<Button type="primary" onClick={this.handleBtnClick}>
提交
</Button>
<List
style={{ marginTop: "10px", width: "300px" }}
bordered
dataSource={this.state.list}
renderItem={(item, index) => (
<List.Item onClick={this.handleItemDelete.bind(this, index)}>
{item}
</List.Item>
)}
/>
</div>
);
}
}
reducer.js
import {
CHANGE_INPUT_VALUE,
ADD_TODO_ITEM,
DELETE_TODO_ITEM,
} from "./actionTypes";
const defaultState = {
inputValue: "",
list: [],
};
// Reducer 可以接受 state,但是决不能修改 state
const reducer = (state = defaultState, action) => {
if (action.type === CHANGE_INPUT_VALUE) {
// 对之前的 state 进行深拷贝
const newState = JSON.parse(JSON.stringify(state));
// 把 state 的数据改成新的值
newState.inputValue = action.value;
return newState;
}
if (action.type === ADD_TODO_ITEM) {
const newState = JSON.parse(JSON.stringify(state));
newState.list.push(newState.inputValue);
newState.inputValue = "";
return newState;
}
if (action.type === DELETE_TODO_ITEM) {
const newState = JSON.parse(JSON.stringify(state));
newState.list.splice(action.index, 1);
return newState;
}
return state;
};
export default reducer;
actionCreator.js
import { ADD_TODO_ITEM, CHANGE_INPUT_VALUE, DELETE_TODO_ITEM } from "./actionTypes";
export const getInputChangeAction = (value) => ({
type: CHANGE_INPUT_VALUE,
value,
});
export const getBtnClickAction = () => ({
type: ADD_TODO_ITEM
})
export const getItemDeleteAction = (index) => ({
type: DELETE_TODO_ITEM,
index,
})
actionTypes.js
export const CHANGE_INPUT_VALUE = "change_input_value";
export const ADD_TODO_ITEM = "add_todo_item";
export const DELETE_TODO_ITEM = "delete_todo_item";
store/index.js
import { createStore } from "redux";
import reducer from "./reducer";
export default createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
5.9 Redux 知识点复习补充
Redux 设计和使用的三项原则
- store 是唯一的。
- 只有 store 能够改变自己的内容。store 里的数据不是 reducer 更新的,而是拿到 reducer 返回的数据,自己对自己的内容更新。因此 reducer 里改变 state 需要先深拷贝。
- reducer 必须是个纯函数。纯函数指的是,指定固定的输入,就一定会有固定的输出,且不会有任何副作用。另外一种话来讲,输入给定的时候输出固定永恒不变,且不对输入参数进行修改(副作用),即为纯函数。
常用 API:
- createStore
- store.dispatch
- store.getState
- store.subscribe