React Hooks是React@16.8版本官方新的编写推荐,前阵子简单对hooks进行了学习,也在探索适用它的使用场景,本文介绍的是redux的hooks实现方案,包括redux常用api的hooks版实现原理及优化方案。 PS:关于如何使用hooks进行编程可以直接看下官方文档Hooks官方文档
先列个目录: 1、实现原理 2、简单方案:递归Store实现redux 3、优化方案:合并Store实现redux
1. 实现思路
在 React 项目中,我们一般按照单一的组件数据流就能实现大部分功能,但随着业务复杂度的增加,跨组件之间的通讯在单一数据流的情况下难以实现。由此 redux、flux、dva、vuex 等统一管理数据的库活着工具就出来了。
redux 集中式的管理数据,通过 Provider 组件将数据共享给子组件,然后再通过 connect 将 store 与组件进行连接,这是 redux 最核心的功能。
既然 redux 核心是为了实现数据共享,我们可以使用 React Context API 上下文来实现, 原理是使用生产子/消费者模式,顶层通过 Provider 去下发数据,子组件通过 Consumer 来消费由 Provider 里提供的数据和方法,从而实现数据共享。
2.方案一:递归Store
目录

应用入口:index.js
import React from 'react';
import ReactDOM, {render} from 'react-dom';
import App from './components/App'
import Provider from './store/provider'
// 挂载节点
render((
<Provider>
<App/>
</Provider>
), document.getElementById('app')
)
在顶层引入【Provider】组件,为所有的子孙组件提供所有数据源store,使得子孙组件拥有获取数据的能力。
store 设计
数据项:count.js
import React, { useReducer } from 'react'
// 初始化state
const initState = {
count: 0
}
// reducer处理器
const reducer = (state, action) => {
const { type, payload } = action
switch (type) {
case 'ADD_COUNT': return { ...state, count: state.count + 1 }
default: return state;
}
}
// 创建上下文
const Context = React.createContext()
const Provider = (props) => {
const [state, dispatch] = useReducer(reducer, initState)
return (
<Context.Provider value={{state, dispatch}}>
{props.children}
</Context.Provider>
)
}
export default { Context, Provider }
数据项的代码中可以看出initState,reducer的定义与用redux时是一摸一样,重点看下面的创建上下文 首相通过React.createContext() 创建一个空的上下文Context,然后定义Provider这个组件,内部使用useReducer把处理器reducer和初始化的initState传入进去,返回的state和dispatch提供到Provider作为数据源。
数据项聚合:provider.js
import React from 'react'
import Count from './modules/count';
import Todo from './modules/todo';
// 聚合count、todo这些数据项
const providers = [
Count.Provider,
Todo.Provider
];
// 递归包裹Provider
const ProvidersComposer = (props) => (
props.providers.reduceRight((children, Parent) => (
return Parent({children})
), props.children)
)
const Provider = (props) => {
return (
<ProvidersComposer providers={providers}>
{props.children}
</ProvidersComposer>
)
}
export default Provider

最后出来的组件结构: Provider > Context.Provider > Context.Provider > App
我们通过ProviderComposer进行递归包裹,把每个Provider进行一层一层的包裹
这里使用了parent({children})替代了<Parent>{children}</Parent>,这样的做法可以减少一层嵌套结构。
数据聚合的思路方案来自:github.com/facebook/re…
如何使用
import React, { useContext, useEffect } from 'react';
// 引入count数据源
import CountStore from '@/store/modules/count'
const App = (props) => {
// 通过useContext使用Count这个store的上下文
const {state, dispatch} = useContext(CountStore.Context)
// 每秒更新count
useEffect(() => {
setInterval(() => {
dispatch({ type: 'ADD_COUNT' })
}, 1000);
}, [])
return (
<div className='app'>
{JSON.stringify(state)}
</div>
)
}
export default App
通过useContext传入对应store的Context,从而获取到由外部对应store的Provider提供的state和dispatch,最终展示效果如下:

存在问题
- 问题一:嵌套复杂问题

