【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的值,並沒有執行函數。
發佈了153 篇原創文章 · 獲贊 8 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章