likes
comments
collection
share

这就是编程:你应该拥有属于自己的React状态管理器库

作者站长头像
站长
· 阅读数 12

前言

“编程,就像一场开卷考试,题目的答案取决于对书本内容的熟悉程度;而一份源代码就好比一本书,正所谓读书百遍其义自见,读懂源码就读懂了编程!”

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,下期同一时间再见👋🏻。

参考资料