【redux】理解并实现redux

前言

  • redux各种地方已经说的差不多了,本篇从头到尾再梳理下。有很多东西不强迫自己写,就懒得写,自己就可能没发现里面一些坑。

原理

  • 很多地方说redux是状态管理器,状态其实就是数据,只是起的名字很牛b。
  • 可以用个比喻来说一下这玩意到底是怎么回事。
  • 比如状态就是个旗子,3个组件abc需要根据旗子行为来做出相应变化,并且组件abc还要能去修改这个旗子。
  • 那么这就产生了一个问题,如果一个组件比如a把旗子改没了,或者把旗子换成可乐了,组件bc不就瞎了?
  • 这种问题怎么解决?基础好的人立马会发现可以用闭包来解决。
  • 先看一个闭包基本例子:
function xxx(){
    let  a = 1
    return {
        add:()=>++a,
        get:()=>a
    }
}
let ins =xxx()
console.log(ins.a);//undefined
console.log(ins.get());//1
console.log(ins.add());//2
console.log(ins.get());//2
console.log(ins.add());//3
console.log(ins.add());//4
console.log(ins.get());//4
  • 可以看见,这里的这个a就相当于上面说的旗子,也就是状态。只要状态放闭包里,那就没人能拿走它。
  • 至于乱改的问题,我们做了一个add方法,这个方法限定这个状态只能这么修改,不能改成别的什么玩意,外界也只能调用这个方法改状态,所以,外界调用某函数并修改状态就是dispatch某个action,
  • 不过这个模型缺少subscribe,其实subscribe就是只要a一改变,监听它的组件会立马收到消息。这个原理就跟发布订阅一样,我们稍微修改下:
function xxx(){
    let listener = []
    let  a = 1
    return {
        add:()=>{
            a++;
            listener.forEach((fn)=>fn())
        },
        get:()=>a,
        subscribe:(listen)=>{
            listener.push(listen)
        }
    }
}
let ins =xxx()
ins.subscribe(()=>{
    console.log(ins.get());
})
ins.add()
ins.add()
  • 只要下面一调用修改函数,订阅函数会立马执行。
  • 这样基本模式就完成了,我们还需要做一些调整,把名字修改的正式一点,另外把订阅函数加个取消订阅的模式:
function createStore(){
    let listeners= []
    let  state = 1
    return {
        dispatch:()=>{
            state++;
            listeners.forEach((fn)=>fn())
        },
        getState:()=>state,
        subscribe:(listener)=>{
            listeners.push(listener)
            return ()=>listeners = listeners.filter((item)=>item!=listener)
        }
    }
}
let ins =createStore()
let x =ins.subscribe(()=>{
    console.log(ins.getState());
})
ins.dispatch()
ins.dispatch()
x()
ins.dispatch()
ins.dispatch()
  • 可以发现这个模式仍然有些局限性,主要在于如果要对状态做出各种修改,我们需要些多个不同名的dispatch来解决,所以,我们可以把改状态的方法全部统一到一个地方reducer来管理,需要什么操作使用action来管理:
function createStore(reducer){
    let listeners= []
    let  state = 1
    return {
        dispatch:(action)=>{
            state = reducer(state,action)
            listeners.forEach((fn)=>fn())
        },
        getState:()=>state,
        subscribe:(listener)=>{
            listeners.push(listener)
            return ()=>listeners = listeners.filter((item)=>item!=listener)
        }
    }
}
function reducer(state,action){
    switch (action.type){
        case 'ADD':
            return ++state;
        case 'MINUS':
            return --state;
    }
}


let ins =createStore(reducer)
let x =ins.subscribe(()=>{
    console.log(ins.getState());
})
ins.dispatch({type:'ADD'})
ins.dispatch({type:'MINUS'})
  • 另外state不可能初始状态是1呀,所以,我们需要给其转初始值。而且,如果只是字面量类型,无法满足多个数据更新,所以需要使用对象来做state。
  • 目前这个也是没法调用赋值的,所以我们对action增加payload字段让其可以传值。
