Redux详解
Redux简介
Redux是一个集中存储和管理全局状态的库,通过reducer纯函数来定义怎样更新state,然后通过dispatch action更新state,state每次更新都要返回一个新对象,也就是说state是不可变的。
基于reducer纯函数和不可变state,redux应用可以实现时间旅行式的调试;
特点:
- 单一数据源store;
- 纯函数reducer;
- 不可变state;
标准(传统、Legacy)Redux
// 1.1 定义action
enum ActionType {
INCREASE,
DECREASE
}
const increase = () => {
return {
type: ActionType.INCREASE,
}
}
const decrease = () => {
return {
type: ActionType.DECREASE,
}
}
// 1.2 定义reducers
const reducer1 = (state, action) => {
if (action.type === ActionType.INCREASE) {
return { ...state, count: state.count + 1 };
}
if (action.type === ActionType.DECREASE) {
return { ...state, count: state.count - 1 };
}
return state;
}
const reducer2 = (state, action) => {
//....
}
const reducers = combineReducer({ counter: reducer1, others: reducer2 });
// 2. 创建store
const initialState = {
counter: {
count: 0
},
others: {
// ...
}
};
const enhancer = compose(
applyMiddleware(thunk, logger),
DevTools.instrument()
);
const store = createStore(reducers, initialState, enhancer);
// React组件
function ReactComponent () {
const [num, setNum] = React.useState(state.getState().counter.count);
const handleClickEvent = () => {
// 4. dispatch action
store.dispatch(increase());
}
React.useEffect(() => {
// 3. 订阅更新
const unsubscribe = store.subscribe(() => {
// 5. 更新React state
setNum(store.getState().count);
})
// 6. 组件卸载时,解除订阅
return () => {
unsubscribe();
}
}, [store]);
return <div>
<p>{ num }</p>
<button onClick={handleClickEvent}>+ 1</button>
</div>;
}
异步Action解决方案:redux-thunk,redux-promise
// redux-thunk
const thunkActionCreator = () => {
return function thunkAction (dispatch, getState) {
setTimeout(() => {
dispatch(someAction);
}, 1000);
}
}
dispatch(thunkActionCreator());
// dispatch会调用redux-thunk注入的逻辑:执行thunk函数,传入dispatch和getState,返回执行结果
// 所以dispatch(thunk)的执行结果就是thunk()的执行结果
// 进一步地,thunk如果返回一个Promise(或任何其他值),就可以链式调用Promise的一系列方法,或者通过async function编写流程化的action
// 例如
const fetchBlogs = (uid) => {
return async (dispatch, getState) => {
const userInfo = await fetchUserInfo(uid);
// 用户详细数据还未加载,则加载进redux store
if (!getState().userInfo) {
dispatch({ type: "USER_INFO", payload: userInfo });
}
const blogs = await fetchBlogsByUserInfo(userInfo);
dispatch({ type: "BLOGS_LISt", payload: blogs });
}
}
// redux-promise
// FSA,Flux Standard Action,只包含以下属性: type (required), payload, error, meta
// 1. 第一种写法,Promise
// redux-promise通过then取得resolved value of Promise,然后dispatch resolved value of Promise
const asyncActionCreator1 = () => new Promise((resolve) => {
setTimeout(() => {
resolve({
type: "PROMISE",
value: 11111
});
}, 1000);
})
// 2. 第二种,FSA
// 外层payload初始化时就会执行executor,然后redux-promise通过promise.then取得resolved value of Promise,然后dispatch一个对象{...action, payload: resolved value of Promise}
const asyncActionCreator2 = () => ({
type: "FSA",
payload: new Promise((resolve) => {
setTimeout(() => {
resolve(22222);
}, 1000);
})
})
// reducer略有不同
const reducer = (state, action) => {
switch (action.type) {
case "PROMISE": return { ...state, value: action.value };
case "FSA": return { ...state, value: action.payload };
default: return state;
}
}
dispatch(asyncActionCreator1());
dispatch(asyncActionCreator2());
简化react组件订阅redux store的过程 —— react-redux
Hooks:
- useSelector,构建redux state选择器,并订阅redux state的更新
- useDispatch,获取dispatch函数,用于分发action
- useStore,获取redux store实例
Components:
- Provider,其实就是react内置组件Context.Provider
- connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?),是一个HOC组件,用于向react组件内注入redux state和dispatch函数,使组件订阅redux state的更新
- mapStateToProps(state, ownProps?),用于将redux state注入组件的props中,并使组件订阅redux state的更新,可以访问到ownProps(具体含义见下文细节)
- mapDispatchToProps
- mapDispatchToProps(dispatch, ownProps?)用于将dispatch封装为各种action的派发函数,并注入connected component,同样可以访问到ownProps
- mapDispatchToPropsObj,作用相同,但是一个action creators构成的对象,这个对象会传递给bindActionCreators,将结果注入connected component,该形式更为简便,redux推荐使用该形式注入action派发函数
- mergeProps(stateProps,dispatchProps,ownProps)用于合并stateProps、dispatchProps和ownProps,默认返回
{ ...ownProps, ...stateProps, ...dispatchProps } - options
- context,只有v6可用,用户可提供自定义的context
- areStatesEqual,默认为严格比较(===),决定是否重新执行mapStateToProps;
- areOwnPropsEqual,默认为浅比较,决定ownProps前后是否一致
- areStatePropsEqual,默认为浅比较,决定stateProps前后是否一致
- areMergedPropsEqual,默认为浅比较,决定mergeProps前后是否一致
- forwardRef,只有v6可用,设为true,则connect执行后得到的HOC将ref转发到connected components
Utils:
- bindActionCreators(),第一个参数为action creator或action creators构成的对象,第二个参数为dispatch,对于单个action creator会返回一个绑定dispatch后的匿名函数,对于action creators对象会返回一个具有同样key的对象,其value是绑定dispatch后的函数;
- batch(),本质是react的unstable_batchedUpdates,将多次dispatch合并进一次组件渲染中,react18默认启用batched updates特性,所以不必使用该api;
性能优化:
- 通过selector缓存数据提取函数;
- mapStateToProps应当是纯函数,应该是同步执行的,尽量避免耗时操作或只在输入改变时执行耗时操作,只在必要时返回一个新的引用;
细节:
- ownProps即传递给connected component的props;
- 对于mapStateToProps,state或ownProps发生改变会导致mapStateToProps的重新调用;
- connect实现了一个浅比较的shouldComponentUpdate,当stateProps或ownProps的引用改变或字段改变,会触发重渲染;
- 若mapStateToProps未定义,则connected component不会订阅redux state的更新;
- 若mapDispatchToProps未定义,connected component的props会接收到dispatch;若定义了,则不会传入dispatch;
重点:
现代化的react-redux不推荐通过connect集成react与redux store,取而代之的是直接在函数组件中使用useSelector、useDispatch
原理:
- 数据传递
Provider在顶层提供一个Context.Provider,并将store以context value形式提供给下层组件,connect从context中取到store,并对mapStateToProps、mapDispatchToProps、mergeProps进行一系列计算,将计算得到的props注入被connect的组件
- 组件更新
7.0版本是通过connect执行后得到HOC,HOC内订阅store更新,并通过useReducer更新子组件; 8.0版本更新组件的方式改为使用useSyncExternalStore,通过redux store的subscribe订阅store的更新,dispatch action之后store发生变化,执行react通过subscribe注册进来的listener,然后react触发一次同步更新(其实之后在render和commit阶段还会进行store一致性检查,一旦此时store改变,就再触发一次同步更新)
现代Redux —— Redux Toolkit(RTK)
API:
- configureStore,创建、初始化store,内置了redux-thunk等middleware
- createSlice,定义局部的reducer,自动生成action creator
- createAsyncThunk,创建thunk action creator
// store.js
const store = configureStore({
user: userSlice.reducer
})
// userSlice.js
const name = "user"
const userSlice = createSlice({
name,
initialState: {
name: "暂未登录"
uid: undefined,
status: "idle"
},
reducers: {
resetUserInfo: (state, action) => {
// ...
}
}
// 一般处理thunk action的回调dispatch的action
extraReducers: (builder) => {
builder
.addCase(login.pending, (state) => {
state.status = "pending";
})
.addCase(login.fulfilled, (state, action) => {
state.status = "fulfilled";
state = {...state, ...action.payload};
})
.addCase(login.rejected, (state, action) => {
state.status = "rejected";
state.error = action.payload;
})
}
})
const login = createAsyncThunk(`${name}/login`, async (args, thunkAPI) => {
const {uname, pwd} = args;
const user = await loginApi(uname, pwd);
return user;
})
// Login.jsx
function Login() {
const dispatch = useDispatch();
const isLoading = useSelector((state) => state.status === "pending");
const [name, setName] = useState("");
const [pwd, setPwd] = useState("");
return <div>
<div>
<label>Name</label>
<input value={name} onChange={(e) => {
setName(e.currentTarget.value);
}}>
</div>
<div>
<label>Password</label>
<input value={pwd} onChange={(e) => {
setPwd(e.currentTarget.value);
}}>
</div>
<button disabled={isLoading} onClick={() => {
dispatch(login(name, pwd))
}}> Login </button>
</div>
}
// UserInfo.jsx
const UserInfo = () => {
const {name, pwd} = useSelector((state) => state.user);
return <div>
<p>Name: {name}</p>
<p>Password: {pwd}</p>
</div>
}
// App.jsx
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<Login />
<UserInfo />
</Provider>
)