一、Redux 外围
官网是这样解释 Redux 的:JavaScript 状态容器,提供可预测化的状态治理。
const state = {
modleOpen: "yes",
btnClicked: "no",
btnActiveClass: "active",
page: 5,
size: 10
}
- Redux 外围概念及工作流程
- store: 存储状态的容器,JavaScript 对象
- View: 视图,HTML 页面
- Actions: 对象,形容对状态进行怎么的操作
- Reducers: 函数,操作状态并返回新的状态
-
Redux 计数器案例 ../Redux/src/counter
<body> <button id="plus">+</button> <span id="count">0</span> <button id="minus">-</button> <script src="https://cdn.bootcdn.net/ajax/libs/redux/4.2.0/redux.min.js"></script> <script> // 3 存储默认状态 let initialState = {count: 0} // 2 创立 reducer 函数 function reducer (state = initialState, action) { // 7 接管 action 并判断 action 的类型 switch (action.type) { case 'increment': return {count : state.count + 1} case 'decrement': return {count : state.count - 1} default: // 初始化是会主动发送一个 init 的 action 用来存储默认的 state return state; } } // 1 创立 store 对象, createStore 有第二个参数代表默认值,也就是 reducer 中的 state 参数 let store = Redux.createStore(reducer); // 4 定义 action let increment = {type: 'increment'} let decrement = {type: 'decrement'} // 5 获取按钮并增加事件 document.getElementById('plus').onclick = function() { // 6 触发 action store.dispatch(increment); } document.getElementById('minus').onclick = function () {store.dispatch(decrement); } // 8 订阅 store,当 store 发生变化的时候会执行回调 store.subscribe(() => { // 获取 store 中存储的状态 console.log(store.getState()) document.getElementById('count').innerText = store.getState().count;}); </script> </body>
- Redux 外围 API
const store = Redux.crateStore(reducer)
: 创立 Store 容器function reducer (state = initialState, action) {}
: 创立用于解决状态的 reducer 函数store.getState()
: 获取状态store.subscribe(function(){})
: 订阅状态store.dispatch({type: 'discription...'})
: 触发 action
二、React + Redux
1. 在 React 中不应用 Redux 时遇到的问题
在 React 中组件通信的数据流是单向的,顶层组件能够通过 props 属性向上层组件传递数据,而上层组件不能向下层组件传递数据,要实现上层组件批改数据,须要
下层组件传递批改数据办法到上层组件,当我的项目越来越大的时候,组件之间传递数据也就变得越来越艰难。
2. 在 React 我的项目中引入 Redux 的益处
应用 Redux 治理数据,因为 Store 独立于组件,使得数据管理独立于组件,解决了组件与组件之间传递数据艰难的问题。
3. 下载 Redux
npm install redux react-redux
4.Redux 工作流程
- 组件通过 dispatch 办法触发 action
- Store 接管 Action 并将 Action 分发给 Reducer
- Reducer 依据 Action 类型对状态进行更改并将更改后的状态返回给 Store
- 组件订阅了 Store 中的状态,Store 中的状态更新会同步到组件
5. 代码案例
计数器案例 ../Redux/react-redux-guide/src/components/Counter.js
弹出框案例 ../Redux/react-redux-guide/src/components/Modal.js
6. 拆分 Reducer
../Redux/react-redux-guide/src/react-redux-guide/src/store/reducers/root.reducer.js
三、Redux 中间件
中间件实质上就是一个函数,Redux 容许咱们通过中间件的形式,扩大和加强 Redux 应用程序,加强体现在对 action 解决能力上,之前的计数器与弹出框案例中。actions 都是间接被 reducer 函数解决的,再退出了中间件当前,在登程了一个 action 之后这个 action 会优先被中间件解决,当两头解决完这个 action 当前,中间件会把这个 action 传递给 reducer,让 reducer 持续解决
1. 开发 Redux 中间件
开发中间件模版代码,实质上是一个函数,并且是一个柯里化的一个函数
export default store => next => action => {}
这个函数中要求咱们返回一个函数,在这个返回的函数中要求咱们再返回一个函数,在最里层的函数中咱们能够执行咱们本人的业务逻辑,在最外层的函数中给咱们提供了一个参数叫 store,能够用 store.getState 获取以后 state 状态,也能够应用 store.dispatch 来触发另外一个 action,至于干什么具体依据应用的业务逻辑来决定。在最里层的函数也有一个形参,这个形参就是组件触发的 action 对象,能够依据 action.type 来决定是否对以后 action 进行解决。两头的函数也有一个参数,这个参数是一个函数,咱们称之为 next,在咱们执行完了逻辑代码之后,咱们要去调用 next 办法,把以后 action 传递给 reducer,或者说传递给下一个中间件,因为中间件能够有多个。中间件开发好之后须要引入咱们写的中间件并且注册给 redux。
2. 注册中间件
中间件在开发实现当前只有被注册能力在 Redux 的工作流程中失效。
import {createStore, applyMiddleware} from 'redux'
import logger from './middlewares/logger'
createStore(reducer, applyMiddleware(logger))
代码案例:src/react-redux-guide/src/store/middleware
/**
* src/react-redux-guide/src/store/middleware/logger.js
* 开发中间件
* 中间件模板代码 export default store => next => action => {}
*/
export default store => next => action => {console.log(action);
console.log(store);
next(action); // 只有调用了 next 办法才能够将 action 传递给下一个中间件或者 reducer
}
/**
* src/react-redux-guide/src/index.js
*/
import {createStore, applyMiddleware} from 'redux';
import RootReducer from "./reducers/root.reducer";
import logger from "./middleware/logger";
import test from "./middleware/test";
// 注册中间件 applyMiddleware(logger); 中间件的执行程序取决去注册的程序
export default createStore(RootReducer, applyMiddleware(logger, test));
-
中间件通用性革新
/** * src/react-redux-guide/src/store/middleware/thunk.js */ import actionTypes from "../actionTypes"; export default store => next => action => {// if(action.type === actionTypes.countIncrementType || action.type === actionTypes.countDecrementType) { // // 开启提早操作 // setTimeout(() => {// next(action); // },2000) // } // 1. 当这个中间件函数不关怀你想执行什么异步操作,只关怀你执行的是不是异步操作 // 2. 如果你执行的是异步操作,再你触发 action 的时候,给中间件传递一个函数,如果执行的是同步操作就传递一个失常 action 对象 // 3. 异步操作代码要写在传递进来的函数中 // 4. 以后这个中间件的函数在点用你传递进来的函数时,要将 dispatch 办法传递进去 if(typeof action === 'function') {return action(store.dispatch); } next(action); }
/**
* src/react-redux-guide/src/store/middleware/thunk.js
*/
import actionTypes from './actionTypes';
const actions = {
// Counter
countIncrementAction: () => ({type: actionTypes.countIncrementType}),
countDecrementAction: () => ({type: actionTypes.countDecrementType}),
// 异步的 action
countIncrementActionAsync: () => dispatch => {setTimeout(() => {dispatch(actions.countIncrementAction());
}, 2000);
},
// Modal
boxShowAction: () => ({type: actionTypes.boxShowType}),
boxHiddenAction: () => ({type: actionTypes.boxHiddenType}),
boxShowActionAsync: () => dispatch => {setTimeout(() => {dispatch(actions.boxShowAction());
}, 2000)
}
}
export default actions;
3.Redux 罕用中间件
1.redux-thunk
redux-thunk 容许咱们在 redux 的工作流程中应用异步操作,与自定义 thunk 中间件用法雷同 (src/react-redux-guide/src/store/middleware/thunk.js
)
装置:npm install redux-thunk
容许 action 返回一个函数,这个函数承受一个 dispatch 参数,在这个函数当中做异步操作。
2.redux-thunk
redux-sage 能够将异步操作从 ActionCreator 文件中抽离进去,放在一个独自的文件中
1. 创立 redux-sage 中间件
import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware();
2. 注册 sagaMiddleware
createStore(reducer, appleMiddleware(sagaMiddleware));
3. 应用 saga 接管 action 执行一步操作
/**
* src/react-redux-guide/src/store/sagas/counter.saga.js
*/
import {takeEvery, put, delay} from 'redux-saga/effects';
import actionTypes from "../actionTypes";
import actions from "../actionCreators";
/**
* takeEvery: 接管 action
* put: 触发 action
* delay: 提早(这里不能够用 setTimeout)
*/
function* countIncrement() {yield delay(2000);
yield put(actions.countIncrementAction());
}
export default function* counterSaga () {yield takeEvery(actionTypes.COUNTINCREMENTSAGATYPE, countIncrement)
}
4. 启用 saga
/**
* src/react-redux-guide/src/store/index.js
*/
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk'; // 官网中间件
import createSagaMiddleware from 'redux-saga';
import counterSaga from './sagas/counter.saga'
import RootReducer from "./reducers/root.reducer";
// 自定义中间件
// import logger from "./middleware/logger";
// import test from "./middleware/test";
// import thunk from "./middleware/thunk";
// 注册中间件 applyMiddleware(logger); 中间件的执行程序取决去注册的程序
// 创立 saga
const sagaMiddleware = createSagaMiddleware();
// export default createStore(RootReducer, applyMiddleware(thunk));
export default createStore(RootReducer, applyMiddleware(sagaMiddleware));
// 启动 saga
sagaMiddleware.run(counterSaga)
5.saga 中 action 传值
// 能够接管到 action 返回的对象
function* countIncrement(action) {yield delay(2000);
// 获取 action 中的值并传递给下一个 action
yield put(actions.countIncrementAction(action.value));
}
export default function* counterSaga () {yield takeEvery(actionTypes.COUNTINCREMENTSAGATYPE, countIncrement)
}
6.saga 拆分合并
/**
* src/react-redux-guide/src/store/sagas/root.saga.js
*/
import {all} from 'redux-saga/effects';
import counterSaga from "./counter.saga";
import modalSaga from "./modal.saga";
// 合并 Saga
export default function* rootSaga() {
yield all([counterSaga(),
modalSaga()])
}
/**
* src/react-redux-guide/src/store/index.js
*/
// 批改启动 saga
sagaMiddleware.run(rootSaga);
4.redux-actions
redux 流程中大量的样板代码编写是十分苦楚的,应用 redux-actions 能够简化 action 和 reducer 的解决
- 下载
npm install redux-actions
-
创立 Action
/** * src/react-redux-guide/src/store/actions/counter.actions.js */ import {createAction} from 'redux-actions'; // 接管和触发 action 只须要应用 createAction() 的返回值就能够 export const incrementAction = createAction('increment'); export const decrementAction = createAction('decrement');
-
创立 Reducer
/** * src/react-redux-guide/src/store/reducers/counter.reducer.js */ import {handleActions as createReducer} from 'redux-actions'; import * as counterActions from '../actions/counter.actions'; /** * handleActions 返回值就是 reducer 函数 */ const initialState = {count: 0,} const handleIncrement = (state, action) => ({count: state.count + 1}) const handleDecrement = (state, action) => ({count: state.count - 1}) export default createReducer({[counterActions.incrementAction]: handleIncrement, [counterActions.decrementAction]: handleDecrement, }, initialState);
3.redux-actions 传值
redux-action 传值并不需要再 actions 中去接管,默认会挂载到 action.payload 的属性中
/**
* src/react-redux-guide/src/components/Counter.js
*/
import React from 'react';
import {connect} from 'react-redux';
import * as counterActions from '../store/actions/counter.actions';
const Counter = (props) => {const {count, handleCountPlus, handleCountMinus} = props;
return (
<section>
<button onClick={() => handleCountPlus(10)}>Plus</button>
<span>{count}</span>
<button onClick={() => handleCountMinus(10)}>Minus</button>
</section>
)
}
const mapStateToProps = state => ({count: state.counter.count})
const mapDispatchToProps = dispatch => ({handleCountPlus(value) {dispatch(counterActions.incrementAction(value)) // redux-actions
},
handleCountMinus (value){dispatch(counterActions.decrementAction(value)) // redux-actions
}
})
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
/**
* src/react-redux-guide/src/store/actions/counter.actions.js
*/
import {createAction} from 'redux-actions';
// 这里不须要去接管传递的值
// export const incrementAction = createAction('increment', value); 谬误
export const decrementAction = createAction('decrement');
/**
* src/react-redux-guide/src/store/reducers/counter.reducer.js
*/
// 应用 redux-action 简化 reducer
const handleIncrement = (state, action) => ({count: state.count + action.payload})
const handleDecrement = (state, action) => ({count: state.count - action.payload})
export default createReducer({[counterActions.incrementAction]: handleIncrement,
[counterActions.decrementAction]: handleDecrement,
}, initialState);
5. 实战案例
-
src/shopping-demo
:前端文件- 启动
npm start
- 端口
localhost:3000
- 启动
-
src/shoppingCartService
:服务端文件- 启动
npm start
- 端口
localhost:3005
- 启动
四、Redux 源码实现
五、Redux-toolkit
1. 概述
redux toolkit 对 redux 进行的二次封装,用于高效的 Redux 开发,使 Redux 的应用变得简略。
2. 疾速入门
- 创立状态切片
对于状态切片,咱们能够认为他就是本来的 Redux 中的那一个个小的 reducer 函数。
在 Redux 中,本来的 Reducer 函数和 Action 对象须要别离创立,当初通过状态切片来代替,它会返回 reducer 函数和 Action 对象。
import {createSlice} from "@reduxjs/toolkit";
const TODOS_FEATURE_KEY = 'todos';
const {reducer: todoReducer, actions} = createSlice({
name: TODOS_FEATURE_KEY, // 切片名称
initialState: [], // 切片状态
reducers: {
// 创立 actionCreator 和状态处理函数
// addTodo 是 action 的名称
addTodo: (state, action) => {
// 在 redux-toolkit 中应用 createSlice 能够间接批改 state 的值,而并不需要返回一个新的状态
state.push(action.payload);
}
}
});
export const {addTodo} = actions;
export default todoReducer;
2. 创立 store
import {configureStore} from "@reduxjs/toolkit";
import TodosReducer, {TODOS_FEATURE_KEY} from "./todos.slice";
export default configureStore({
reducer: {[TODOS_FEATURE_KEY]: TodosReducer
},
devTools: process.env.NODE_ENV !== 'production'
});
3. 配置 Provider
import React from 'react';
import ReactDOM from 'react-dom/client';
import {Provider} from "react-redux";
import App from './App';
import store from "./Store";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App/>
</Provider>
</React.StrictMode>
);
4. 组件中触发 Action
import {useDispatch, useSelector} from "react-redux";
import {TODOS_FEATURE_KEY, addTodo} from "../../Store/todos.slice";
const Todo = () => {
// 获取 dispatch 办法
const dispatch = useDispatch();
// 获取最新的状态
const todos = useSelector(state => state[TODOS_FEATURE_KEY]);
return (
<div>
<button onClick={() => dispatch(addTodo({title: '测试工作'}))}> 增加工作 </button>
<ul>
{
todos && todos.map(todo => (<li key={todo.title}>{todo.title}</li>
))
}
</ul>
</div>
)
}
export default Todo;
- action 预处理
当 Action 被触发后,能够通过 prepare 办法进行预处理,解决实现后交给 Reduce.prepare 办法必须返回对象
import {createSlice} from "@reduxjs/toolkit";
export const TODOS_FEATURE_KEY = 'todos';
// 应用 reducer.prepare 进行 action 预处理
const {reducer: TodosReducer, actions} = createSlice({
name: TODOS_FEATURE_KEY, // 切片名称
initialState: [], // 切片状态
reducers: {
// 创立 actionCreator
addTodo: {
// 状态处理函数
reducer: (state, action) => {console.log('reducer', action);
state.push(action.payload)
},
// action 预处理,必须返回一个对象 {payload: {...todo, title: 'haha'}},笼罩原有的 payload
prepare: todo => {console.log('prepare', todo)
// 批改 action 传过来的数据
return {payload: {id: Math.random(), ...todo}
}
}
}
}
});
export const {addTodo} = actions;
export default TodosReducer;
- 执行异步的操作形式
第一种形式
import {createSlice, createAsyncThunk} from "@reduxjs/toolkit";
import axios from 'axios';
export const TODOS_FEATURE_KEY = 'todos';
/**
* 异步操作:第一种
* createAsyncThunk('action-type', (payload, thunkAPI) => {异步操作})
*/
// 定义申请办法
const loadTodos = createAsyncThunk('async-load-todos', (payload, thunkAPI) => {axios.get(payload)
.then(res => {
// 触发保留数据 action setTodos
thunkAPI.dispatch(setTodos(res.data));
})
.catch(err => new Error(err))
});
// 应用 reducer.prepare 进行 action 预处理
const {reducer: TodosReducer, actions} = createSlice({
name: TODOS_FEATURE_KEY, // 切片名称
initialState: [], // 切片状态
reducers: {
// 创立 actionCreator
addTodo: {
// 状态处理函数
reducer: (state, action) => {console.log('reducer', action);
state.push(action.payload)
},
// action 预处理,必须返回一个对象 {payload: {...todo, title: 'haha'}},笼罩原有的 payload
prepare: todo => {console.log('prepare', todo)
// 批改 action 传过来的数据
return {payload: {id: Math.random(), ...todo}
}
}
},
setTodos: (state, action) => action.payload
}
});
// jsx
export const {addTodo, setTodos} = actions;
export {loadTodos};
export default TodosReducer;
import {TODOS_FEATURE_KEY, addTodo, loadTodos} from "../../Store/todos.slice";
import {useDispatch, useSelector} from "react-redux";
import {useEffect} from "react";
const Todo = () => {
// // 获取 dispatch 办法
const dispatch = useDispatch();
// // 获取最新的状态
const todos = useSelector(state => state[TODOS_FEATURE_KEY]);
// // 组件挂载实现之后申请
useEffect(() => {dispatch(loadTodos('http://localhost:3001/todos'));
}, [])
return (
<div>
<button onClick={() => dispatch(addTodo({title: '测试工作'}))}> 增加工作 </button>
<ul>
{
todos && todos.map(todo => (<li key={todo.title}>{todo.title}</li>
))
}
</ul>
</div>
)
}
export default Todo;
第二种形式
import {createSlice, createAsyncThunk} from "@reduxjs/toolkit";
import axios from 'axios';
export const TODOS_FEATURE_KEY = 'todos';
/**
* 异步操作:第二种
* createAsyncThunk('action-type', payload => {异步操作})
* 不须要再去编写保留数据的 action, 能够间接在 extraReducers 中解决异步的 action
*/
const loadTodos = createAsyncThunk('async-load-todos', payload => {return axios.get(payload)
.then(res => res.data)
.catch(err => new Error(err));
})
// 应用 reducer.prepare 进行 action 预处理
const {reducer: TodosReducer, actions} = createSlice({
name: TODOS_FEATURE_KEY, // 切片名称
initialState: [], // 切片状态
reducers: {
// 创立 actionCreator
addTodo: {
// 状态处理函数
reducer: (state, action) => {console.log('reducer', action);
state.push(action.payload)
},
// action 预处理,必须返回一个对象 {payload: {...todo, title: 'haha'}},笼罩原有的 payload
prepare: todo => {console.log('prepare', todo)
// 批改 action 传过来的数据
return {payload: {id: Math.random(), ...todo}
}
}
},
setTodos: (state, action) => action.payload
},
// 第二种异步申请编写形式:extraReducers 解决异步申请
extraReducers: {
// 解决 loadTodos 返回的 Promise 对象的胜利状态
[loadTodos.fulfilled]: (state, action) => {console.log('fulfilled');
return action.payload;
},
[loadTodos.pending]: (state, action) => {console.log('pending');
return state;
},
}
});
export const {addTodo, setTodos} = actions;
export {loadTodos};
export default TodosReducer;
// jsx
import {TODOS_FEATURE_KEY, addTodo, loadTodos} from "../../Store/todos.slice";
import {useDispatch, useSelector} from "react-redux";
import {useEffect} from "react";
const Todo = () => {
return (
<div>
<button onClick={() => dispatch(addTodo({title: '测试工作'}))}> 增加工作 </button>
<ul>
{
todos && todos.map(todo => (<li key={todo.title}>{todo.title}</li>
))
}
</ul>
</div>
)
}
export default Todo;
-
配置中间件
import {configureStore, getDefaultMiddleware} from "@reduxjs/toolkit"; import TodosReducer, {TODOS_FEATURE_KEY} from "./todos.slice"; import logger from 'redux-logger'; export default configureStore({ reducer: {[TODOS_FEATURE_KEY]: TodosReducer }, /** * middleware: [] 默认隐式的会挂载 @reduxjs/toolkit 内置的中间件 * 如果显示编写进去并增加自定义或者其余中间件时,须要通过 getDefaultMiddleware()办法获取内置的中间件并挂载下来,不然内置的中间件就会被笼罩 */ middleware: [...getDefaultMiddleware(), logger], devTools: process.env.NODE_ENV !== 'production' });
-
实体适配器
将状态放入实体适配器,实体适配器提供操作状态的各种办法,简化操作。- 创立实体适配器
import {createEntityAdapter} from "@reduxjs/toolkit"; import axios from 'axios'; export const TODOS_FEATURE_KEY = 'todos'; const loadTodos = createAsyncThunk('async-load-todos', (payload, thunkAPI) => {axios.get(payload) .then(res => { // 触发保留数据 action setTodos thunkAPI.dispatch(setTodos(res.data)); }) .catch(err => new Error(err)) }); /** * 创立实体适配器 * todosAdapter.getInitialState(); // entities: {} 存储状态 ids: [] 存储每一条状态的 id */ const todosAdapter = createEntityAdapter(); const {reducer: TodosReducer, actions} = createSlice({ name: TODOS_FEATURE_KEY, // 切片名称 // 将状态放入适配器当中 initialState: todosAdapter.getInitialState(), reducers: { // 创立 actionCreator addTodo: { // 状态处理函数 reducer: (state, action) => { // 应用状态适配器向状态中增加数据 todosAdapter.setOne(state, action.payload); // 增加一条 }, // action 预处理,必须返回一个对象 {payload: {...todo, title: 'haha'}},笼罩原有的 payload prepare: todo => ({payload: {id: Math.random(), ...todo}}) }, setTodos: (state, action) => todosAdapter.setMany(state, action.payload), }, }); export const {addTodo, setTodos} = actions; export {loadTodos}; export default TodosReducer;
-
简化实体适配器
const {reducer: TodosReducer, actions} = createSlice({ name: TODOS_FEATURE_KEY, // 切片名称 // 将状态放入适配器当中 initialState: todosAdapter.getInitialState(), reducers: { // 创立 actionCreator addTodo: { /** * 简化实体适配器函数,自动检测第二个是否是 action 参数,如果是 action 的话就会去找 action.payload 并将其放到 state 中 */ reducer: todosAdapter.setOne, // action 预处理,必须返回一个对象 {payload: {...todo, title: 'haha'}},笼罩原有的 payload prepare: todo => {console.log('prepare', todo) // 批改 action 传过来的数据 return {payload: {id: Math.random(), ...todo} } } }, setTodos: todosAdapter.setMany } });
- 更改实体适配器的惟一示意
实体适配器要求每一个实体必须领有 id 属性作为惟一标识,如果实体中的惟一标识字段不叫作 id, 须要应用 selectId 进行申明。
const todosAdapter = createEntityAdapter({selectId: todo => todo.cid})
-
状态选择器
提供了从实体状态器中获取状态的快捷路径。import {createSelector} from '@reduxjs/toolkit'; /** * 创立状态选择器 * createSelector(相应的 state, 具体的状态选择器(由实体适配器返回) */ const {selectAll} = todosAdapter.getSelectors(); export const selectorTodos = createSelector(state => state[TODOS_FEATURE_KEY], selectAll); // jsx const dispatch = useDispatch(); // 应用状态选择器获取状态 const todos = useSelector(selectorTodos); useEffect(() => {dispatch(loadTodos('http://localhost:3001/todos')); }, [])