function createStore(reducer,initialState){
    let listeners= []
    let  state = initialState
    return {
        dispatch:(action)=>{
            state = reducer(state,action)
            listeners.forEach((fn)=>fn())
        },
        getState:()=>state,
        subscribe:(listener)=>{
            listeners.push(listener)
            return ()=>listeners = listeners.filter((item)=>item!=listener)
        }
    }
}
function reducer(state,action){
    switch (action.type){
        case 'ADD':
            return {
                ...state,count:state.count+action.payload
            };
        case 'MINUS':
            return {
                ...state,count:state.count-action.payload
            };
    }
    return state
}

let store =createStore(reducer,{count:1})
let x =store.subscribe(()=>{
    console.log(store.getState());
})
store.dispatch({type:'ADD',payload:5})//{count:6}
store.dispatch({type:'MINUS',payload:2})//{count:4}
  • 这里还有个问题,就是写字符串容易写错,所以我们可以把action改成函数形式:
let action = {
     add (payload){return {type:'ADD',payload:payload}},
     minus(payload){return {type:'MINUS',payload:payload}}

}

let store =createStore(reducer,{count:1})
let x =store.subscribe(()=>{
    console.log(store.getState());
})
store.dispatch(action.add(5))//{count:6}
store.dispatch(action.minus(2))//{count:4}

  • 目前来看,基本所有功能都完成了,但是可以发现 ,每次都要手动去dispatch,再action麻烦,不能直接选哪个action就派发么?
  • 那就直接做个函数,传入action和dispatch,然后返回一个重新绑定action为键,值为函数dispatch(action)即可。
let action = {
     add (payload){return {type:'ADD',payload:payload}},
     minus(payload){return {type:'MINUS',payload:payload}}
}
function bindActionCreators(actionCreators,dispatch){
    let boundActionCreators={}
    for(let key in actionCreators){
        boundActionCreators[key]=function(...args){
           return dispatch(actionCreators[key](...args))
        }
    }
    return boundActionCreators
}

let store =createStore(reducer,{count:1})
let x =store.subscribe(()=>{
    console.log(store.getState());
})
let bindAction = bindActionCreators(action,store.dispatch)
bindAction.add(8)
bindAction.minus(2)
  • 这个自动派发指定action的函数就搞定了。
  • 但是一个项目里reducer里的情况会特别多,每个组件实际上都有自己的一套处理状态的方式,这就导致reducer会写的特别长,不好管理。
  • 我们想把每个组件的reducer逻辑给拆到对应组件去,这样不就非常方便了,最后再靠一个函数把所有reducer全部整合:
function createStore(reducer,initialState){
    let listeners= []
    let  state = initialState
    return {
        dispatch:(action)=>{
            state = reducer(state,action)
            listeners.forEach((fn)=>fn())
        },
        getState:()=>state,
        subscribe:(listener)=>{
            listeners.push(listener)
            return ()=>listeners = listeners.filter((item)=>item!=listener)
        }
    }
}
function reducer1(state={count:5},action){//初始值分配到每个reducer上使用
    switch (action.type){
        case 'ADD1':
            return {
                ...state,count:state.count+action.payload
            };
        case 'MINUS1':
            return {
                ...state,count:state.count-action.payload
            };
    }
    return state
}
function reducer2(state={count:2},action){
    switch (action.type){
        case 'ADD2':
            return {
                ...state,count:action.payload
            };
        case 'MINUS2':
            return {
                ...state,count:action.payload
            };
    }
    return state
}

