redux
Redux
是 JavaScript
应用的状态容器,提供可预测的状态管理。它的核心概念和之前用过的 Vuex
类似但是核心思想会有不一样的地方。
- 对于
State
的维护redux
使用reducer
的概念而Vuex
使用的是mutations
action
不能直接修改state
数据而是通过调用对应的reducer
基本用法
示例代码
import { createStore } from "redux";
import { createContext, useContext } from "react";
const initState = {
name: "sam",
age: 20,
};
function reducer(state = initState, action) {
switch (action.type) {
case "SET_NAME":
return {
...state,
name: action.payload,
};
case "SET_AGE":
return {
...state,
age: action.payload,
};
default:
return state;
}
}
const store = createStore(reducer);
const StoreContext = createContext();
export { store, StoreContext };
// 组件中使用
function Home() {
const { getState, dispatch, subscribe } = useContext(StoreContext);
const { name } = getState();
const [, forceRender] = useState(0);
useEffect(() => {
subscribe(() => forceRender(Math.random()));
}, []);
return <div>{name}</div>;
}
function App() {
return (
<>
<StoreContext.Provider value={store}>
<Home />
</StoreContext.Provider>
</>
);
}
注意的点
- 需要自身创建一个
Context
,然后通过Provider
传递给子组件 - 需要使用
useContext
获取store
实例 - 为了能够让页面同步发生变化需要有一个强制渲染的函数,使组件能够重新渲染
- 如果页面中有多个
reducer
需要使用combineReducers
进行合并成一个rootState
进行管理,并通过key
将state
进行区分。实现类似命名空间的效果
原理
createStore
这个函数是一个核心需要收集每一个 reducer
中的状态,并且需要用户提供更新组件的方法。
function createStore(reducer) {
let currentState = undefined;
let currentListeners = [];
function getState() {
return currentState;
}
function subscribe(listener) {
if (!currentListeners.includes(listener)) {
currentListeners.push(listener);
}
// 返回卸载的方法
return function () {
currentListeners = currentListeners.filter((item) => item !== listener);
};
}
function dispatch(action) {
let newState = reducer(currentState, action);
if (currentState !== newState) {
// 通知组件进行更新
currentListeners.forEach((listener) => listener());
currentState = newState;
}
}
// 初始化调用一次 dispatch 拿到初始化的 state
dispatch({ type: "@@REDUX/INIT" });
return { getState, subscribe, dispatch };
}
combineReducers
这个函数是用来合并多个 reducer
函数的,并且需要将合并后的 state
进行命名空间的管理。
function combineReducers(reducers) {
const rootState = {};
for (const key in reducers) {
const reducer = reducers[key];
rootState[key] = reducer(undefined, { type: "@@REDUX/INIT" });
}
return function combineReducer(state, action) {
let hasChanged = false;
const nextState = {};
for (const key in reducers) {
const reducer = reducers[key];
// 不管上一次是否有命中 action 都会继续去执行,所以就要规范 action 的命名需要唯一
const nextStateForKey = reducer(rootState[key], action);
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== rootState[key];
}
return hasChanged ? nextState : state;
};
}
// 用法
const reducer = combineReducers({
user: userReducer,
login: loginReducer,
});
const store = createStore(reducer);
const { user, login } = store.getState();
缺陷
- 缺少追踪异步数据的能力
- 结合
react
需要手动加上重复代码,如 获取上下文、更新组件方法等
react-redux
react-redux
是一个 react
应用的 redux
库,它提供了一些 react
组件和函数,可以方便地在 react
应用中使用 redux
。解决了上面的 痛点 2
核心思想一致,但是在用法上做了对应调整。
基本用法
import { Provider, connect } from "react-redux";
import { createStore } from "redux";
import { createContext, useContext } from "react";
const initState = {
name: "sam",
age: 20,
};
function reducer(state = initState, action) {
switch (action.type) {
case "SET_NAME":
return {
...state,
name: action.payload,
};
case "SET_AGE":
return {
...state,
age: action.payload,
};
default:
return state;
}
}
const store = createStore(reducer);
export { store };
// 组件中使用
connect(
(state) => ({ name: state.name }),
(dispatch) => ({
setName: (name) => dispatch({ type: "SET_NAME", payload: name }),
})
)(function Home(props) {
const { name, dispatch } = props;
return <div>{name}</div>;
});
function App() {
return (
<>
<Provider store={store}>
<Home />
</Provider>
</>
);
}
核心就是通过 connect
包裹需要接受 Store
的组件,并通过返回的映射状态和 dispatch 传递到组件的props
中接收,确实少了很多重复代码。
原理分析
Provider
import { createContext } from "react";
const Context = createContext();
function Provider({ store, children }) {
return <Context.Provider value={store}>{children}</Context.Provider>;
}
export { Provider, Context };
主要目的还是要拿到 store
的内容,然后再传递下去,再者就是方便 connect
函数的获取 state
。
connect
import { useContext, useMemo, useState, useEffect } from "react";
function connect(mapStateToProps, mapDispatchToProps) {
return function () {
return function Hoc(props) {
const { getState, dispatch, subscribe } = useContext(Context);
if (!mapStateToProps) {
mapStateToProps = (state) => ({});
}
if (!mapDispatchToProps) {
mapDispatchToProps = (dispatch) => ({ dispatch });
}
const [, forceUpdate] = useState(0);
useEffect(() => {
subscribe(() => forceUpdate(Math.random()));
}, []);
const state = getState();
const nextStateProps = useMemo(
() => mapStateToProps(state, props),
[state]
);
const dispatchProps = mapDispatchToProps(dispatch);
return <Component {...props} {...nextStateProps} {...dispatchProps} />;
};
};
}
这个只是简略的写法源码中还有大量的判断和处理,这里就不一一贴出来了。主要就是利用 Hoc
的形式对组件重新处理,让它可以少些一些重复性的代码。
useSelector 和 useDispatch
对于上面 connect
的代码来说可能不太美观还需要去理解 HOC,最重要的是通过 props
传递可能会有一些意想不到的 bug
。比如属性覆盖等等,所以就出现了 useSelector/useDispatch
来简化这一操作
function Home(props) {
const name = useSelector((state) => state.name);
const dispatch = useDispatch();
const changeName = useCallback(() => {
dispatch({ type: "SET_NAME", payload: "123" + Math.random() });
}, [dispatch]);
return (
<>
{name}
<button onClick={changeName}>点击我</button>
</>
);
}
function App() {
return (
<>
<Provider store={store}>
<Home />
</Provider>
</>
);
}
WARNING
如果想要使用多个状态的需要调用多次 useSelector
。不然会有重复渲染的情况
https://redux.js.org/usage/deriving-data-selectors#optimizing-selectors-with-memoization
// ❌ WARNING: this _always_ returns a new reference, so it will _always_ re-render!
const { name, age } = useSelector((state) => ({
name: state.name,
age: state.age,
}));
// good
const name = useSelector((state) => state.name);
const age = useSelector((state) => state.age);
redux-thunk/redux-promise
我们经常会有需求是经过异步请求后再去修改状态,redux 本身是不支持这种操作的,所以就出现了 redux-thunk/redux-promise
这两个中间件。
function Home(props) {
const name = useSelector((state) => state.name);
const dispatch = useDispatch();
const validName = useCallback(async () => {
await sleep(3000);
return { type: "SET_NAME", payload: "123" + Math.random() };
}, [dispatch]);
const changeName = useCallback(() => {
dispatch(validName());
}, [dispatch]);
return (
<>
{name}
<button onClick={changeName}>点击我</button>
</>
);
}
这个时候因为 validName
执行返回的是一个 Promise
,并不会去等待这个 Promise
执行完毕再去执行 dispatch
所以就导致这个 dispatch(Promise)
是无效的,且会发出报错。
DANGER
createStore.ts:201 Uncaught Error: Actions must be plain objects. Instead, the actual type was: 'Promise'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions
这两个都是让 redux
拥有异步处理 action
能力的中间件。用法会不一样,两者选其一就行
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { rootReducer } from "./reducer";
import promise from "redux-promise";
const store = createStore(reducer, applyMiddleware(thunk));
const store = createStore(reducer, applyMiddleware(promise));
基本用法
Details
// redux-thunk
function Home(props) {
const name = useSelector((state) => state.name);
const dispatch = useDispatch();
// const validName = useCallback(async () => {
// await sleep(3000);
// return { type: "SET_NAME", payload: "123" + Math.random() };
// }, [dispatch]);
const validName = useCallback(() => {
return async (dispatch) => {
await sleep(3000);
dispatch({ type: "SET_NAME", payload: "123" + Math.random() });
};
}, [dispatch]);
const changeName = useCallback(() => {
dispatch(validName());
}, [dispatch]);
return (
<>
{name}
<button onClick={changeName}>点击我</button>
</>
);
}
// redux-promise
function Home(props) {
const name = useSelector((state) => state.name);
const dispatch = useDispatch();
// const validName = useCallback(async () => {
// await sleep(3000);
// return { type: "SET_NAME", payload: "123" + Math.random() };
// }, [dispatch]);
const validName = useCallback(() => {
await sleep(3000);
dispatch({ type: "SET_NAME", payload: "123" + Math.random() });
}, [dispatch]);
const changeName = useCallback(() => {
dispatch(validName());
}, [dispatch]);
return (
<>
{name}
<button onClick={changeName}>点击我</button>
</>
);
}
原理分析
中间件原理
redux-thunk/redux-promise
是利用了 redux
的中间件机制,所以很有必要了解下 redux
的中间件是个什么东西。
加入了 middleware
的流程张这样
一个符合 redux
规范的中间件长这样
function middleware(store) {
return function (next) {
return function (action) {
// 中间件的逻辑
next(); // 调用下一个中间件
};
};
}
其中 next
表示下一个中间件执行,也就是说在中间件中能够控制什么时机让下一个中间件执行。试想我们可不可以这样子
function middleware(store) {
return function (next) {
return function (action) {
// 中间件的逻辑
action instanceof Promise ? action.then(() => next()) : next();
};
};
}
这样子好像就可以解决 action 是异步调用的问题了。搞定收工!
这样的中间件再被 applyMiddleware
方法调用,主要流程如下
根据上面的结构我们我们来看看 applyMiddleware
方法的实现是怎么样的
function applyMiddleware(...middlewares) {
return function (createStore) {
return function (reducer, preloadedState) {
const store = createStore(reducer, preloadedState);
const middlewareAPI = {
getState: store.getState,
dispatch: store.dispatch,
};
const chain = middlewares.map((middleware) => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch,
};
};
};
}
function compose(...funcs) {
// 这里因为我们需要按照 middleware 的顺序执行,而 next 又是 下一个中间件
// 顺序执行的话,对于如何拿下一个中间件 又是一件麻烦事,所以我们可以倒序执行
return (dispatch) => {
for (let i = funcs.length - 1; i >= 0; i--) {
// 这样就拿到下一个中间件,然后通过闭包的机制存储起来
// 这里的 dispatch[i] = function (next){ return function(action){}}
// dispatch[i] 执行后通过闭包就保存了 next 变量
// 这里实现的很妙
dispatch = funcs[i](dispatch);
}
// 返回第一个中间件
return dispatch;
};
}
其中 compose
在源码中还有一个简写版本 ,但是功能跟上一个是一样的
function compose(...funs) {
if (funs.length === 0) return (arg) => arg;
if (funs.length === 1) return funs[0];
/// a(b(c(d))
return funs.reduce(
(a, b) =>
(...args) =>
a(b(...args))
);
}
redux-thunk
理解 redux
的中间件原理后,对于 redux-thunk
就很好理解了
function thunk({ dispatch, getState }) {
return function (next) {
return function (action) {
if (action === "function") {
return action(dispatch, getState);
}
return next(action);
};
};
}
// 结合使用理解
const dispatch = useDispatch();
const validName = useCallback(() => {
return async (dispatch) => {
await sleep(3000);
dispatch({ type: "SET_NAME", payload: "123" + Math.random() });
};
}, [dispatch]);
const changeName = useCallback(() => {
dispatch(validName());
}, [dispatch]);
redux-promise
这个就更好理解了
import isPromise from "is-promise";
import { isFSA } from "flux-standard-action";
function promiseMiddleware({ dispatch }) {
return (next) => (action) => {
if (!isFSA(action)) {
return isPromise(action) ? action.then(dispatch) : next(action);
}
return isPromise(action.payload)
? action.payload
.then((result) => dispatch({ ...action, payload: result }))
.catch((error) => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
})
: next(action);
};
}
isFSA
则是检查 action
是否符合这种格式 action creator
也是一个函数,其返回值必须是一个payload
为promise
的FSA
,FSA
全称flux standard action
,意指符合flux
标准的action
,该action
的判断函数如下:
{
type: 'ADD_TODO',
payload: {
text: 'Do something.'
}
}
{
type:'SET_EMOJIS1',
payload:fetch('https://api.github.com/emojis').then(res=>res.json())
}
redux-saga
todo
dva
todo
@reduxjs/toolkit
Redux Toolkit
以两个关键的 API
开始,这简化了在每个 Redux
应用中常见的操作:
configureStore
通过单个函数调用设置一个配置完善的Redux store
,包括合并reducer
、添加thunk
中间件以及设置Redux DevTools
集成。与createStore
相比更容易配置,因为它接受命名选项参数。createSlice
让你使用Immer
库 来编写reducer
,可以使用"mutating"
JS 语法,比如state.value = 123
,不需要使用拓展运算符。 内部基于你的reducer
名称生成action type
字符串。最后,它在TypeScript
中表现的很好。
基本用法
先来看看没有带异步操作的用法
import { createSlice, configureStore } from "@reduxjs/toolkit";
const userModule = createSlice({
name: "user",
initialState: {
name: "sme",
age: 20,
},
reducers: {
updateName(state, action) {
state.name = action.payload;
},
},
});
export const { updateName } = userModule.actions;
export const store = configureStore({
reducer: {
user: userModule.reducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
// 使用
function Home(props) {
const name = useSelector((state) => state.name);
const dispatch = useDispatch();
const changeName = useCallback(() => {
// 直接通过 暴露出来的 action 来派发
dispatch(updateName("123" + Math.random()));
}, [dispatch]);
return (
<>
{name}
<button onClick={changeName}>点击我</button>
</>
);
}
从目前为止看,好像对比以前的代码是简单了不少,而且逻辑也比较分明。唯一一点就是 需要手动导出 Slice
中的 action
再来看看如果有异步的情况
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve("");
}, ms);
});
}
export const asyncUpdateName = createAsyncThunk(
"user/asyncUpdateName",
async (value: string) => {
await sleep(3000);
console.log("异步执行完毕", value);
return {
name: "async_sme",
};
}
);
const userModule = createSlice({
name: "user",
initialState: {
name: "sme",
age: 20,
},
reducers: {
updateName(state, action) {
state.name = action.payload;
},
},
extraReducers(builder) {
// 异步action 的各种状态
builder
.addCase(asyncUpdateName.pending, (state, action) => {
state.name = "loading";
})
.addCase(asyncUpdateName.fulfilled, (state, { payload }) => {
state.name = payload.name;
})
.addCase(asyncUpdateName.rejected, (state, { payload }) => {
state.name = "error";
});
},
});
function Home(props) {
const name = useSelector((state) => state.name);
const dispatch = useDispatch();
const changeName = useCallback(() => {
// 直接通过 暴露出来的 action 来派发
dispatch2(asyncUpdateName("asymc555555"));
}, [dispatch]);
return (
<>
{name}
<button onClick={changeName}>点击我</button>
</>
);
}
emmmmm,看起来就很头疼。。。。明明只是调用一个异步函数而已,却要写一大堆方法
另外有一个小技巧,因为我们每一个useDispatch
和 useSelector
都要写类型,实际上有一种简洁的办法
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// 二次封装:对useDispatch,useSelector进行封装,解决每次使用都要导入RootState,AppDispatch
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
总结
这篇文章也是对 redux
系列的一次学习,是没想到为什么一个状态管理会变得如此复杂...