实际上,被包裹了很多层,在这个store不断拓展的情况下,N个store,就会包裹N层,App会被包裹得很深入,而且我们调试起来也很麻烦,这简直就是「嵌套地狱」啊!
- 问题二:重新渲染问题
// 获取count、todo、xxx
const {countState, countDispatch} = useContext(CountStore.Context)
const {totoState, todoDispatch} = useContext(TodoStore.Context)
const {xxxState, xxxDispatch} = useContext(xxxStore.Context)
当内部组件需要使用多个store的情况下,我们需要使用多个上下文。当其中一个store中某个不相关的state进行更新,组件可能又需要重新渲染一遍,不仅调用麻烦,还会造成无谓的渲染问题。
那么,有没有更好的方案呢?
3.方案二:合并Store
针对方案一的问题,我们有了新的目标: 1、能否减少嵌套地狱的问题? 2、能否对使用的state,dispatch进行合理的分发? 3、能否减少无谓的重新渲染问题?
Store设计
数据项:count.js
export const state = {
count: 0
}
export const actions = {
addCount: payload => ({
type: 'ADD_COUNT',
payload
})
}
export const reducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case 'ADD_COUNT':
return { ...state, count: state.count + 1 }
default:
return state;
}
}
export default { reducer, state, actions }
数据项这里有点不一样,state跟reducer与方案一样,这里多了个actions,主要用于dispatch的分发。
上下文:context.js
import React, { createContext } from 'react'
// 全局上下文Context
const Context = createContext('context')
export default Context
创建并提供全局的上下文
供应商:provider.js
import React, { useReducer, useContext } from 'react'
import Context from './context'
import count from './modules/count'
import todo from './modules/count'
// 聚合数据项store
const stores = {
count,
todo
}
// 全局供应商Provider
const Provider = (props) => {
const reducers = {}
const combineStates = {}
Object.keys(stores).forEach((key) => {
reducers[key] = stores[key].reducer
// 合并state
combineStates[key] = stores[key].state
})
// 合并reducer
const combineReducers = combine(reducers)
// 获取state、dispatch
const [state, dispatch] = useReducer(combineReducers, combineStates);
return (
<Context.Provider value={{ state, dispatch }}>
{props.children}
</Context.Provider>
)
}
// 对reducer进行合并
function combine (reducers) {
return function (state = {}, action) {
return Object.keys(reducers).reduce((newState, key) => {
newState[key] = reducers[key](state[key], action)
return newState
}, {})
}
}
export default Provider
这里是关键代码,我们看看这里做了什么
1、引入全局上下文Context
2、聚合store,汇总到stores对象里
3、把所有store的state合并成 combineStates
4、把所有store的reducer合并成 combineReducers
5、把合并后的combineReducers和combineStates传入useReducer,获取到state和dispatch
5、把state、dispatch作为数据源给到Context.Provider
在完成上面所有步骤后,我们就得到一个全局唯一的供应商Provider
如何使用?
import React, { useState, useContext, useEffect} from 'react';
import { Context } from '@/store'
const App = (props) => {
const {state, dispatch} = useContext(Context)
// 每秒更新count
useEffect(() => {
setInterval(() => {
dispatch({ type: 'ADD_COUNT' })
}, 1000);
}, [])
return (
<div className='app'>
{JSON.stringify(state)}
</div>
)
}
export default App
使用方法跟方案一一样,但是唯一不同的是:Context由使用单个store,变为使用全局stores。这个Context包含了全部的store。
最终展示效果如下:

存在问题
- 问题一:重新渲染问题
任何一个store中某个不相关的state进行更新,使用了useContext的组件就得重新渲染。
解决方案
1、如何分发state、dispatch
我们使用连接器conncet把组件与store连接起来,并把所需的state、dispatch分发到组件的props里。
import React, { useContext, useMemo } from 'react';
import { Context } from './context'
// 连接器
function connect (mapStateToProps, mapDispatchToProps) {
return WrappedComponent => {
const MemoHook = (props = {}) => {
const { state, dispatch } = useContext(Context)
// 分发state
const mapState = handleState(mapStateToProps, state, props)
// 分发dispatch
const mapDispatch = handleDispatch(mapDispatchToProps, dispatch, props)
return (
<WrappedComponent {...props} {...mapState} {...mapDispatch} />
)
}
return MemoHook
}
}
// 实现mapStateToProps
function handleState (mapStateToProps, state, props) {
if (typeof mapStateToProps === 'function') {
return mapStateToProps(state, props)
} else {
return {}
}
}
// 实现mapDispatchToProps
function handleDispatch (mapDispatchToProps, dispatch, props) {
if (typeof mapDispatchToProps === 'function') {
// 如果mapDispatchToProps是函数,传入dispatch、props
return mapDispatchToProps(dispatch, props)
} else if (typeof mapDispatchToProps === 'object') {
// 如果mapDispatchToProps是对象,则遍历对象进行处理
let obj = {}
Object.keys(mapDispatchToProps).forEach((key) => {
obj[key] = (p) => dispatch(mapDispatchToProps[key](p), props)
})
return obj
} else {
return {}
}
}
export default connect;
conncet连接器与redux的conncet原理相似, 接受2个参数mapStateToProps、mapDispatchToProps
1、获取全局上下文Context的state、dispatch 2、通过handleState进行state的分发 3、通过handleDispatch进行action的分发 4、把过滤出来的state、action注入到组件里

如何使用?
import React, { useState, useEffect} from 'react';
import { Context, connect } from '@/store'
import { actions as countActions } from '@/store/modules/count'
const App = (props) => {
// 每秒更新count
useEffect(() => {
setInterval(() => {
// addCount已经注入到props里了
props.addCount()
}, 1000);
}, [])
return (
<div className='app'>
{/* state.count 已经注入到props了 */}
{JSON.stringify(props.count)}
</div>
)
}
// 使用方法与redux的conncet一样
export default connect(
// mapStateToProps
({count}) => ({count}),
// mapDispatchToProps
(dispatch, props) => ({
addCount: (args) => dispatch(countActions.addCount(args))
})
)(App)
我们通过conncet把组件与store连接起来,并把对应的state和dispatch分发到组件的props中。 使用方法和redux的connect一样,可以看看文档connect使用方法
2、如何解决多余的重新渲染问题
我们使用到useMemo,只需在conncet中进行点小改造
function connect (mapStateToProps, mapDispatchToProps) {
return WrappedComponent => {
const MemoHook = (props = {}) => {
const { state, dispatch } = useContext(Context)
const mapState = handleState(mapStateToProps, state, props)
const memoState = [...Object.values(mapState), ...Object.values(props)]
return useMemo(() => {
const mapDispatch = handleDispatch(mapDispatchToProps, dispatch, props)
return (
<WrappedComponent {...props} {...mapState} {...mapDispatch} />
)
}, memoState)
}
return MemoHook
}
}
我们把分发state及组件的props记录下来,作为useMemo的监听目标,当state或者props的内容更新后才重新渲染组件WrapperComponent。这样那些不相关的store更新时就不会触发到WrapperComponent进行重新渲染。 我们把handleDispatch放到的useMemo内,因为actions在connect时就应该被定义好,后续也不会修改,所以没必要重新渲染时再去重新处理
最终效果:
