React源码解析系列(十) -- Redux的工程化与React-Redux的解读

331 阅读7分钟

上一章讲了React源码解析系列(九) -- Redux的实现原理与简易模拟实现的基本原理与部分api的实现,但是为什么仅仅Redux是不够支持大型项目开发的,主要的原如下:

  • 因为我们知道在Redux中,createStore函数在调用的时候,会传递一个reducer进去,那么相对于大型系统来说,状态远远不止唯一的一个,我们又知道对公共状态的唯一修改只能通过reducer进行,那么处理这些公共状态的reducer就只能放在同一个reducer中,这样造成的后果是:

    • 代码过于混乱,不利于维护。
    • 对于协同开发,每个人单独负责不同的模块,如果是对公共状态的依赖的东西,那么每个人修改的将会是同一个reducer,不利于代码合并(大厂规范:尽量只做增量代码,少mod别的东西)。
  • 解决方案:针对上面的问题,聪明的你是不是想到了。既然要求代码代码规范与代码逻辑好维护,那我可不可以尝试一下代码模块化开发呢?我可不可以维护多reducer,每个独立的reducer去处理对应的公共状态,最后我再把reducer进行合并呢?当然可以,这就是我们接下来要讲的Redux的工程化。

Redux工程化的要求

  • 对派发的行为对象进行统一管理
// sinbarkAction.js
import { SINBARK_READ, SINBARK_WRITE } from "./actionType";
const sinbarkAction = {
  // 触发的方法
  read(payload) {
    return {
      type: SINBARK_READ,
      payload
    };
  },
  write() {
    return {
      type: SINBARK_WRITE,
      payload: 2
    };
  }
};

export default sinbarkAction;
// goodsAction.js
import { GOODS_READ, GOODS_WRITE } from "./actionType";
const goodsAction = {
  // 触发的方法
  read_q() {
    return {
      type: GOODS_READ,
      payload: 1
    };
  },
  write_q() {
    return {
      type: GOODS_WRITE,
      payload: 2
    };
  }
};

export default goodsAction;
// index.js
import sinbarkAction from "./sinbarkAction";
import goodsAction from "./goodsAction";

const actions = {
  sinbark: sinbarkAction,
  goods: goodsAction
};

export default actions;
  • 对派发的行为标识进行统一管理
    • 尽可能的保证派发行为标识语判断标识一致
    • 避免派发的行为标识产生冲突
// actionType.js
export const SINBARK_READ = "SINBARK_READ";
export const SINBARK_WRITE = "SINBARK_WRITE";
export const GOODS_READ = "GOODS_READ";
export const GOODS_WRITE = "GOODS_WRITE";
  • 对reducer单独管理并且与行为标识、初始状态进行绑定,最后合并reducer
// sinbarkReducer.js
import { SINBARK_READ, SINBARK_WRITE } from "../actions/actionType";
import { cloneDeep } from "lodash";

const initilaValue = {
  count: 10
};

const sinbarkReducer = (state = initilaValue, action) => {
  state = cloneDeep(state);
  const { type, payload } = action;
  switch (type) {
    case SINBARK_READ:
      state.count += payload;
      break;
    case SINBARK_WRITE:
      state.count += payload;
      break;
    default:
  }
  console.log(state);
  return state;
};

export default sinbarkReducer;
// goodsReducer.js
import { GOODS_READ, GOODS_WRITE } from "../actions/actionType";
import { cloneDeep } from "lodash";

const initilaValue = {
  goodsCount: 1
};

const goodsReducer = (state = initilaValue, action) => {
  state = cloneDeep(state);
  const { type, payload } = action;
  switch (type) {
    case GOODS_READ:
      state.goodsCount += payload;
      break;
    case GOODS_WRITE:
      state.goodsCount += payload;
      break;
    default:
  }
  return state;
};

export default goodsReducer;
//index.js
import { combineReducers } from "redux";
import sinbarkReducer from "./sinbarkReducer";
import goodsReducer from "./goodsReducer";

const reducer = combineReducers({
  sinbark: sinbarkReducer,
  goods: goodsReducer
});

export default reducer;

上述代码的本质就是按模块来处理不同的reducerstateaction。处理的结果是:

image.png

我们在触发事件,现在不是单一的dispatch一个含有type字段的对象了,而是通过执行action中的函数,得到行为对象进而去派发给store进行更新了,比如:

import actions from '@action/index.js';
import store from "./store";
...
// 非工程化代码
<div onClick={()=>store.dispatch({type:'SINBARK_READ', payload:1})}>handleSinbarkRead</div>

// 工程化代码
<div onClick={()=>store.dispatch(actions.sinbark.read)}>handleSinbarkRead</div>

combineReducers的源码

combineReducer能够帮助我们把众多的reducer合并到一起,真的是简简单单的函数合并吗?我们一起来看一下源码就知道了。

