单页面应用的开发,一个项目需要管理的状态越来越多,服务器的响应,客户端缓存数据,本地生成未持久化的。ui状态都需要保存,单单的一个变量不足以满足我们的业务。状态管理器由此而生

解决的问题

  1. 全局状态容器,暂存的数据
  2. 数据在各个组件之间的共享
  3. 保存当前不常用的数据,达到缓解服务器压力的目的
  4. 让数据变的可预测

    核心概念

  5. 让一个数据变的有可预测性,推崇单向数据流

  6. 把数据交给一个纯函数去处理,不在数据流动中产生副作用

Redux - 图1

三大原则

单一数据源

整个应用的state应该储存在object tree中,并且只有一个store。

state是只读的

immutable state
改变state的方式就是派发action,一个action是描述了已经发生事件的对象,用来描述具体的动作
action由reducer接收,根据不同的动作处理不同的state

纯函数修改state

为了对应不同的action动作而决定不同的处理方式,需要编写不同的reducers
reducer只是一个纯函数,接收一个old state和action,返回state。随着应用的扩大应该合理的拆分reducer,控制他们的调用顺序.
记录每一次的动作类型,可以在这记录错误日志,这对前端线上代码很友好,不会导致线下复现不了而苦恼

工作流程

根据上图可以看出,views 发出action, 由store分配给reducers去处理,返回一个新的state,重新通知views去更新UI。
下面列出最经典的 Todo List案例

  1. const initState = {
  2. counter: 0
  3. };
  4. function counter(state = initState, action) { // reducer
  5. switch (action.type) {
  6. case "INCREMENT":
  7. return {
  8. counter: state.counter + 1
  9. };
  10. case "DECREMENT":
  11. return {
  12. counter: state.counter - 1
  13. };
  14. default:
  15. return state;
  16. }
  17. }
  18. // 或者传入第二个参数 initState
  19. const store = createStore(counter, applyMiddleware(thunk));
  20. // mian.js
  21. <Context.Provider value={Store}>
  22. <App />
  23. </Context.Provider>
  24. // context.js
  25. export default function (Component) {
  26. return (props) => {
  27. const store = React.useContext(Context);
  28. return <Component {...props} store={store} />;
  29. };
  30. }
  31. //app.js
  32. export default HocContainer(
  33. ({store}) => {
  34. const [state, setState] = React.useState(0);
  35. function getState() {
  36. let { counter } = store.getState();
  37. setState(counter);
  38. }
  39. React.useEffect(() => {
  40. return store.subscribe(getState);
  41. }, []);
  42. const inc = () => {
  43. store.dispatch({ type: "INCREMENT" });
  44. };
  45. const dnc = () => {
  46. store.dispatch({ type: "DECREMENT" });
  47. };
  48. return (
  49. <div>
  50. <p>{state}</p>
  51. <button onClick={inc}> 加加 </button>
  52. <button onClick={dnc}>减减</button>
  53. </div>
  54. );
  55. }
  56. )

api解读

createStore

用来创建一个车store对象,

  1. 接收一个reducer。返回一个新的state tree
  2. preloadedState, 初始化的state。
  3. enhancer 一个增强器,用来强化store createor。可以通过自定义的复合函数改变store接口

@return Store

  • getState 返回当前最新的state
  • dispatch 接收一个action负责传递给store,最后交给reducer去处理。
  • subscribe 订阅store ,每当store由变化的时候会被通知到每一个订阅者
  • replaceReducer 替换当前store用来计算的reducer。 一个高级的api,只有需要代码分割的,在加载一些独立的reducer的时候才可能用到它。

    combineReducers

    随着应用的复杂,我们可以把很多个reducer拆分成多个单独的函数,每个函数独立管理state的一部分。
    该函数的作用就是和并多个reducer函数,最终可以为合并后的reducer调用store方法。就像是下面这样

    const mergeR = resucers => (states, action) => Object.entries(reducers).reduce((state, reduce) => {
        let  [redName, fn]  = reduce;
      let prevState = state[redName];
      state[redName] = fn(prevState, action)
      return state
    }, states)
    

    如果参数了es6对象的增强写法,默认是以reducer的name属性命名的。
    注意点。

  • 如果当前的reducer未能匹配到action时, 必须把state原封不动的返回

  • 永远不能返回undefined,如果由,该方法会爆出异常
  • 如果传入state就是undefined,应该es6的默认参数语法来设置默认初始state很容易,也可以手动检查第一个车参数是否未undefined

    bindActionCreators

    该方法用来增强actions的,类似于这样
    const bindActions = (objects, dispatch) => {
    for (let [key, fn] of Object.entries(objects)) {
      objects[key] = (...args) => dispatch(fn(...args));
    }
    return objects
    }
    

    compose

    需要合成的多个函数。预计每个函数都接收一个参数。它的返回值将作为一个参数提供给它左边的函数,以此类推。
    例外是最右边的参数可以接受多个参数,因为它将为由此产生的函数提供签名
    const compose = (...fns) => fns.reduce((a, b) => (...args) => a(b(...args)))
    
    参数从右向左依次调用,参数是收一个函数的返回值

    applyMiddleware

    中间件加载组合的方式,像下面这样,可以用来加载中间件,会执行以下操作,把需要的中间件传入即可

