简单温故

React最经典的状态管理库非redux莫属。
简单的说,redux在flux单项数据流思想上又限制了三大原则:单一数据源、纯函数改变值、只读状态(只能用action去修改)。dva.js可以说算是react中redux使用过程中标准化流程的封装(号称是:最佳实践)。
在一步一步实现redux之前,可以简单温故下redux:
https://redux.js.org/introduction/getting-started

1. 最基本状态管理

作为状态管理库,最核心的就是管理状态,实现的第一步做这几件事:

  1. 实现一个基本的store
  2. 发布/订阅模式可以监听到数据变化
  3. 输出getState、subscribe、changeState

    1. function createStore(initState = {} ) {
    2. let state = initState;
    3. // 订阅者
    4. const listners = [];
    5. // 得到当前数据
    6. const getState = () => {
    7. return state;
    8. }
    9. // 订阅数据
    10. // lis是一个函数
    11. const subscribe = (lis) => {
    12. listners.push(lis);
    13. }
    14. // 数据变化调用此函数
    15. // 当数据变化后,遍历订阅者,调用之
    16. const changeState = (newState) => {
    17. state = newState; // 变化数据
    18. listners.forEach(lis => lis()); // 通知
    19. }
    20. return {
    21. getState,
    22. subscribe,
    23. changeState,
    24. }
    25. }

这样一个createStore就基本实现了最简单版本的redux数据管理的要求,当然很简陋,测试下:

  1. const initState = {
  2. name: 'yxnne'
  3. };
  4. const store = createStore()
  5. const listner1 = () => {
  6. console.log('in lisner1 :', store.getState())
  7. }
  8. const listner2 = () => {
  9. console.log('in lisner2 :', store.getState())
  10. }
  11. store.subscribe(listner1);
  12. store.subscribe(listner2);
  13. setTimeout(() => {
  14. store.changeState({
  15. ...store.getState(),
  16. name: 'super yxnne'
  17. });
  18. }, 2000)

2. 使用reducer

上一步只完成了数据的存储和订阅,但在redux中有一个很重要的原则就是改变数据只能通过reducer来修改数据,所以这一步,需要做这样的处理。
reducer是什么东西呢,其输入是action、 即将被修改的状态值,输出是新的状态值

  1. // 若用TS定义的话就是这样
  2. function reducer(action: string): IState;

借助reducer的话,自然是不需要上面的 changeState 函数了。但是需要提供一个dispatch函数,用来给reducer派发action,大致就是这样:

  1. function dispatch(action) {
  2. const newState = reducer(state, action);
  3. // 通知订阅者
  4. }

这样的话createStore就需要修改成:

  1. function createStore(reducer, initState = {} ) {
  2. let state = initState;
  3. // 订阅者
  4. const listners = [];
  5. // 得到当前数据
  6. const getState = () => {
  7. return state;
  8. }
  9. // 订阅数据
  10. // lis是一个函数
  11. const subscribe = (lis) => {
  12. listners.push(lis);
  13. }
  14. // 使用dispatch
  15. // 派发action
  16. const dispatch = (action) => {
  17. const newState = reducer(state, action);
  18. state = newState;
  19. listners.forEach(lis => lis());
  20. }
  21. return {
  22. getState,
  23. subscribe,
  24. dispatch,
  25. }
  26. }