export default function combineReducers<S>(
  reducers: ReducersMapObject<S, any>
): Reducer<CombinedState<S>>
export default function combineReducers<S, A extends Action = AnyAction>(
  reducers: ReducersMapObject<S, A>
): Reducer<CombinedState<S>, A>
export default function combineReducers<M extends ReducersMapObject>(
  reducers: M
): Reducer<
  CombinedState<StateFromReducersMapObject<M>>,
  ActionFromReducersMapObject<M>
>
export default function combineReducers(reducers: ReducersMapObject) {
  /**
   *  const reducer = combineReducer({
   *    a: aReducer,
   *    b: bReducer 
   *  })
  */
  // 获取所有的key [a, b]
  const reducerKeys = Object.keys(reducers)
  // 存储所有的reducer
  const finalReducers: ReducersMapObject = {}

  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      ...
    }
    
    // 处理reducer为函数的情况,用{a:aReducer}
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // {a:aReducer, b:bReducer}
  const finalReducerKeys = Object.keys(finalReducers)
  let unexpectedKeyCache: { [key: string]: true }
  ...
  
  return function combination(
    state: StateFromReducersMapObject<typeof reducers> = {},
    action: AnyAction
  ) {
    ...
    if (process.env.NODE_ENV !== 'production') {
      ...
    }
    
    // 记录是否变化
    let hasChanged = false
    
    // 记录新的state
    const nextState: StateFromReducersMapObject<typeof reducers> = {}
    
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i] // 取key值
      const reducer = finalReducers[key] // 绑定reducer
      const previousStateForKey = state[key] // 取老状态的key值
      // 执行reducer得到新值
      const nextStateForKey = reducer(previousStateForKey, action)
      
      if (typeof nextStateForKey === 'undefined') {
        const actionType = action && action.type
        throw new Error(
          ...
        )
      }
      nextState[key] = nextStateForKey // 更新state
      // 变更状态
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    
    // finalReducerKeys为处理后的key数组,
    // state为上一次 || 原始对象
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    // 依赖变更就返回新值,没变就返回老值  
    return hasChanged ? nextState : state
  }
}

通过上述代码我们知道了,combineReducers并不是合并所有的reducer,他只是创建了一个新的reducer,在这个reducer里面执行了所有依赖传入的reducer,得到了新值,然后会把这个值返回出去并且更新原来的state。上述代码我们可以简化为更容易理解的版本:

const combineReducers = (reducers) => {
  // 遍历对象,获得key
  let reducerKeys = Object.keys(reducers);
  // 创建一个新的reducer,返回出去
  return combination = (state={}, action) => {
    // 记录每一个reducer执行的结果
    let nextState = {};
    // 遍历执行每一个reducer
    reducerKeys.forEach(key=>{
      let reducer = reducers[key];
      nextState[key] = reducer(state[key], action)
    })
    // 返回新值对象
    return nextState;
  }
}

react-redux

因为我们前面说redux可以实现状态共享,可以把store挂在根节点上,通过上下文来进行数据传递,但是这种需要同学们有额外的操作。又可以为组件引入store达到目的,但是组件引入store,对组件的复用会有影响。再者针对于函数组件来讲,我们需要手动往事件池添加事件以达到能够更新的目的。不不不,我不希望有多余的操作能不能实现目标操作哦?当然可以。react-redux提供了一种能够把全局状态注册到上下文中去的方法,并且可以自动的往事件池中添加事件以达到更新的目的。

Provider的作用

//index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import store from '@/store.js';
import Child from './Child';

const App = () => {
  return (
    <Provider store={store}>
      <Child/>
    </Provider>
  )
}

ReactDOM.render(<App/>, document.getElementById('root'))

Provider组件可以往组件上下文里面注册全局状态。

connect的作用

connect的作用可以把statedispatch映射到props里面去。

import { connect } from "react-redux";
import actions from "../actions";

const Child = (props) => {
  let { read, count, write } = props;
  return (
    <div className="Child">
      <h3>{count}</h3>
      <div onClick={write.bind(null, 20)}>+20</div>
      <div onClick={read.bind(null, 10)}>+10</div>
      <div onClick={write_q.bind(null, 20)}>+20</div>
      <div onClick={read_q.bind(null, 10)}>+10</div>
    </div>
  );
};

// connect(mapStateToProps, mapDispatchToProps)(Component)

//单个state与reducer模块
export default connect(state=>state.sinbark, actions.sinbark)(Child)
// 多个state与reducer模块
export default connect(
  state => return {
    // 为两个不同的state
    sinbark: state.sinbark,
    goods: state.goods
  
}, {
  // 为两个不同的reducer
  ...action.sinbark, ...actions.goods
})(Child)

这里就不放效果图了,各位同学可以自行上codneSandBox去体验。

源码解读

我们知道Provider组件就做了一件事情,那就是注册上下文,很明显它和React提供的Context功能是一模一样的。 Provider源码的github链接

