状态管理

196 阅读5分钟

Redux

redux 是一个为 JavaScript 应用设计的可预测的状态容器。

1. redux遵循三大原则

  • 单一数据源:应用的 state存储在一个单一的 store 中
  • 状态只读:仅可以通过发出一个 action(一个描述更改的对象)来改变状态
  • 纯函数执行状态更改:需要编写纯函数 reducers 描述如何根据action 更改 state

2. redux 相关概念

action

扁平的 javascript 对象,是将数据从应用发送至store的载体,可通过store.dispatch()发送一个action

action 中必须定义属性 type 以声明要执行的动作类型,type取值为字符串常量。

reducers

actions 描述发生了什么,reducers则指定应用是如何响应actions并更改状态的。

const todoReducer = (state=[], action)=> {
    switch(action.type){
        case 'add_todo':
            return [...state, { text: action.text, completed: false }];
        default:
            return state; 
    }
}

实际应用中我们会按照业务逻辑根据模块定义不同的reducer,然后通过 combineReducers()组合,combineReducers()主要是生成一个函数,该函数可以根据key来筛选state的一部分数据并调用对应的reducer进行处理。

import { combineReducers } from 'redux'
const rootReducer = combineReducers({
    todos: todoReducer, 
    users: userReducer,
})

store

store 用于组合 actions 和 reducers,主要有以下职责:

  • 维护应用状态
  • 允许通过store.getState()获取状态
  • 允许通过store.dispatch(action)更新状态
  • 可通过store.subscribe(listener)注册监听器,state每次更新时都会调用listener
  • 可通过store.subscribe(listner)返回的函数注销监听器
import { createStore } from 'redux';
const store = createStore(rootReducer, initialState)

// 注册监听函数
const unsubscribe = store.subscribe(()=> console.log(store.getState()))

store.dispatch({type: 'add_todo', text: 'cooking'})

// 停止监听 state 的更新
unsubscribe()  

middleware

当我们需要做一些异步数据流的操作时,如请求数据或者日志打印,此时就需要用到 redux 中间件。

中间件可以拦截dispatch的一切东西,传递actions给下一个中间件,但是最后一个中间件的派发的action必须是一个扁平的object,用于 reducer 的处理。

我们通过使用applyMiddleware来进行 redux 的增强。

import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'

const loggerMiddleware = createLogger()

const store = createStore(
  rootReducer,
  applyMiddleware(
    thunkMiddleware, // lets us dispatch() functions
    loggerMiddleware // neat middleware that logs actions
  )
)