接收用来执行的中间件,返回一个函数,会被createStore接收并把createStore传入,在此返回一个函数用来接收创建从库的参数。把当前仓库的方法合并成一个新对象,对当前函数的参数遍历执行,新的对象传入,得到内层函数

const store = createStore(counter, applyMiddleware(thunk));

具体实现

createStore

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
}

createStore(reducer, null, applyMiddleware(...))

函数开始会对参数进行校验。把enhancer处理掉。所以像第八行这样也可以
enhancer有值,会进行一下操作

if (typeof enhancer !== 'undefined') {
  return enhancer(createStore)(reducer, preloadedState)
}

所以说有了中间件,dispattch在enhancer内部已经改写,在applyMiddleware增强函数里

let isDispatching = false

function ensureCanMutateNextListeners() {
  if (nextListeners === currentListeners) {
    nextListeners = currentListeners.slice()
  }
}

function subscribe(listener) {

  if (isDispatching) {
     throw new Error(`抛出异常` )
  }

  let isSubscribed = true

  ensureCanMutateNextListeners()
  nextListeners.push(listener)

  return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(`抛出异常` )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      currentListeners = null
  }
}


function dispatch(action) {
  if (!isPlainObject(action)) {
    throw new Error(`抛出异常` )
  }

  if (typeof action.type === 'undefined') {
    throw new Error(`抛出异常` )
  }

  if (isDispatching) {
     throw new Error(`抛出异常` )
  }

  try {
    isDispatching = true
    currentState = currentReducer(currentState, action)
  } finally {
    isDispatching = false
  } 
  const listeners = (currentListeners = nextListeners)
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i]
    listener()
  }

  return action
}

首先在内部维护了一个车 isDispatch 的变量,在初始化未false。在更改 currentState 时被改成了true。后续执行”通知”操作,就是subscribe的订阅操作。所以isDispatch 可以理解为“锁”。

那为什么要上锁呢?
在执行当前reducer时会上锁,避免在执行过程中被再次调用dispatch,从而避免套娃时dispatch,导致的死循环。
因为在reducer中调用了dispatch, 而dispath返回来又会执行reducer…..整活

current - nextListener
每一次执行订阅前都会确保,二者指向不同的引用,操作的都是nextListener,而current相当于辅助了next数组。都是为了后面执行通知操作的稳定性。
因为在后面循环通知的过程中,如果直接操作next数据,当数组的长度发生了变化,通过操作就挂了,所以复制一份,指向不同的引用,在执行中,就不会和之前的操作有任何影响。(但我自己试了一下,没有发生这种情况)

而在unsubscribe卸载函数里,调用了该函数也是删除某一个订阅者,如果当前是在更新阶段,在更新过程中执行卸载操作显然是不合理的,而应该在更新之后或者更新前去执行卸载操作。所以用到之前的锁。

applyMiddleware

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
     // if (typeof enhancer !== 'undefined') {
        //return enhancer(createStore)(reducer, preloadedState)
      //}
    const store = createStore(...args) 


    let dispatch = () => {
      throw new Error(`抛出异常`)
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

在第六行首先创建了一个store,参数为之前传入的reducer。
而下面的dispatch,的异常抛出,是不希望在执行中间件的时候被执行派发操作。因为禁止套娃💚
而该函数的参数就是我们的中间件。执行所有的中间件,传入对应的参数。传出API。

这里以redux-thunk为例

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

    return next(action);
  };
} 

dispatch = compose(...chain)(store.dispatch)

所以在中间件内部可以拿到getState,和dispath,返回的函数中,next方法就是11行传入的dipatch。compose成一个dispatch。而执行之后得到的dispath是compose函数之后的函数,所以dispatch每次执行的时候,都会执行中间件中的函数逻辑,

 if (typeof action === "function") {
    return action(dispatch, getState);
  }

  console.log(next); // function dispatch(action) { } 

  console.log(action); // {type: "INCREMENT"}

  return next(action);
}

通过测试可以看到,next就是dispatch函数,是compose函数执行之后注入的 store.dispatch 方法。它可能是最原始的dispatch方法,也可能是其他中间件覆盖过的方法。 而传入进去middlewareAPI在此被放入我们的函数action中。在我们用到dispatch的时候,dispatch已经被compse函数执行后被覆盖掉了。

compose

组合,从头到尾的执行一组函数。 接受初始值,前一个函数的返回值作为下一个函数的参数

const compose = (...fns) => fns.reduce((a, b) => (...args) => a(b(...args)))