let reducers = {
    reducer1,
    reducer2
}
function conmbineReducers(reducers){
    return function  (state={},action){//这个state和action是createstore的dispatch里面
        let nextState={}
        for (let key in reducers){
            let reducerForKey = reducers[key]//reducer1的fn...
            let previousStateForKey =state[key]//state里reducer1对应的值
            let nextStateForKey = reducerForKey(previousStateForKey,action)//把值传入对应的reducer
            nextState[key]=nextStateForKey//结果给对象{reducer1:newstate,reducer2:newstate}
        }
        return nextState
    } 
}

let reducer = conmbineReducers(reducers)

let action = {
     add (payload){return {type:'ADD1',payload:payload}},
     minus(payload){return {type:'MINUS1',payload:payload}}
}

function bindActionCreators(actionCreators,dispatch){
    let boundActionCreators={}
    for(let key in actionCreators){
        boundActionCreators[key]=function(...args){
           return dispatch(actionCreators[key](...args))
        }
    }
    return boundActionCreators
}


let store =createStore(reducer)
let x =store.subscribe(()=>{
    console.log(store.getState().reducer1.count);
    console.log(store.getState().reducer2.count);
})
let bindAction = bindActionCreators(action,store.dispatch)
bindAction.add(8)
bindAction.minus(2)
//13 2 11 2
  • 我们做2个reducer分别叫reducer1和reducer2,然后使用函数combineReducers来组合,返回一个组合后的reducer。每当有action进行派发,会走这个组合后的reducer,这个reducer会遍历所有的子reducer,取出他们的名字,查找state里对应这个名字的对象。这个对象也就是专属于这个reducer,并把传来的action交给这个reducer处理,最后返回一个新的值。
  • 另外把初始值交给每个reducer,这样更加方便拓展。
  • 实际上说白了就是给state对象里面加了一层。
  • 我个人觉得这个设定有些不好的地方在于要遍历所有的reducer,如果项目大到一定地步比如几千个几万个reducer,可能这个方式不太好。应该再搞个判断,传来的action带什么标志,不用遍历,拿到对应reducer的state后直接走对应的reducer处理。而如果要派发给所有reducer里的数据就直接正常走遍历即可。
  • 还有这个初始值是我们一开始每个组件都传入的,但是如果有组件不传就会报错,所以在开始需要dispach一个谁都不匹配的action.type:dispatch({ type: Symbol() }),来生成对应的state,这样更好一点。
  • 还有个中间件功能,这个功能可以在dispatch时候加一些自己逻辑,最简单的实现方法当然是重写store.dispatch了,但是这样不利于扩展。先介绍下下面这个函数:
function add1(str) {
    return '1' + str;
}
function add2(str) {
    return '2' + str;
}
function add3(str) {
    return '3' + str;
}
function compose (...fns){
    return fns.reduce((a,b)=>((...args)=>a(b(...args))))
}
let composedFn = compose(add3, add2, add1);
let result = composedFn('xxx');
console.log(result);//321xxx
  • 这个compose有个特点,可以返回一个组合所有效果的函数。而每个函数的返回值又会作为参数往下传。
  • 另外还有个好处,就是可以去柯里化,否则要写几个括号还得看有几个中间件。
function thunk({ dispatch, getState }) {
    return function (next) {//next代表调用下一个中间件或者store.dispatch
        return function (action) {//这个函数就是重写后的dispatch方法了
            if (typeof action === 'function') {
                action(dispatch, getState);
            } else {
                next(action);
            }
        }
    }
}
function applyMiddleware(...middlewares) {
    return function (createStore) {
        return function (reducer) {
            let store = createStore(reducer);
            let dispatch;
            let middlewareAPI = {
                getState: store.getState,
                dispatch: action => dispatch(action)
            }
            let chain = middlewares.map(middleware => middleware(middlewareAPI));
            dispatch = compose(...chain)(store.dispatch);
            return { ...store, dispatch }
        }
    }
}
let store = applyMiddleware(promise, thunk, logger)(createStore)(reducer);
  • 这里稍微有点绕,middlewareAPI里的dispatch进行引用赋值,compose后拿到改写的dispatch。
  • 而compose中间件后传的store.dispatch就传递给中间件的next上。
  • 我觉得这种dispatch传值引用方式一般人想不到,写出这代码的对执行顺序上面理解很到位了。
  • redux基本上就这些内容,最后还附加个react-redux上使用的connect跟上面内容有些关系。