3. redux 的一些原理

  1. 如何移除监听器?

    // createStore(){} 内部定义的变量,用于存储监听器
    let nextListeners = []
    function subscribe(listener){
        // 新增监听器
        nextListeners.push(listener)
        
        // 调用返回的函数则可以移除监听器
        return function unsubscribe() {
            const idx = nextListeners.indexOf(listener)
            nextListeners = nextListeners.slice(idx, 1)
        }
    }
    
  2. dispatch()

    调用 reducer 更新 state,然后依次执行监听器,最后返回传入的 action

    function dispatch (action) {
        currentState = rootReducer(currentState, action)
        listeners.forEach(l => l())
        return action
    }
    
  3. combineReducer()

    // 接口定义参考
    
    interface ICombinedReducer {
        ( state: {}, action: {type: string; [key: string]: any} ) : object
    }
    
    // combineReducer 的接口形式
    interface ICombineReducer {
        ( reducers: { [key: string]: function } ) : ICombinedReducer
    }
    
    

    传入的对象是所有 reducer 的集合 ,使用闭包保存该对象,返回值是一个函数,该函数作用就是针对传入的一个action把所有的 reducer 遍历一遍,汇总计算后的状态并返回,所以这里就需要在每个 reducer 中针对不需要处理的action.type设置默认状态

    // 源码抽离
    function combineReducer ( reducers ){
        const finalReducers = Object.keys(reducers).reduce((ret, key)=>{
            ret[key] = reducers[key]
            return ret
        }, {})
        return function combination(state={}, action){
            // 对每个 action 调用所有的 reducer
            Object.keys(finalReducers).reduce((newState, key)=> {
                newState[key] = finalReducers[key](state[key], action)
                return newState
            }, {})
        }
    }
    
  4. applyMiddleware(...middlewares)

    /**
    * 实际在 createStore 中使用时:enhancer(createStore)(reducer, preloadedState); 
    * 此处的 enhancer =  applyMiddleware(...middlewares)  其实就是用闭包把 middlewares 给保存起来
    * 传入的 middleware 要求必须符合 `store => next => action => {}`的形式
    */
    export default function applyMiddleware(...middlewares) {
      // 此处的 args 即是指 reducer  和 initialState
      return createStore => (...args) => {
        // 创建 store , 此时 store = { dispatch, subscribe, getState, ...,}
        const store = createStore(...args);
        
        // 定义 dispatch ,中间件构建结束会更新dispacth的取值,构建中间件时调用就会抛错
        let dispatch = () => {
          throw new Error('...');
        };
    
        // 用于存储 middleware chain
        let chain = [];
    
        // 定义传给 middleware 的 参数,对 dispatch 进行引用可以获取增强后的dispatch
        const middlewareAPI = {
          getState: store.getState,
          dispatch: (...args) => dispatch(...args)
        };
    
        // middleware = ({getState, dispatch}) => next => action => {}
        // map之后: middleware = next => action => { /* 这里可以访问传入的 getState 和 dispatch */ }
        chain = middlewares.map(middleware => middleware(middlewareAPI));
    
        dispatch = compose(...chain)(store.dispatch);
    
        return {
          ...store,
          dispatch
        };
      };
    }
    

    这里最核心的代码即是 compose()的函数组合,先看一下 compose的源码

    function compose(...funcs){
    
        /* ... 省略边界处理相关代码.... */ 
        
        return funcs.reduce((a,b) => (...args)=> a(b(...args)))
    }
    

    compose(chain)(store.dispatch)的执行分如下两步理解:

    • 计算compose(chain),得到composedChain = (...args) => m1(m2(m3(m4(...args)))),作用是调用这个函数时,将 middleware chain中 的函数按照从右至左的顺序调用,最右侧的函数参数是传入参数,剩余函数的参数是前一个函数的返回值,此时每个中间件形如 mw = next => action => {}
    • 计算composedChain(store.dispatch)
    // 1) 执行 m[len-1] 
    //         入参为 next = store.dispatch
    //         返回值为函数 action => { /* 此处可以访问原始的 store.dispatch */ }
    // 2) 执行 m[len-2] 
    //         入参为 next = action => { /* 这里是m[len-1]的函数体 */ } 
    //         返回值为函数 action => { /* 此处若执行 next(action) 则代表调用 m[len-1] 处理 action */ }
    // ....
    // n) 执行 m[1]
    //         入参为 next = action => { /* 这里是m[2]的函数体 */ }
    //         返回值为函数 action => { /* m1 函数体,若调用 next(action) 则为调用 m2 处理 action */ }
    
    // 参考如下伪代码说明:
    dispatch = action => {
        /* m1 函数体 */
    
        // do sth before call next
        
        next === action => {
            /* m2 函数体 */
            
            next === action => {
                /* m3 函数体 */
                
                next === action => {
                    /* m4 函数体 */
                    
                    next === store.dispatch
                }
            }
        }
        
        // do sth after call next
    }
    
    

    对照上述伪代码可以看出,中间件需要实现next(action)的逻辑才可以保证链式调用,从前到后直到最后的中间件,然后再从后到前直到第一个中间件。

Redux Thunk

Redux-Thunk 是一个 redux 中间件,用于异步处理。

Redux-Thunk 的主要作用是当传入的 action 是一个函数时,那就直接调用这个函数,并传入 dispatch 等参数,可以在这个函数内部调用 dispatch。

// 源码
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk

使用示例如下:

// store 已经配置好使用 thunk 中间件,具体如何配置可参考 redux middleware 部分
import store from './store.js'
function App(){
    const [count, setCount] = useState(store.getState());

    const handleAsyncDec = () => {
        store.dispatch(dispatch => {
            setTimeout(() => {
                dispatch({  type: "INC" });
            }, 1000);
        });
    };
    
    useEffect(() => {
        store.subscribe(() => {
            setCount(store.getState());
        });
     }, []);
     
    return (
        <div>
            <button onClick={handleAsyncDec}>异步+1</button>
            <p>count: {count}</p>
        </div>
    );
}