先来研究下 Redux 中几个常见方法的原理。
初识 reducer 原理
reducer 是用来干什么的?一句话概括,reducer 是用来规范 react state 的创建流程的。
从下边这段代码我们可以看出,更新用户名时直接修改了 state 的值,这样是不规范的。
import React, { useState, useContext, createContext } from 'react'
const appContext = createContext(null)
const User = () => {
const { appState } = useContext(appContext)
return <div>User: {appState.user.name}</div>
}
const UserModifier = () => {
const { appState, setAppState } = useContext(appContext)
const onChange = (e) => {
appState.user.name = e.target.value // 直接修改了 state 的值,不规范
setAppState({ ...appState })
}
return <div><input onChange={onChange} value={appState.user.name}></input></div>
}
const FirstChild = () => <section>firstChild<User /></section>
const SecondChild = () => <section>secondChild<UserModifier /></section>
const ThirdChild = () => <section>thirdChild</section>
export const App = () => {
const [appState, setAppState] = useState({
user: { name: 'marshall', age: 22 }
})
const contextValue = { appState, setAppState }
return <appContext.Provider value={contextValue}>
<FirstChild/>
<SecondChild/>
<ThirdChild/>
</appContext.Provider>
}
我们可以这样改写下,这就是一个简化版的 reducer 函数:
import React, { useState, useContext, createContext } from 'react'
const appContext = createContext(null)
const reducer = (state, { type, payload }) => { // 简化版 reducer
if (type === 'updateUser') {
return {
...state,
user: {
...state.user,
...payload
}
}
} else {
return state
}
}
const User = () => {
const { appState } = useContext(appContext)
return <div>User: {appState.user.name}</div>
}
const UserModifier = () => {
const { appState, setAppState } = useContext(appContext)
const onChange = (e) => {
setAppState(reducer(appState, { type: 'updateUser', payload: { name: e.target.value } }))
}
return <div><input onChange={onChange} value={appState.user.name}></input></div>
}
const FirstChild = () => <section>firstChild<User /></section>
const SecondChild = () => <section>secondChild<UserModifier /></section>
const ThirdChild = () => <section>thirdChild</section>
export const App = () => {
const [appState, setAppState] = useState({
user: { name: 'marshall', age: 22 }
})
const contextValue = { appState, setAppState }
return <appContext.Provider value={contextValue}>
<FirstChild/>
<SecondChild/>
<ThirdChild/>
</appContext.Provider>
}
dispatch 规范 setState 流程
看下边这段代码,如果我们要修改 state 中的其他参数,那我们每次都要写 setAppState,reducer 和 appState,太冗余了。
const UserModifier = () => {
const { appState, setAppState } = useContext(appContext)
const onChange = (e) => {
// 重复度很高
setAppState(reducer(appState, { type: 'updateUser', payload: { name: e.target.value } }))
setAppState(reducer(appState, { type: 'updateGroup', payload: { group: e.target.value } }))
setAppState(reducer(appState, { type: 'updateTitle', payload: { title: e.target.value } }))
}
return <div><input onChange={onChange} value={appState.user.name}></input></div>
}
我们可以手写一个 dispatch 优化
const UserModifierWrapper = () => {
const { appState, setAppState } = useContext(appContext)
const dispatch = (action) => {
setAppState(reducer(appState, action))
}
return <UserModifier dispatch={dispatch} state={appState}/>
}
const UserModifier = ({ dispatch, state }) => {
const onChange = (e) => {
dispatch({ type: 'updateUser', payload: { name: e.target.value } })
dispatch({ type: 'updateGroup', payload: { group: e.target.value } })
dispatch({ type: 'updateTitle', payload: { title: e.target.value } })
}
return <div><input onChange={onChange} value={state.user.name}></input></div>
}
connect 来历
dispatch 和 connect 由 react-redux 实现。
在上边 dispatch 的模块我们为了获取全局上下文,给 <UserModifier /> 组件封装了一个 Wrapper, 那如果 <User /> 组件也需要这样封装那代码就过于冗余了。于是 react-redux 给我们提供了 connect 方法,方便我们将组件与全局上下文关联起来,下边是一个 connect 方法的简单实现。
const connect = (Component) => {
return (props) => {
const { appState, setAppState } = useContext(appContext)
const dispatch = (action) => {
setAppState(reducer(appState, action))
}
return <Component {...props} dispatch={dispatch} state={appState}/>
}
}
const UserModifier = connect(
({ dispatch, state }) => {
const onChange = (e) => {
dispatch({ type: 'updateUser', payload: { name: e.target.value } })
}
return <div><input onChange={onChange} value={state.user.name}></input></div>
}
)
使用 connect 减少 render
目前只有 <FirstChild /> 中使用了 appState.user.name 属性。但当我们修改 appState.user.name 属性时,其他没用到 appState 的组件也被刷新了,这是因为 appState 是 <App /> 组件下的 state, 因此当我们调用 setState 的时候它的子组件都会被刷新。
我们可以使用 connect 来减少 reRender。
import React, { useState, useContext, createContext, useEffect } from 'react'
const appContext = createContext(null)
// 首先要把 <App /> 中的 setState 干掉,避免子组件全部重新执行
const store = {
state: {
user: { name: 'marshall', age: 22 }
},
setState (newState) {
store.state = newState
store.listeners.map(fn => fn(store.state))
},
listeners: [],
subscribe (fn) {
store.listeners.push(fn)
return () => {
const index = store.listeners.indexOf(fn)
store.listeners.splice(index, 1)
}
}
}
const reducer = (state, { type, payload }) => {
if (type === 'updateUser') {
return {
...state,
user: {
...state.user,
...payload
}
}
} else {
return state
}
}
const connect = (Component) => {
return (props) => {
const { state, setState, subscribe } = useContext(appContext)
const [_, update] = useState({}) // 当 setState 时, update 只能刷新当前组件。
useEffect(() => {
subscribe(() => update({}))
}, [])
const dispatch = (action) => {
setState(reducer(state, action))
}
return <Component {...props} dispatch={dispatch} state={state}/>
}
}
const User = connect(({ state }) => {
console.log('User reRender' + Math.random())
return <div>User: {state.user.name}</div>
})
const UserModifier = connect(
({ dispatch, state }) => {
console.log('UserModifier reRender' + Math.random())
const onChange = (e) => {
dispatch({ type: 'updateUser', payload: { name: e.target.value } })
}
return <div><input onChange={onChange} value={state.user.name}></input></div>
}
)
const FirstChild = () => {
console.log('FirstChild reRender' + Math.random())
return <section>firstChild<User /></section>
}
const SecondChild = () => {
console.log('SecondChild reRender' + Math.random())
return <section>secondChild<UserModifier name={'marshallddddd'}/></section>
}
const ThirdChild = () => {
console.log('thirdChild reRender' + Math.random())
return <section>thirdChild</section>
}
export const App = () => <appContext.Provider value={store}>
<FirstChild/>
<SecondChild/>
<ThirdChild/>
</appContext.Provider>
这样我们就可以做到当 state 更新时只有被 connect 了的组件会被更新
Redux 乍现
通过上边 reducer, dispatch 和 connect 这三个例子,redux 最常用的几个方法就介绍完了,下边我们将这几个方法抽象出来封装一个 redux.js 文件
// redux.jsx
import React, { useState, useContext, createContext, useEffect } from 'react'
export const connect = (Component) => {
return (props) => {
const { state, setState, subscribe } = useContext(appContext)
const [_, update] = useState({}) // 当 setState 时, update 只能刷新当前组件。
useEffect(() => {
subscribe(() => update({}))
}, [])
const dispatch = (action) => {
setState(reducer(state, action))
}
return <Component {...props} dispatch={dispatch} state={state}/>
}
}
export const reducer = (state, { type, payload }) => {
if (type === 'updateUser') {
return {
...state,
user: {
...state.user,
...payload
}
}
} else {
return state
}
}
export const store = {
state: {
user: { name: 'marshall', age: 22 }
},
setState (newState) {
store.state = newState
store.listeners.map(fn => fn(store.state))
},
listeners: [],
subscribe (fn) {
store.listeners.push(fn)
return () => {
const index = store.listeners.indexOf(fn)
store.listeners.splice(index, 1)
}
}
}
export const appContext = createContext(null)
selector from react-redux
connect 支持 selector
export const connect = (selector) => (Component) => {
return (props) => {
const { state, setState, subscribe } = useContext(appContext)
const [_, update] = useState({})
const data = selector ? selector(state) : { state } // 判断是否传了 selector
useEffect(() => {
subscribe(() => update({}))
}, [])
const dispatch = (action) => {
setState(reducer(data, action))
}
return <Component {...props} {...data} dispatch={dispatch}/>
}
}
// 传 selector
const User = connect(state => { return { user: state.user } })(({ user }) => {
return <div>User: {user.name}</div>
})
// 不传 selector
const UserModifier = connect()(({ dispatch, state }) => {
const onChange = (e) => {
dispatch({ type: 'updateUser', payload: { name: e.target.value } })
}
return <div><input onChange={onChange} value={state.user.name}></input></div>
})
使用 selector 实现精准渲染
我们在更新 FirstChild 和 SecondChild 的 state.user.name 时会发现 ThirdChild 也重新渲染了,这是我们所不愿意见到的。
下面我们将通过
selector 实现,组件只在自己的数据变化时渲染。
const changed = (oldState, newState) => {
let hasChanged = false
for (const key in oldState) {
if (oldState[key] !== newState[key]) {
hasChanged = true
break
}
}
return hasChanged
}
export const connect = (selector) => (Component) => {
return (props) => {
const { state, setState, subscribe } = useContext(appContext)
const [_, update] = useState({}) // 当 setState 时, update 只能刷新当前组件。
const data = selector ? selector(state) : { state }
useEffect(() => {
store.subscribe(() => { // 注意这个 useEffect 会多次执行,要去 store 里取对应属性
const newData = selector ? selector(store.state) : { state: store.state }
if (changed(data, newData)) { // 加一步判断,只有当组件数据变化时 reRender。
console.log('updated')
update({})
}
})
}, [selector])
const dispatch = (action) => {
setState(reducer(state, action))
}
return <Component {...props} {...data} dispatch={dispatch}/>
}
}