react-redux里connect

  • 使用react-redux里面有个connect方法:
export default connect(
    mapStateToProps,
    mapDispatchToProps
)(Component);
  • 这个函数作用是把action和state作为属性对象传给组件,组件可以通过props.add或者props.number得到state或者action。
  • 其中mapStateToProps是一个函数,用来拿到当前组件的state对象,就是类似上面说的命名空间对象。
  • mapDispatchToProps则是拿的actions,源码里面对这个有3种判断,是函数是对象或者没传,有些人会传个函数自己手动dispatch而不借助其内部的bindActionCreators也是可以的。也可以直接传action对象借助其自动派发做成绑定后的action。
  • connect原理类似下面这样:
export default function (mapStateToProps, mapDispatchToProps) {
    return function (OldComponent) {
        return function (props) {
            let context = useContext(ReactReduxContext);//context.store
            let [state, setState] = useState(mapStateToProps(context.store.getState()));
            //useState的惰性初始化
            let [boundActions] = useState(() => bindActionCreators(mapDispatchToProps, context.store.dispatch));
            useEffect(() => {
                return context.store.subscribe(() => {
                    setState(mapStateToProps(context.store.getState()));
                });
            }, []);
            return <OldComponent {...state} {...boundActions} />
        }
    }
}
  • 为什么要使用useState的惰性初始化?因为每次状态变更都会刷新oldComponenet,这样走一遍oldComponent又进行connect,然后进行bindActionCreators就没有必要。使用惰性初始化这个值就存起来,不会再次触发bindActionCreators这个函数了。这个初始化有点特别,useState的源码里是这么定义的:
 function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}
function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  invariant(
    queue !== null,
    'Should have a queue. This is likely a bug in React. Please file an issue.',
  );

  queue.lastRenderedReducer = reducer;

  if (numberOfReRenders > 0) {
    // This is a re-render. Apply the new render phase updates to the previous
    // work-in-progress hook.
    const dispatch: Dispatch<A> = (queue.dispatch: any);
    if (renderPhaseUpdates !== null) {
      // Render phase updates are stored in a map of queue -> linked list
      const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
      if (firstRenderPhaseUpdate !== undefined) {
        renderPhaseUpdates.delete(queue);
        let newState = hook.memoizedState;
        let update = firstRenderPhaseUpdate;
        do {
          // Process this render phase update. We don't have to check the
          // priority because it will always be the same as the current
          // render's.
          const action = update.action;
          newState = reducer(newState, action);
          update = update.next;
        } while (update !== null);

        // Mark that the fiber performed work, but only if the new state is
        // different from the current state.
        if (!is(newState, hook.memoizedState)) {
          markWorkInProgressReceivedUpdate();
        }

        hook.memoizedState = newState;
        // Don't persist the state accumulated from the render phase updates to
        // the base state unless the queue is empty.
        // TODO: Not sure if this is the desired semantics, but it's what we
        // do for gDSFP. I can't remember why.
        if (hook.baseQueue === null) {
          hook.baseState = newState;
        }

        queue.lastRenderedState = newState;

        return [newState, dispatch];
      }
    }
    return [hook.memoizedState, dispatch];
  }
  • 可以看见是这么回事:第一次加载走的是mount,然后会判断传入是不是函数,如果是函数就走函数逻辑拿到返回值,然后传给hook.memoizedState,而后面更新就不会进mount了,直接进update,跳转到updatereducer里,然后直接取hook.memoizedState的值,并没有执行函数。
发布了163 篇原创文章 · 获赞 9 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/yehuozhili/article/details/103866241