深入理解Redux
深入理解Redux
本文代码详见我的 GitHub 源码链接
Redux 和 React-Redux
Redux是一个状态管理库,用于解决组件间状态共享/通信的问题。
Redux的整体思路是通过仓库(Store)存储状态(state)和更新状态(setState),并通过发布订阅模式在状态更新时,重新渲染依赖状态组件。
React Redux则是将仓库(Store)和组件进行连接的工具,让React组件可以使用注入其中的读写状态接口。
基本概念
store:仓库,用来存储状态及更新状态所需的发布订阅系统
state:状态,用于存储状态信息(数据)
setState:状态变更方法,用于更新状态,并发布数据变更信息
核心概念
Redux及React Redux有以下核心概念:
action: 操作,包括操作类型(type)和更新的数据(payload)
reducer:规范state状态(数据),确保更新数据时,生成一个新的对象
dispatch:派发函数,用来派发操作(action)。
是写接口的封装,简化setState(reducer(旧数据state,操作action))
connect:将组件与全局状态(state)连接起来。
输入一个组件,返回一个包装好的组件(为原组件提供读接口state和写接口dispatch)。这样直接使用包装好的组件即可。 connect是React Redux实现的
除此之外connect还是一个经过柯里化(currying)的函数,connect提供MapStateToProps(类似selector)功能,只在选中的数据发生变化时进行更新(提供读接口封装)。提供MapDispatchToProps功能,对dispatcher进行封装简化 (提供写接口的封装)
柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)。 详见链接
Provider: 全局状态(state)由Provider提供给子组件,connect获取后包装好后,新的组件即可使用。
动手实践!手写Redux
分步详解,可通过每一个commit及代码注释获取。详见 Commits
- store:仓库,用来存储状态及更新状态所需的发布订阅系统
将数据及操作单独存储为store变量,state/setState读写数据,并提供订阅发布接口,在setState数据变动时,调用组件传入的函数(发布)。所有依赖数据的组件都需要订阅,因此在connect中提供数据给组建时,可以同时进行订阅(由于只需订阅一次,因此使用useEffect)。
//用于初始化redux(reducer和state)
export const createStore = (reducer, initState) => {
store.state = initState
store.reducer = reducer
return store
}
export const store = {
state: undefined,
reducer: undefined,
listeners: [],
subscribe(fn) {
store.listeners.push(fn)
//返回取消订阅函数
return () => {
const idx = store.listeners.indexOf(fn)
this.listeners.splice(idx, 1)
}
},
update() {
store.listeners.map(fn => fn({}))
},
setState(newValue) {
store.state = newValue
store.update()
}
}
- reducer:规范state状态(数据),确保更新数据时,生成一个新的对象。封装reducer函数,输入旧对象和操作(操作类型type+新数据payload),输出一个新的对象,避免因引用相同,Hooks不触发重新渲染。
const initState = {user: {name: "howard", age: 23}}
const reducer = (state, action) => {
const {type, payload} = action
if (type === "updateUser") {
return {
...state,
user: {
...state.user,
...payload
}
}
} else {
return state
}
}
- dispatch:用来简化setState(reducer(旧数据,操作)),组件通过props获取dispatch。封装dispatch函数名,输入操作action(type+payload),执行操作。因为只有操作action会变化,其他都不会,算是一个语法糖。 dispatch需要通过props提供给组件,即需要对组件进行一层包装。这样直接使用包装好的组件即可。
- connect:将组件与全局状态连接起来。输入一个组件,返回一个包装好的组件(为原组件提供dispatch)。这样直接使用包装好的组件即可。
//connect中
const connect = (Component)=>{
...
const dispatch = (action) => {
setState(store.reducer(state, action))
}
...
return <Component {...props} {dispatcher} {state}/>
}
注意:
- 此时可以通过三种方式访问数据,似乎接口不统一,目前统一使用connect传入的props获取。
- 全局state,因为要提供给provider value,所以必须引入
- 通过useContext获取
- 通过connect封装时传入的dispatch和appState获取(本质上也是使用useContext)
- 增加测试,发现所有通过connect依赖store的组件,在任何数据产生变化时都会渲染,不论自己依赖的数据是否变化,需要优化
通过柯里化(currying)为connect提供MapStateToProps(selector)功能,只在选中的数据发生变化时进行更新。
MapStateToProps封装读功能:
//所有用到store数据的组件都应该用connect包裹
//通过柯里化增加selector细化组件依赖的数据
export const connect = (MapStateToProps, MapDispatchToProps) => (Component) => {
//返回一个对Component进行了包装的新函数组件
return (props) => {
//使用一个setState,在dispatch变更数据时,进行渲染
const [, update] = useState({})
const {state, setState} = store
//每个依赖state的组件订阅数据变动
//每次产生新数据时(包括第一次生成),connect都会记下数据,监听器发布时,数据已经变化,对比store的数据和记录的数据即可,然后connect会重新执行,记下新的数据。
const data = MapStateToProps ? MapStateToProps(state) : {state}
...
return <Component {...props} {...dispatchers} {...data}/>
}
}
疑问:如何获取旧数据以确认是否更新?
- 每次产生新数据时(包括第一次生成),connect都会记下数据,监听器发布时,数据已经变化,对比store的数据和记录的数据即可,然后connect会重新执行,记下新的数据。
为防止重复订阅,useEffect中返回取消订阅的函数,在下次调用useEffect之前取消订阅。 useEffect返回的函数是在组件卸载的时候执行/执行当前 effect 之前对上一个 effect 进行清除
//同样在connect函数中
...
useEffect(() => {
//subscribe的返回值是取消该订阅的函数,在下次调用useEffect时执行
return store.subscribe(() => {
//更新订阅中调用的函数,如果依赖的数据变化了,再更新。
const newData = MapStateToProps ? MapStateToProps(store.state) : {state: store.state}
if (hasChanged(data, newData)) {
update({})
}
})
}, [MapStateToProps])
...
同样通过柯里化为connect提供MapDispatchToProps(提供写接口的封装)功能
MapDispatchToProps对dispatcher进行封装简化:封装写功能
//同样在connect函数中
const dispatch = (action) => {
setState(store.reducer(state, action))
update({})
}
const dispatchers = MapDispatchToProps ? MapDispatchToProps(dispatch) : {dispatch}
return <Component {...props} {...dispatchers} {...data}/>