前言
redux主要目的就是为了解决多处对同一状态修改带来的问题,而反映到react上就是多个层级不同的组件对同一个状态的操作。首先,需要让子组件有方法去访问到统一个状态,在react中刚好context就是做着个事情的,但是如果要进行状态变更的话就需要修改到context里面的状态,这会提高组件间的耦合性。所以我们可以将context和redux结合起来,既能够通过context获取store又能够通过redux来集中处理state。
1. 使用context来让所有子组件能够访问state
首先需要搞清楚如何通过使用context来让子组件获取到其中的状态,
// context.js
export const Context = React.createContext()
// index.js
class Index extends Component {
state = {
themeColor: 'red'
}
render() {
return (
// 在需要用到状态的组件外部包裹Provider
<Context.Provider value={this.state}>
<Header />
<Content changeColor={e => this.setState({ themeColor: e })} />
</Context.Provider>
)
}
}
class Header extends Component {
static contextType = Context
render() {
// 通过this.context进行访问状态
return <h1 style={{ color: this.context.themeColor }}>hong</h1>
}
}
而如果需要直接通过context来修改数据,就需要通过修改顶层的value来重新渲染数据。 至此,我们就了解了context的简单使用模式。
2. 引入redux,结合context达到redux集中数据管理
通过在Connext.Provider中value传入store,使得包裹的子组件能够通过context获取到store,也因此可以调用getState获取最新的state,subscribe来监听dispatch对状态的修改从而通过setState来重新渲染页面。
// index.js
const createStore = reducer => {
let state = null
const listeners = [] // 存储渲染回调
const getState = () => state // 外部需要获取最新的state传给renderApp
const dispatch = action => {
state = reducer(state, action)
listeners.forEach(fn => fn())
}
const subscribe = fn => {
listeners.push(fn)
}
dispatch({}) //初始化state
return { dispatch, subscribe, getState }
}
const reducer = (state, action) => {
if (!state) {
return {
themeColor: 'red'
}
}
switch (action.type) {
case 'CHANGE_COLOR':
return {
...state,
themeColor: action.payload
}
default:
return state
}
}
const store = createStore(reducer)
class Index extends Component {
state = {
themeColor: 'red'
}
render() {
return (
<Context.Provider value={store}>
<Header />
<Content changeColor={e => this.setState({ themeColor: e })} />
</Context.Provider>
)
}
}
// ...
// 子组件内
class ThemeSwitch extends Component {
static contextType = Context
state = {
themeColor: ''
}
componentWillMount() {
const store = this.context
this.setState({
themeColor: store.getState().themeColor
})
store.subscribe(() => {
this.setState({
themeColor: store.getState().themeColor
})
})
}
render() {
return (
<div>
<button
style={{ color: this.state.themeColor }}
onClick={() => {
this.context.dispatch({ type: 'CHANGE_COLOR', payload: 'red' })
}}
>
Red
</button>
<button
style={{ color: this.state.themeColor }}
onClick={() => {
this.context.dispatch({ type: 'CHANGE_COLOR', payload: 'blue' })
}}
>
Blue
</button>
</div>
)
}
}
但是这样直接去结合context和redux会使得业务代码和redux相关代码耦合严重,非常不好使用,需要将redux相关和组件解耦。
3. 通过connect封装高阶组件将数据以props形式传给子组件的方式解耦
由于组件大量依赖于context和store,导致其复用性很差,所以需要将redux和context相关从组件中抽离,这就需要使用高阶组件来对原组件进行封装,新组件再和原组件通过props来进行数据传递,保持原组件的pure。 首先是通过connect来进行高阶组件的封装:
// react-redux.js
// 将之前的组件中redux和context相关放入connect,然后将所有state全部以props传给子组件
const Context = React.createContext()
const connect = WrapperComponent => {
class Connect extends React.Component {
static contextType = Context
state = {
allProps: {}
}
componentWillMount() {
const store = this.context
this.setState({
allProps: store.getState()
})
store.subscribe(() => {
this.setState({
allProps: store.getState()
})
})
}
_change(e) {
const { dispatch } = this.context
dispatch({ type: 'CHANGE_COLOR', payload: e })
}
render() {
return <WrapperComponent {...this.state.allProps} change={e => this._change(e)} />
}
}
return Connect
}
// ...
同时在子组件中通过connect(Component)的形式导出这个高阶组件,而原先index.js中Context.Provider由于Context已经移到react-redux.js中,所以也需要对外部导出一个Provider去接收外部传给context的store
// 因为需要和connect共用一个Context所以封装到一起
const Provider = props => {
return <Context.Provider value={props.store}>{props.children}</Context.Provider>
}
至此,这个react-redux已经能用,并且外部组件也只是单纯通过props接收state保持了很好的复用性,context和redux相关也已经和组件实现分离。但是现在的组件从高阶组件接收到的总是所有的state,需要通过一种方式来告诉connect接收哪些数据。
4. 通过mapStateToProps和mapDispatchToProps来告诉connect接收的state和如何触发dispatch
在之前的基础上,我们需要知道每个原组件需要获取哪些state。
我们传入一个名为mapStateToprops
的函数,它接受最新的state作为参数,返回一个对象作为原组件接受的state的props;同样,我们需要在修改state的组件中,告诉connect我们接受一个怎样dispatch的函数,我们传入一个名为mapDispatchToprops
的函数,它接受dispatch作为参数,返回一个对象作为原组件接受的修改state的函数的props:
const mapStateToProps = state => {
return {
themeColor: state.themeColor
}
}
const mapDispatchToProps = dispatch => {
return {
change(color) {
dispatch({ type: 'CHANGE_COLOR', payload: color })
}
}
}
// 暂时这样进行调用
export default connect(
ThemeSwitch,
mapStateToProps,
mapDispatchToProps
)
现在,每个组件都能够只获取到本组件使用到的state和修改state的函数。
const connect = (WrapperComponent, mapStateToProps, mapDispatchToProps) => {
class Connect extends React.Component {
static contextType = Context
state = {
allProps: {}
}
componentWillMount() {
const store = this.context
this._change(store)
store.subscribe(() => {
this._change(store)
})
}
// 需要在初始化和执行dispatch回调时都获取最新的state
_change(store) {
const stateProps = mapStateToProps ? mapStateToProps(store.getState()) : {}
const dispatchProps = mapDispatchToProps ? mapDispatchToProps(store.dispatch) : {}
this.setState({
allProps: {
...stateProps,
...dispatchProps
}
})
}
render() {
// 对外部直接传入的props也直接传给原组件
return <WrapperComponent {...this.state.allProps} {}...this.props}/>
}
}
return Connect
}
5. 对react-redux进行渲染优化
现在我们的react-redux还存在一些问题,就是当state改变时,Provider包裹的所有子组件都会重新进行渲染,因为mapStateToProps
和maoDispatchToProps
每次返回新的对象,再传给原组件时相当于props发生改变,就会引起重新渲染。现在我们要对它进行优化。
暂时我自己优化的方法是通过在Connect组件的shouldComponentUpdate
方法中通过判断state的改变来达到优化渲染的目的。
通过在每次调用connect时使用一个变量保存当前使用到的state,在下一次渲染时候在shouldComponentUpdate中对比两次使用到的state是否发生变化来决定是否渲染,同时还需要对外部直接传递的props进行判断是否变化。
const connect = (mapStateToProps, mapDispatchToProps) => {
let oldState // 保存当前使用的state
return WrapperComponent => {
class Connect extends React.Component {
static contextType = Context
state = {
allProps: {}
}
componentWillMount() {
const store = this.context
oldState = mapStateToProps(store.getState())
this._change(store)
store.subscribe(() => {
this._change(store)
})
}
shouldComponentUpdate(props) {
// 判断直接传入的props是否更改
if (Object.keys(props).some(key => props[key] !== this.props[key])) {
return true
}
const newState = mapStateToProps(this.context.getState())
// 判断两次使用的state是否更改
const flag = Object.keys(oldState).some(key => oldState[key] !== newState[key])
oldState = newState
return flag
}
_change(store) {
const stateProps = mapStateToProps ? mapStateToProps(store.getState()) : {}
const dispatchProps = mapDispatchToProps ? mapDispatchToProps(store.dispatch) : {}
this.setState({
allProps: {
...stateProps,
...dispatchProps
}
})
}
render() {
return <WrapperComponent {...this.state.allProps} {...this.props} />
}
}
return Connect
}
}
这里调用connect和之前不太一样,这里会返回一个函数而不是返回高阶组件,react-redux就是采用这样的方式。其实这里我也不太明白为什么需要这样做。
总结
- 通过context可以让Context.Provider子组件都能够访问到同一个数据,通过修改Context.Provider的value可以去重新渲染子组件的数据
- 通过将store放到context让所有子组件去通过redux的模式去修改渲染state
- 直接在组件中混入redux相关内容导致组件复用性很差,所以将redux相关逻辑放入connect封装的高阶组件中,然后通过props的形式传递state给原组件
- 所有组件都会接收全部state,需要通过mapStateToProps来描述接收哪些state;修改state也需要获取到dispatch,通过mapDispatchToProps来获取dispatch进行state修改。
- 修改任意state都会导致所有组件重新渲染,原因是mapStateToProps、mapDispatchToProps会返回一个新的对象导致props更新,需要通过在Connect中的shouldComponentUpdate来对两次使用到的state和外部直接传入的props进行对比再决定是否重新渲染