function Provider<A extends Action = AnyAction>({
  store, // store属性
  context, // 上下文
  children, // 子元素 || 组件
  serverState,
}: ProviderProps<A>) {

  const contextValue = useMemo(() => {
    const subscription = createSubscription(store)
    return {
      store,
      subscription,// 注册订阅者,订阅reducer
      getServerState: serverState ? () => serverState : undefined,
    }
  }, [store, serverState])
  
  // 上一次的state
  const previousState = useMemo(() => store.getState(), [store])

  useIsomorphicLayoutEffect(() => {
    const { subscription } = contextValue
    subscription.onStateChange = subscription.notifyNestedSubs
    subscription.trySubscribe()
    
    // 两次的state不一样,通知订阅者去准备更新更新
    if (previousState !== store.getState()) {
      subscription.notifyNestedSubs()
    }
    
    return () => {
      // 撤销订阅
      subscription.tryUnsubscribe()
      // 重置状态
      subscription.onStateChange = undefined
    }
  }, [contextValue, previousState])

  const Context = context || ReactReduxContext
  // 返回Context.Provider组件,与createContext非常相似
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

Provider的源码大致就是这样,如果更简洁一点的话就是下面这样子的:

// 创建上下文
const Context = createContext(null);

//定义Provider函数
export Provider = (props) => {
  // 拿到store和children
  const {children, store} = props;
  // 源码里面的contextValue是一个包装对象
  return <Context.Provider value={{store}}>{children}</Context.Provider>
}

connect函数,接受两个参数mapStateToPropsmapDispatchToProps返回一个函数,函数的入参为组件,返回一个可供用户调用的包含propsmapStateToPropsmapDispatchToProps组件,源码的github链接。所以我们来实现一个connect来加强理解。

import {useContext, useMemo, useState, useLayoutEffect } from 'react';
import {bindActionCreators} from 'redux';

export default const connect = (mapStateToProps, mapDispatchToProps) => {
  // 因为在前面如果缺少mapStateToProps, mapDispatchToProps其中一个,也是可以的。
  // 他们俩本身都是函数。
  // 避免出现问题,我们进行兼容处理。
  if(!mapStateToProps){
    mapStateToProps = () => {
      return {}
    }
  }
  
  if(!mapDispatchToProps){
    mapDispatchToProps = () => {
      return {}
    }
  }
  
  // 返回一个可执行的函数,入参为组件
  return function connectWithComponent(component){
    // 可以调用的组件,传递props
    return function newComponent(props){
      // 获取store
      let {store} = usecontext(Context);
      // 获取store里面的方法
      let {getState, dispatch, subscribe} = store;
      
      // 执行mapStateToProps
      let state = store.getState();
      // 如果state没有变,不作处理
      let nextProps = useMemo(()=>{
        return mapStateToProps(state)
      },[state])
      
      // 执行mapDispatchToProps
      let nextDispatch = useMemo(()=>{
        //如果传入的是函数
        if(typeof mapDispatchToProps === 'function'){
          return mapDispatchToProps(dispatch)
        }
        // 如果传入的是对象
        return bindActionCreators(mapDispatchToProps, diapacth);
      },[dispatch])
      
      //手动追加更新事件,以便于触发更新
      let [,forceUpdateWithEmpty] = useState(0);
      useLayoutEffect(()=>{
        subscribe(()=>{
          // 随机数保证每次追加的state不一样
          forceUpdate(Math.radom().toFixed(10))
        })
      },[subscribe])
      
      return <Component {...props} {...mapStateToProps} {...mapStateToProps} />
    } 
  }
}

中间件应用方案

applyMiddleWareredux提供的一种中间件处理方案,他能够配合一些其他的插件帮助我们在redux执行流程中去做一些额外的操作,比如打印日志、异步操作。

import { createStore, applyMiddleware } from "redux";
import reducer from "../reducer/index";
// import thunk from "redux-thunk";

function logger({ getState }) {
  return (next) => (action) => {
    console.log("will dispatch", action);
    console.log("will dispatch", getState());

    // 调用 middleware 链中下一个 middleware 的 dispatch。
    const returnValue = next(action);

    console.log("state after dispatch", getState());
    console.log("returnValue", getState());

    // 一般会是 action 本身,除非
    // 后面的 middleware 修改了它。
    return returnValue;
  };
}

export default createStore(reducer, applyMiddleware(logger));

还有redux-sagaredux-thunk等插件,在这里就过多的写代码了,其实说到中间件,也可以理解成具有导向的钩子函数吧,怎么理解呢?在一个执行流程中,我们编写代码一定要符合开闭原则(对修改关闭,对扩展开放),但是开放指的不是去修改代码本体,比如装饰器模式,那中间件也可以理解成钩子函数,是对现有的代码做的一种扩展的应用程序函数。说到中间件,就不得不提一下洋葱模型

image.png

洋葱模型:根据函数分割代码,由外到内依次执行Request的部分逻辑,再由内到外依次执行Response的部分逻辑。

总结

这一章我们了解了redux的工程化,也去实现了combineReducers等一些方法,也看了react-redux的用法与Providerconnect的实现原理,中间件的实现方案。下一章我们一起来探讨一下react的生命周期与事件绑定,直通车 >>> React源码解析系列(十一) -- react生命周期与事件系统的解读