使用的时候就是,需要自定义一个reducer:

  1. const reducer = (state, action) => {
  2. let newState = {};
  3. switch (action) {
  4. case 'add_age':
  5. newState = { ...state, age: 30
  6. break;
  7. default:
  8. // ....
  9. return newState
  10. }

在createStore需要把reducer传进去:

  1. // createStore需要把reducer传进去
  2. const store = createStore(reducer, initState)
  3. setTimeout(() => {
  4. // 派发action用以改变数据
  5. store.dispatch('add_age');
  6. }, 2000)

3. 合并reducer

接下来需要处理的情况是,当有多个reducer的时候,将reducer合并起来。(实际使用中通常会把相关的数据写成一个reducer,所以通常情况下项目中的reducer可不止一个)
所以需要实现combineReducers,先分析之:

  1. 多个reducer合并成一个,那么reducer其实是一个函数,那么合并多个reducer之后也应该是个函数;
  2. 使用了combineReducers之后,redux的单一数据源还是单一数据源,只不过增加了一层‘索引’,即reducer key, 或者说是类似namespace的东西。这一点就要求我们做合并reducer的时候,对combineReducers传参数用对象,key是索引,value是reducer。

最终使用起来应该是:

  1. const finalReducer = combineReducers({
  2. user: userReducer, // namespace user
  3. product: productReduer, // namespace product
  4. });
  5. const store = createStore(finalReducer, initState);
  6. // ....略

那么,实现combineReducers如下:

  1. const combineReducers = (allReducers) => {
  2. return (state, action) => {
  3. const nextState = {};
  4. Object.keys(allReducers).forEach(key => {
  5. const reducer = allReducers[key];
  6. // previousStateForKey:
  7. // 某namespace为key的值的先前值
  8. const previousStateForKey = state[key];
  9. // 改变后的值是执行reducer
  10. nextState[key] = reducer(previousStateForKey, action);
  11. });
  12. return nextState;
  13. }
  14. }

上面这个函数combineReducers中,返回的是一个函数,这个函数的如参和reducer需要一致。
看下所谓合并的过程,其实就是对每一个action调用所有的reducer,当然,没有命中reducer,自然是走了其default的逻辑了;
另外,这个合并后函数的输出确如上文所说那样,相当于合并了所有的reducer的namespace对应的state,组成一个大的对象。
比如现在有这两个reducer:

  1. // user
  2. const userReducer = (state = {list:[]}, action) => {
  3. let newState = {};
  4. switch (action.type) {
  5. case 'user_add':
  6. newState = {
  7. ...state,
  8. list: [ ...state.list, action.payload]
  9. }
  10. break;
  11. default:
  12. newState = state
  13. break;
  14. }
  15. return newState
  16. }
  17. // product
  18. const productReducer = (state = {list:[]}, action) => {
  19. let newState = {};
  20. switch (action.type) {
  21. case 'prod_clear':
  22. newState = {
  23. ...state,
  24. list: []
  25. }
  26. break;
  27. default:
  28. newState = state
  29. break;
  30. }
  31. return newState
  32. }

合并reducer并初始化store:

  1. const initState = {
  2. user: { list: [] },
  3. product: { list: [{id: '1'}, { id: 2 }] }
  4. }
  5. // 合并
  6. const reducer = combineReducers({
  7. user: userReducer,
  8. product: productReducer
  9. });
  10. const store = createStore(reducer, initState);
  11. // ....后续逻辑和之前一样的

这样就完美了combine部分就没啥问题了~
不过这里需要增强下健硕性:当createStore中initState不传,那程序会崩溃的:因为在combineReducer中:nextState[key] = reducer(previousStateForKey, action); 这里的previousStateForKey 是undefined了,那么真正执行到reducer中逻辑会报错。
避免这个问题,就要求在写reducer的时候一定要给出默认state:

  1. const productReducer = (state = {list:[]}, action) => { }

另外,在createStore返回结果之前,触发一次初始化过程,这样在逻辑上更有完整性,相当于我们在开始之前,先把整个默认数据构造好了:

  1. dispatch({ type: Symbol() });

中间件

分析中间件

个人理解,中间件这种思想是AOP思想的一种落地方式。从设计模式的角度上属于责任链模式。
先简单看下redux中间件,分析下它,比如现在有一个中间件loggerMiddleware,作用是记录日志:

  1. const loggerMiddleware = (store) => (next) => (action) => {
  2. console.log(`before ${action} state :>`, store.getState());
  3. next(action);
  4. console.log(`after :>`, store.getState());
  5. };

两年前,第一眼看到这个东西我的第一反应就是:什么鬼…. “(store) => (next) => (action) => ”后面知道这种形式和函数被currying后的形式。
首先看下中间件内部,在调用next前后都输出了state值。
其次,这个函数单看是看不出什么的,需要结合使用,一看就明白了:

  1. const next = store.dispatch;
  2. const logger = loggerMiddleware(store);
  3. // 最关键的一步
  4. store.dispatch = logger(next);

最关键的一步,核心就是新的函数(含有中间件功能的函数)需要替换dispatch。但是,最终还是要调用dispatch的,所以,对于就一个中间件logger来讲,next就是原来的dispatch。
那么,考虑更复杂的情况:假设现在不止logger,还有timer,auth——即更多的中间件:

  1. const logger = loggerMiddleware(store);
  2. const timer = timerMiddleware(store);
  3. const auth = authMiddleware(store);

假设现在就用这三个中间件,该如何替换dispatch呢?

  1. const next = store.dispatch;
  2. // 这样搞
  3. store.dispatch = auth(timer(logger(next)));

就像上面代码这样,按照期望的调用顺序,将中间件嵌套调用,内层的就是外层的next,而最内层的next自然是原来的dispatch。
代码如预期般丝滑柔顺~

compose

上面成功的实现了中间件机制,虽说功能无恙,但是不够优雅。
redux针对中间件有处理方法:applyMiddleware; 简单阐述下原理,applyMiddleware的入参就是需要应用的中间件,依次传入;applyMiddleware返回一个接受createStore的函数,在这个函数入参和createStore一致,因为内部对createStore进行了调用,并且改变了dispatch(借助compose合并中间件将dispatch赋给合并后的函数),返回值和createStore也保持一致。

  1. const applyMiddleware = function (...middlewares) {
  2. return function (oldCrateStore) {
  3. return function (reducer, initState) { // 此函数就是包裹的createStore
  4. // 得到原始的store
  5. const store = oldCrateStore(reducer, initState);
  6. const simpleStore = { getState: store.getState };
  7. // 用store 初始化 中间件
  8. const chain = middlewares.map((middleware) => middleware(simpleStore));
  9. // 合并中间件,并生成新的dispatch
  10. const dispatch = compose(...chain)(store.dispatch);
  11. return {
  12. ...store,
  13. dispatch,
  14. };
  15. };
  16. };
  17. };

这里面用了compose,compose很简单,借助reduce方法:

  1. function compose(...funcs) {
  2. if (funcs.length === 0) return (arg) => arg;
  3. if (funcs.length === 1) return funcs[0];
  4. return funcs.reduce((a, b) => (...args) => a(b(...args)));
  5. }

最后,applyMiddleware要使用,还需要配合改写下createStore,不需要做很多事情,就是参数中新加一个参数,类型是函数,然后函数体中判断下是不是有这个参数,如果有,执行此参数,利用其返回函数生成新的store:

  1. export default function createStore(
  2. reducer,
  3. initialState,
  4. rewriteCreateStoreFunc
  5. ) {
  6. if (rewriteCreateStoreFunc) {
  7. // 生成新的createStore方法
  8. const newCreateStore = rewriteCreateStoreFunc(createStore);
  9. // 新的createStore方法中会调用createStore
  10. // 因为没有给参数rewriteCreateStoreFunc
  11. // 所以在调用createStore的时候不会进入:if (rewriteCreateStoreFunc)
  12. return newCreateStore(reducer, initialState);
  13. }
  14. // ...略 (后面一点没改)
  15. }

使用的时候,这样使用:

  1. const rewriteCreateStoreFunc = applyMiddleware(
  2. loggerMiddleware,
  3. timerMiddleware,
  4. authMiddleware
  5. )
  6. const store = createStore(reducer, initState, rewriteCreateStoreFunc);

顺理成章,点到为止~

thunk

针对redux的中间件已经完全实现了。
最后讨论一个经典的异步中间件方案——thunk,redux-thunk

回忆下如何使用redux-thunk?

  1. // in React onCkick handle
  2. const handleAddClick = () => dispatch(addAsync);
  3. // .....
  4. function addAsync() {
  5. return (dispatch) => {
  6. setTimeout(() => {
  7. dispatch({
  8. type: 'ADD',
  9. });
  10. }, 1000);
  11. };
  12. }

比如有一个点击事件的处理函数handleAddClick,这个函数调用了 dispatch(addAsync), addAsync返回了一个函数,这个函数中存在异步逻辑,上文代码的异步逻辑是定时任务,当任务结束后,再派发一个同步任务。
在查看redux-thunk的源码前,先解释下原理,redux-thunk首先是个中间件,在这个中间件内部做了一个逻辑就是,拦截下action是函数类型的action,执行这个action;那么其它不是函数类型的action继续走next,执行后续中间件。
源码很简洁:

  1. function createThunkMiddleware(extraArgument) {
  2. // 中间件接口 store => next => action
  3. return ({ dispatch, getState }) => (next) => (action) => {
  4. // action要是函数
  5. // 拦截并执行
  6. // 不走next了
  7. if (typeof action === 'function') {
  8. return action(dispatch, getState, extraArgument);
  9. }
  10. return next(action);
  11. };
  12. }
  13. const thunk = createThunkMiddleware();
  14. thunk.withExtraArgument = createThunkMiddleware;
  15. export default thunk;

最后一个问题,什么是thunk?

A thunk is a function that wraps an expression to delay its evaluation.(thunk就是一个函数包裹了一个用以延迟求值的表达式)

  1. // 立即执行 1 + 2
  2. let x = 1 + 2;
  3. // 1 + 2 的执行被延迟了
  4. // 因为foo可以稍后被调用
  5. // foo 就是一个 thunk!
  6. let foo = () => 1 + 2;

从这个角度上来看redux-thunk。还是上文的例子:

  1. function addAsync() {
  2. return (dispatch) => {
  3. setTimeout(() => {
  4. dispatch({
  5. type: 'ADD',
  6. });
  7. }, 1000);
  8. };
  9. }

再去理解下,其实本质是想执行的是(只不过不是现在):dispatch({ type: ‘ADD’, })。所以我们站在redux的立场上,就是借助addAsync的派发,从而延迟的派发了这个type是ADD的action。

redux和函数式编程思想

搞完了redux最核心的东西,回头从函数式编程的角度,看看redux。
redux这个东西体现了函数式编程的思想:
函数式编程讲究数据放入中容器,通过菡子对容器中的数据做映射,从而改变数据。
那么这些思想对应到redux中,store就是容器,映射(map)就是reducer,而函子就是各种各有的action~
另外,看redux源码我们可以看到全部是函数式编程的概念:currying、compose、reduce、高阶函数、thunk….

总之,redux很经典,函数式编程是个活儿~