實現一個簡單的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進行對比再決定是否從新渲染

代碼地址

相關文章
相關標籤/搜索