这就是编程:你应该拥有属于自己的React状态管理器库
前言
“编程,就像一场开卷考试,题目的答案取决于对书本内容的熟悉程度;而一份源代码就好比一本书,正所谓读书百遍其义自见,读懂源码就读懂了编程!”
Hello,《这就是编程》每周六晚如期更新,带大家走进编程世界,一起读懂源码,读懂编程!
经过前面二期对Promise及async/await实现原理的深入分析:
相信已经对JavaScript异步编程不再犯怵!
那本期要读什么源码呢?🤔
本期先不读源码了,一起来写源码,从零开始实现一个极简版React状态管理器!
Let's start!
准备工作
首先想象一下状态管理器应该是什么样子:
- 模块化,需要把整个项目的复杂状态分解到多个模块里,每一个模块拥有自己的命名空间;模块间相互独立,又相互联系
- 配置化原子reducer,通过它导出对应的actionCreator,调用actionCreator就能创建特定action来泛化调用对应的原子reducer获取下一个的state
- 当然还要支持同步或异步effect,且要能在effect内部非常方便的获取当前state以及调用其他模块的effect
OK,以上面的标准样子来实现状态管理器。现在假设每个模块model的数据结构是:
interface State { [string]: any };
interface Model {
state: State,
reducers: { [ActionType]: (prevState: State, payload: Payload) => State },
effects: {} | (dispatch): {} => {}
}
通过useStore这个自定义hook来创建状态管理器:
function useStore(models, options = {}) {}
全文以如下models为例:
const models = {
counter: {
state: { count: 0 },
reducers: {
increment(state, payload) {
return { ...state, count: state.count + 1 }
},
decrement(state, payload) {
return { ...state, count: state.count - 1 };
},
},
effects: {
incrementSync() {
this.increment();
},
incrementAsync() {
setTimeout(() => {
this.increment();
}, 5e3);
}
},
}
}
Reducer
reducer接收action和上一个state生成下一个state,需要通过reducer导出actionCreator,用reducer的方法名作为actionCreator创建的action.type。
通过这个思路,首先对model的reducers进行标准化:
const commonReducers = {
setState(state, payload) {
return {
...state,
...payload,
};
},
};
首先把所有model的通用reducer提取出来作为commonReducers,这里添加setState作为每一个model更新状态值的通用reducer。
开始标准化处理:
const normalizedModels = useMemo(() => {
return mapValues(models, (model, modelKey) => {
const { state, reducers = {}, effects } = model;
const normalizedReducers = { ...commonReducers, ...reducers, };
// create root reducer for every model
const reducerForModelKey = createReducer(normalizedReducers, modelKey);
// create all actionCreators for every model
const actionCreators = getActionCreators(normalizedReducers, modelKey);
return {
state,
reducer: reducerForModelKey,
actionCreators,
effects,
};
});
}, [models]);
对每一个model的reducers对象,通过createReducer创建支持泛化调用的独立mixed reducer,即通过action.type从reducers对象中映射找到对应的原子reducer进行action处理生成下一个的state:
export function createReducer(reducers, namespace) {
return (state, action = {}) => {
const { type: namespacedType, payload = {}, meta, error } = action;
const [ns, type] = namespacedType.split('/');
if (namespace !== ns) {
return state;
}
const reducer = reducers[type];
if (!reducer) {
console.warn(`actionType '${type}' not found`);
return state;
}
return reducer(state, payload, { meta, error });
};
}
同时对reducers的每一个方法名,通过getActionCreators创建对应的actionCreator,调用actionCreator就能得到与reducer方法名对应的action。
然后对于action.type来说,为了区分不同model的同类型action,还需要添加命名空间前缀,这里就使用models配置里每个model的key值来作为namespace,如例子中的"counter":
export function getActionCreators(reducers, namespace) {
if (!reducers) {
return {};
}
return Object.keys(reducers).reduce((memo, type) => ({
...memo,
[type]: createAction(type, namespace, reducers[type].prepareAction),
}), {});
}
通过createAction,一个reducer导出一个actionCreator:
export function createAction(type, namespace, prepareAction) {
const namespacedType = `${namespace}/${type}`;
function actionCreator(...args) {
if (prepareAction) {
let prepared = prepareAction(...args);
if (!prepared) {
throw new Error('prepareAction did not return an object');
}
return {
type: namespacedType,
payload: prepared.payload,
...('meta' in prepared && { meta: prepared.meta }),
...('error' in prepared && { error: prepared.error }),
};
}
return { type: namespacedType, payload: args[0] };
}
adoptActionCreatorType(namespacedType, actionCreator);
return actionCreator;
}
对于模块counter来说,得到的actionCreators的结构是这样:
{
increment: () => ({ type: 'counter/increment' })
...
}
即每当调用increment方法,就会得到{ type: 'counter/increment' }的action,然后就能映射到reducers对象里的increment方法:
increment(state, payload) {
return { ...state, count: state.count + 1 }
}
最后将每一个model的mixed reducer通过combineReducers合并成root reducer:
const rootReducer = useMemo(() =>
combineReducers(mapValues(normalizedModels, (model) => model.reducer)),
[normalizedModels]
);
combineReducers返回combination reducer,当它响应action时,在它内部每一个model的mixed reducer都会响应action处理生成下一个的state:
export function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers);
const finalReducers = {};
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key];
}
}
const finalReducerKeys = Object.keys(finalReducers);
return function combination(state = {}, action) {
let hasChanged = false;
const nextState = {};
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i];
const reducer = finalReducers[key];
const previousStateForKey = state[key];
const nextStateForKey = reducer(previousStateForKey, action);
if (typeof nextStateForKey === 'undefined') {
const actionType = action && action.type;
throw new Error(
`When called with an action of type ${actionType ? `"${String(actionType)}"` : '(unknown type)'}, the slice reducer for key "${key}" returned undefined. ` +
`To ignore an action, you must explicitly return the previous state. ` +
`If you want this reducer to hold no value, you can return null instead of undefined.`);
}
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length;
return hasChanged ? nextState : state;
};
}
Dispatch
有了root reducer之后,还需要一个dispatch方法来触发action,并在其中调用root reducer来响应这个action生成下一个state。
首先获取初始state值,也就是将所有model的初始state配置提取合并到一起:
// get the all initial state
const getInitialState = useCallback(() => {
return mapValues(normalizedModels, (model) => model.state);
}, [normalizedModels]);
到目前为止,已经得到了initialState、rootReducer、actionCreators,现在就需要创建dispatch方法。大家直觉反应会使用useReducer:
const [state, dispatch] = useReducer(rootReducer, initialState);
但是事实上这里不会使用useReducer,而是使用useState:
const [dummyState, dispatchInner] = useState();
之所以没有使用useReducer,是因为希望所有模块的状态值更新都是同步的。这里通过useState获取dispatchInner仅仅作为rerender trigger。这里的dummyState不会被使用,而是将state值缓存在一个ref上,同时以初始state值作为默认值,这样在初次渲染时,就能获取到初始state值了:
// get the sync state for dispatcher
const syncState = useRef(getInitialState());
因为dispatchInner只是rerender trigger而不能触发action,因此还需要将dispatchInner封装成最终dispatch来触发action。因为dispatch不像dispatchInner那样在React里保持引用,这里需要经过useCallback处理:
// get the facade dispatch
const dispatch = useCallback((action) => {
syncState.current = rootReducer(syncState.current, action);
batch(() => dispatchInner({}));
return action;
}, [dispatchInner]);
syncState.current = rootReducer(syncState.current, action);
当dispatch调用时,rootReducer响应当前action处理得到下一个的state值后,ref上的state也一起同步更新,这样就能实现在所有的effects里能直接获得当前最新的state值了,状态管理器的state值流转不再依赖组件更新。
同时将ref挂到dispatch.getState上,子组件可以通过useContext从状态管理器上获取到最新的state值:
// get the facade state getter for dispatcher
dispatch.getState = () => syncState.current ?? {};
注意:
dispatch在内部调用dispatchInner来触发rerender,但是假如在同一时间多次触发dispatch,这时dispatchInner也会多次调用导致多次触发rerender,所以还需要一个批处理机制。
在React18里默认自带批处理,笔者使用的不是React18
function useBatch() {
const last = useRef(0);
const id = useRef(null);
const reset = () => {
if (id.current) {
clearTimeout(id.current);
id.current = null;
}
};
const batch = (cb) => {
const now = Date.now();
const next = Math.max(0, (1000 / 60) - (now - last.current));
last.current = next + now;
reset();
id.current = setTimeout(cb, Math.round(next));
};
return { batch };
}
简单的通过useBatch得到一个batch方法,将一帧(以满帧60帧为例)中的所有dispatch触发的dispatchInner合并成一次,减少rerender。
Mutation
接下来涉及状态管理器里最重要的环节,就是effects,它们是承载actionCreator的地方,可以是纯粹包含多个actionCreator的同步effect,也可能是包含异步数据请求等异步处理的异步effect:
useMemo(() => {
Object.keys(normalizedModels).forEach((modelKey) => {
const { actionCreators, effects } = normalizedModels[modelKey];
const actions = bindActionCreators(actionCreators, dispatch);
const effectsForModel = mapValues(
typeof effects === 'function' ? effects(dispatch) : effects,
(effect) => (payload) => {
return effect.apply(dispatch[modelKey], [payload, dispatch.getState(), dispatch]);
});
// attach both actionCreators and effects into dispatcher
assign(dispatch, { [modelKey]: { ...actions, ...effectsForModel } });
});
}, [normalizedModels, dispatch]);
ActionCreator
因为effect的基础是actionCreator,需要通过它们创建action来触发状态更新。
前面得到的actionCreators还只是单纯的action creator,之所以纯粹,因为调用它们只会得到对应的action对象,但是最终希望它们能直接触发action,所以还需要对所有的actionCreators进行bindActionCreators处理,将dispatch绑定到actionCreators上:
export function bindActionCreator(actionCreator, dispatch) {
function bound(...args) {
return dispatch(actionCreator.apply(this, args));
}
adoptActionCreatorType(actionCreator.type, bound);
return bound;
}
export function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch);
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error(`bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. ` +
`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`);
}
const boundActionCreators = {};
for (const key in actionCreators) {
const actionCreator = actionCreators[key];
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
}
}
return boundActionCreators;
}
bindActionCreator的处理非常简单,就是创建一个闭包函数,在内部将所有入参透传到actionCreator上,然后直接调用dispatch来触发由actionCreator创建的action。
Effect
而对于effects,它们既可以是同步effect,也可以是异步effect,将它们标准化为如下所示:
(payload) => {
return effect.apply(dispatch[modelKey], [payload, dispatch.getState(), dispatch]);
}
这样统一了model配置中的effect的入参:
- paylaod,effect的负载,可以是任意类型,但为了方便,可以对象方式传参
- dispatch.getState(),即syncState.current,因为会在每一次dispatch触发后被最新的state值覆盖,所以每一个effect里就能同步获取最新的state值,这样就可以非常方便的进行同步逻辑处理,尤其是对state取值及判断。笔者个人认为这是一个蛮好的设计,你怎么认为呢?欢迎留言讨论。👏🏻
注意:
由于在一个model中可能调用其他model,因此model的effects配置项中每一个原始effect需要获取dispatch引用,所以effects配置项的类型可以是:
- function,接收dispatch为入参
- effect.apply(dispatch[modelKey], [payload, dispatch.getState(),dispatch]),effect标准化过程中给每一个原始effect增加dispatch为参数
最后将所有actionCreators及effects以model名为key挂到dispatch上,这样effect中this指针就指向了自己所处的model,即dispatch[modelKey]:
// attach both actionCreators and effects into dispatcher
assign(dispatch, { [modelKey]: { ...actions, ...effectsForModel } });
这样就能通过dispatch直接调用actionCreators及effects了,比如对于上面的model counter:
dispatch.counter.increment();
dispatch.counter.decrement();
OK,到这里,store就创建完毕了,最后将它返回给组件消费:
const store = {
get state() {
return dispatch.getState();
},
dispatch,
};
return store;
Context
现在有了useStore,还需要创建context来承接。通过React的createContext获取Provider及Consumer,同时增加两个自定义hook。
export const Context = createContext(null);
// export the facade Provider with store embeded
export const Provider = function Provider(props) {
const store = useStore(props.models);
return <Context.Provider value={store}>{props.children}</Context.Provider>;
};
export const Comsumer = Context.Comsumer;
一个useSelector用于获取某一model下的state:
export function useSelector(selector) {
const { state } = useContext(Context);
return typeof selector === 'function' ? selector(state) : state;
}
一个useDispatch用于在子组件中获取dispatch,然后也能通过dispatch.getState获取到state值,以及从dispatch[modelKey]下获取某个model下的actionCreators及标准化effects。
export function useDispatch() {
const { dispatch } = useContext(Context);
return dispatch;
}
结尾
到这里,一个极简的React状态管理器就完成了,其实完全不需要像redux、@redux/toolkit等等这些额外依赖,现在你拥有了自己的状态管理库😄。
当然,在里面还有很多花样可以玩,比如:
- 统一的loading状态处理,可能需要额外增加一个ref来统一存放关于loading效果的state值
- 异步请求封装,这里推荐一个库react-query
- 生命周期钩子
- 等等...
感兴趣的小伙伴可以自行实现,然后全部代码在这里github.com/binarybutte…,欢迎fork👏🏻,喜欢的话再来个star❤️。
OK,本期先分享到这里,如果读者有任何疑惑,欢迎评论区留言讨论👏🏻。每一次编程创作的过程,都像是在创作一件艺术品,不断打磨,往往最简单的代码实现里会展现精妙的编程巧思,这个过程可以极大的收获编程成就感🤔。
OK,bye bye,下期同一时间再见👋🏻。
参考资料
转载自:https://juejin.cn/post/7206627150622195771