实现一个简单的react-redux

前言

redux主要目的就是为了解决多处对同一状态修改带来的问题,而反映到react上就是多个层级不一样的组件对同一个状态的操做。首先,须要让子组件有方法去访问到统一个状态,在react中恰好context就是作着个事情的,可是若是要进行状态变动的话就须要修改到context里面的状态,这会提升组件间的耦合性。因此咱们能够将context和redux结合起来,既可以经过context获取store又可以经过redux来集中处理state。html

1. 使用context来让全部子组件可以访问state

首先须要搞清楚如何经过使用context来让子组件获取到其中的状态,react

// 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的简单使用模式。git

2. 引入redux,结合context达到redux集中数据管理

经过在Connext.Provider中value传入store,使得包裹的子组件可以经过context获取到store,也所以能够调用getState获取最新的state,subscribe来监听dispatch对状态的修改从而经过setState来从新渲染页面。github

// 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相关和组件解耦。redux

3. 经过connect封装高阶组件将数据以props形式传给子组件的方式解耦

因为组件大量依赖于context和store,致使其复用性不好,因此须要将redux和context相关从组件中抽离,这就须要使用高阶组件来对原组件进行封装,新组件再和原组件经过props来进行数据传递,保持原组件的pure。 首先是经过connect来进行高阶组件的封装:bash

// 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的storeapp

// 由于须要和connect共用一个Context因此封装到一块儿
const Provider = props => {
	return <Context.Provider value={props.store}>{props.children}</Context.Provider>
}

复制代码

至此,这个react-redux已经能用,而且外部组件也只是单纯经过props接收state保持了很好的复用性,context和redux相关也已经和组件实现分离。可是如今的组件从高阶组件接收到的老是全部的state,须要经过一种方式来告诉connect接收哪些数据。ide

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包裹的全部子组件都会从新进行渲染,由于mapStateToPropsmaoDispatchToProps每次返回新的对象,再传给原组件时至关于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就是采用这样的方式。其实这里我也不太明白为何须要这样作。

总结

  1. 经过context可让Context.Provider子组件都可以访问到同一个数据,经过修改Context.Provider的value能够去从新渲染子组件的数据
  2. 经过将store放到context让全部子组件去经过redux的模式去修改渲染state
  3. 直接在组件中混入redux相关内容致使组件复用性不好,因此将redux相关逻辑放入connect封装的高阶组件中,而后经过props的形式传递state给原组件
  4. 全部组件都会接收所有state,须要经过mapStateToProps来描述接收哪些state;修改state也须要获取到dispatch,经过mapDispatchToProps来获取dispatch进行state修改。
  5. 修改任意state都会致使全部组件从新渲染,缘由是mapStateToProps、mapDispatchToProps会返回一个新的对象致使props更新,须要经过在Connect中的shouldComponentUpdate来对两次使用到的state和外部直接传入的props进行对比再决定是否从新渲染

代码地址

相关文章
相关标签/搜索