概述
在react项目中,redux经常用来管理应用的数据,react-redux用来绑定redux, 这样你的组件可以从store中读取数据,并且可以dispatch actions更新store, redux主要思想是让应用中的数据按照约定的规则流动,即单向数据流:
如图: 对于组件的修改,通过action被dispatch到store, store根据action和当前的state计算出下一次的state, component拿到更新后的state重新渲染组件;可以看到数据只是单项流动
疑问
- redux中间件redux-thunk,redux-lodder在上图的单向数据流中其实做了什么?
- react组件如何订阅store中state的变化,通过store.subscribe ? 那么如果保证子组件的订阅更新发生在父组件之后呢?(试想一下:父子组件都订阅了store中某一个state的变化,如果父组件响应订阅更新在子组件之后,子组件可能重复渲染)
- react-redux中的hooks, useSelector, useDispatch, useStore等, 用useSelector替换connect方法?
问题分析
动手实现一个简单的redux, react-redux, 中间件,然后对比一下我们的实现有哪些潜在的问题,实际上怎么解决的
实现一个简单的redux, react-redux
如何使用redux和react-redux, redux.createStore方法创建一个store, store注入到Provider组件上,然后通过connect方法拿到store中的state和dispatcher,然后通过组件的props拿到这些方法和属性
createStore创建store,store对象具有dispatch方法,subscribe方法,getState方法, replaceReducer方法,那么通过观察者模式可以实现
function createStore(reducer, preloadedState) {
let listeners = []
let state = preloadedState
let currentReducer = reducer
let subscribe = (fn) => {
listeners.push(fn)
return () => {
const index = nextListeners.indexOf(listener)
listeners.splice(index, 1)
}
}
let dispatch = (action) => {
state = currentReducer(state, action)
for (let i = 0; i< listeners.length; i++) {
const listener = listeners[i]
listener()
}
}
let getState = () => {
return state
}
let repalceReducer = (nextReducer) => {
currentReducer = nextReducer
dispatch({type:'@@redux/REPLACE'})
}
return {
dispatch
subscribe,
getState,
replaceReducer
}
}
redux实际实现要比这个复杂,比如说listeners存在两个变量中, currentListeners和nextListeners,dispatch执行的总是currentListeners中的函数,subscribe和unsubscibe总是在nextListeners中增加或者移除listener,这样可以避免在dispatching过程中,listeners数组发生改变,上面还可以看到replaceReducer其实更新了state,触发了订阅, replaceReducer可以用在按需加载reducer的场景中,看下combiceReducers的实现,你会发现一次combine几百个reducer并不是一件好事,replaceReducer动态替换reducer提升效率
接下来的问题如何在组件中订阅state的更新,并且可以dispatch action,以更新state;redux-redux使用了Context, redux中的Provider组件就是对Context.Provider做了封装
export const ReactReduxContext = React.createContext(null)
function Provider({store, children, context}) {
const [provider, setProvider] = useState({
state: store.getState(),
dispatch: store.dispatch
})
useEffect(() => {
store.subcribe(() => {
setProvider({
state: store.getState(),
dispatch: store.dispatch
})
})
}, [store])
const Context = context || ReactReduxContext
return <Context>{props.children}</Context>
}
export default Provider
有了Provider组件,我们还需要一个connect函数,connect函数接收mapStateToProps和mapDispatchToProps,返回一个高阶组件,这个高阶组件接收React组件,返回一个PureComponent;
import ReactReduxContext from './Provider'
export default function connect (mapStateToProps, mapDispatchToProps) {
return function (WrappedComment) {
return function (props) {
return (
<ReactReduxContext>
{
({state, dispatch}) => {
<WrappedComment {...mapStateToProps(state, props)} {...mapDispatchToProps(dispatch, props)} />
}
}
</ReactReduxContext>
)
}
}
}
其实这个返回的组件,react-redux又用React.memo进行包装,保证只在props发生变化的时候才会重新渲染。react-redux对于connect的实现比这里要复杂得多,如开头提出的问题: react-redux需要保证父组件的更新在子组件之前,react的connect方法其实是对connectAdvanced方法的封装(参见官网),connectAdvanced方法放回一个高阶组件,如上面所封装的connect方法,返回的组件又使用React.memo变为PureComponent, react-redux如何保证父组件订阅store的更新,发生在子组件之前呢? 也是通过subscirpe方法;每一个connect方法返回的高阶组件内部都用了一个Context.Provider包装WrappedComponent, 她的value为store和Subscription对象,这样每一个子组件的Subscription对象可以拿到离它最近的父组件的Subscription对象,这样形成了一个Subscription订阅树,通过Subvcription控制更新顺序, 直接上图
中间件实现
什么是中间件?中间件就是对dispatch方法做了封装,比如说每次dispatch一个action前,我需要发送一条日志,如果不使用中间件,你的做法是
log(url)
dispatch(action)
每触发一个action都要这样写,是不是很麻烦呢,可以考虑用中间件解决;中间件就是在redux流程中对redux dispatch方法做了封装;
先看下redux中间件的结构:
({getState, dispatch}) => (nextDispatch) => (action) => {
// do something here
nextDispatch(action)
}
做过react项目的同学都知道, applyMiddleware方法,可以将中间件组合起来,其内部使用了compose方法
假设有三个函数a,b,c,那么
const d = compose(a,b,c)
// d(x) 其实就等同于a(b(c(x)))
// compose(...middlewares)(store.dispatch)
// 就等同于middleware1(middleware2(store.dispatch))
我们看下redux里的实现:
export default function compose(...funcs) {
if (funcs.length === 0) {
return (arg) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
这样就实现了dispatch的包装
react-redux中的hooks api
一共有四个, useStore, useDispatch, useReduxContext, useSelector, 前三个没什么好说的,顾名思义,获取redux的store, dispatch, context,主要是useSelector, 这个api理论上可以取代connect方法;简单说一下useSelector方法的实现,这个函数的内部也有一个Subscription对象,如上面图中的订阅对象,每次会检查selector方法返回的state是否更新,来确定组件是否重新渲染,检查state更新的方法是根据useSlector的第二个参数:equalityFn,equalityFn默认等于
const refEquality = (a, b) => a === b
总结
redux和react-redux的api很多,没有一个个的去解释;介绍了核心流程:redux和react-redux数据的主流程,如上图中:需要明白数据是单向数据流,react-redux通过订阅对象来实现父子组件的更新;还有很多细节点:listeners其实是一个双向链表,mapStateToProps和mapDispatchToProps其实有多种类型,react-redux代理模式和开闭原则,保证了当后续这些参数类型发生变化后,可以方便扩展,react-redux和redux用的打包工具是rollup等。
先熟悉这些api的用法,看源码的时候再对照本文可以少走一点弯路
参考文章:https://www.cnblogs.com/fuGuy...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。