上一章讲了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;
上述代码的本质就是按模块来处理不同的reducer
,state
,action
。处理的结果是:
我们在触发事件,现在不是单一的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
的作用可以把state
、dispatch
映射到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
函数,接受两个参数mapStateToProps
、mapDispatchToProps
返回一个函数,函数的入参为组件,返回一个可供用户调用的包含props
、mapStateToProps
、mapDispatchToProps
组件,源码的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} />
}
}
}
中间件应用方案
applyMiddleWare
是redux
提供的一种中间件处理方案,他能够配合一些其他的插件帮助我们在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-saga
、redux-thunk
等插件,在这里就过多的写代码了,其实说到中间件,也可以理解成具有导向的钩子函数吧,怎么理解呢?在一个执行流程中,我们编写代码一定要符合开闭原则
(对修改关闭,对扩展开放),但是开放指的不是去修改代码本体
,比如装饰器模式
,那中间件也可以理解成钩子函数
,是对现有的代码做的一种扩展的应用程序函数。说到中间件,就不得不提一下洋葱模型
。
洋葱模型:根据函数分割代码,由外到内依次执行Request
的部分逻辑,再由内到外依次执行Response
的部分逻辑。
总结
这一章我们了解了redux
的工程化,也去实现了combineReducers
等一些方法,也看了react-redux
的用法与Provider
、connect
的实现原理,中间件的实现方案。下一章我们一起来探讨一下react
的生命周期与事件绑定,直通车 >>> React源码解析系列(十一) -- react生命周期与事件系统的解读。