1. 简易版的修改方法
此版本仅是使用最简单的方法修改名字后实现同步变化。
import React, { useState, useContext } from 'react'
const appContext = React.createContext(null)
const App = () => {
const [appState, setAppState] = useState({
user: { name: 'frank', age: 18 }
})
const contextValue = { appState, setAppState }
return (
<appContext.Provider value={contextValue}>
<大儿子 />
<二儿子 />
<幺儿子 />
</appContext.Provider>
)
}
const 大儿子 = () => (
<section>
大儿子
<User />
</section>
)
const 二儿子 = () => (
<section>
二儿子
<UserModifier />
</section>
)
const 幺儿子 = () => <section>幺儿子</section>
const User = () => {
const contextValue = useContext(appContext)
return <div>User:{contextValue.appState.user.name}</div>
}
const UserModifier = () => {
const { appState, setAppState } = useContext(appContext)
const onChange = e => {
appState.user.name = e.target.value
setAppState({ ...appState })
}
return (
<div>
<input value={appState.user.name} onChange={onChange} />
</div>
)
}
export default App
2. 优化上述过程
2.1 reducer 雏形
由于上述修改的过程可以单独写个函数,因此有了如下代码:
const reducer = (state, { type, payload }) => {
switch (type) {
case 'updateUser':
return {
...state,
user: {
...state.user,
...payload
}
}
default:
return state
}
}
const UserModify = () => {
...
const onChange = e => {
setAppState(reducer(appState, { type: 'updateUser', payload: { name: e.target.value } }))
}
...
}
2.2 使用 dispatch 优化
上述代码有些问题,如果我们要修改 type 或者 payload 中的某些参数,那么我们需要复制很多次,为解决这个问题,我们可以增加一个函数,每次将需要修改的数据传给此函数就可以实现部分优化;但是方法中并不能使用 hooks,于是我们需要新增一个组件。
...
const 二儿子 = () => (
<section>
二儿子
<Wrapper />
</section>
)
...
const Wrapper = () => {
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 } })
}
return (
<div>
<input value={state.user.name} onChange={onChange} />
</div>
)
}
2.3 封装 connect 函数
...
const 二儿子 = () => (
<section>
二儿子
<UserModifier >context</UserModifier>
</section>
)
...
const connect = Component => {
const Wrapper = props => {
const { appState, setAppState } = useContext(appContext)
const dispatch = action => {
setAppState(reducer(appState, action))
}
return <Component {...props} dispatch={dispatch} state={appState} />
}
return Wrapper
}
const UserModifier = connect(({ dispatch, state, children }) => {
const onChange = e => {
dispatch({ type: 'updateUser', payload: { name: e.target.value } })
}
return (
<div>
{children}
<input value={state.user.name} onChange={onChange} />
</div>
)
})
2.4 实现精准 render
当在打印上述每个组件的日志信息时,发现输入框中的内容每次发生变化,每个组件都会重新渲染,这是因为 setAppState 是写到主组件中的,一旦发生变化,那么磁组件中的所有内容都会重新渲染,为解决这个问题,我们可以采用如下方法:
2.4.1 useMemo
const App = () => {
const [appState, setAppState] = useState({
user: { name: 'frank', age: 18 }
})
const contextValue = { appState, setAppState }
const x = useMemo(() => {
return <幺儿子 />
}, [])
return (
<appContext.Provider value={contextValue}>
<大儿子 />
<二儿子 />
{ x }
</appContext.Provider>
)
}
不过这种方式的弊端在于组价很多的情况下我们每个组件都要使用 useMemo,相对来说会很繁琐。
2.4.2 抽离静态数据保存为 store 数据
const store = {
state: { user: { name: 'frank', age: 18 } },
setState(newState) {
store.state = newState
}
}
const appContext = React.createContext(null)
const App = () => {
return (
<appContext.Provider value={store}>
<大儿子 />
<二儿子 />
<幺儿子 />
</appContext.Provider>
)
}
const connect = Component => {
const Wrapper = props => {
const { state, setState } = useContext(appContext)
// 这里 setState 修改的是 store 中的数据,而页面中的数据不会更新,因此我们需要手动更新以确保页面重 // 新加载
const [, update] = useState({})
const dispatch = action => {
setState(reducer(state, action))
update({})
}
return <Component {...props} dispatch={dispatch} state={state} />
}
return Wrapper
}
const UserModifier = connect(({ dispatch, state, children }) => {
console.log('UserModifyer执行了' + Math.random())
const onChange = e => {
dispatch({ type: 'updateUser', payload: { name: e.target.value } })
}
return (
<div>
{children}
<input value={state.user.name} onChange={onChange} />
</div>
)
})
2.4.3 使用发布订阅者模式
const connect = Component => {
const Wrapper = props => {
const { state, setState, subscribe } = useContext(appContext)
const [, update] = useState({})
// 在首次的时候订阅一次后,之后用的都是相同的更新函数
useEffect(() => {
subscribe(() => {
update({})
})
}, [])
const dispatch = action => {
setState(reducer(state, action))
}
return <Component {...props} dispatch={dispatch} state={state} />
}
return Wrapper
}
const store = {
state: { user: { name: 'frank', age: 18 } },
setState(newState) {
store.state = newState
// 在每次更新的时候通知所有订阅者
store.listeners.forEach(fn => fn(store.state))
},
listeners: [],
subscribe(fn) {
store.listeners.push(fn)
// 在订阅完成之后希望可以删除此调用
return () => {
const index = store.listeners.indexOf(fn)
store.listeners.splice(index, 1)
}
}
}
2.5 抽离为 redux 文件
import { createContext, useContext, useEffect, useState } from 'react'
export const store = {
state: { user: { name: 'frank', age: 18 } },
setState(newState) {
store.state = newState
// 在每次更新的时候通知所有订阅者
store.listeners.forEach(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 }) => {
switch (type) {
case 'updateUser':
return {
...state,
user: {
...state.user,
...payload
}
}
default:
return state
}
}
export const connect = Component => {
const Wrapper = props => {
const { state, setState, subscribe } = useContext(appContext)
const [, update] = useState({})
useEffect(() => {
console.log('connect执行了' + Math.random())
subscribe(() => {
update({})
})
}, [])
const dispatch = action => {
setState(reducer(state, action))
}
return <Component {...props} dispatch={dispatch} state={state} />
}
return Wrapper
}
export const appContext = createContext(null)
2.6 实现 react-redux 中的 selector
export const connect = selector => Component => {
const Wrapper = props => {
const { state, setState, subscribe } = useContext(appContext)
const [, update] = useState({})
const data = selector ? selector(state) : { state }
useEffect(() => {
subscribe(() => {
update({})
})
}, [])
const dispatch = action => {
setState(reducer(state, action))
}
return <Component {...props} {...data} dispatch={dispatch} />
}
const User = connect(state => {
return {user: state.user}
})(({ user }) => {
// 如果后端返回的数据形式为 state.xxx.yyy.zzz.user.name
return <div>User:{user.name}</div>
})
const UserModifier = connect()(({ dispatch, state, children }) => {
...
}
2.7 实现不同数据的精准渲染
const changed = (oldState, newState) => {
let changed = false
for (const key in oldState) {
if (oldState[key] !== newState[key]) {
changed = true
break
}
}
return changed
}
export const connect = selector => Component => {
const Wrapper = props => {
const { state, setState, subscribe } = useContext(appContext)
const [, update] = useState({})
const data = selector ? selector(state) : { state }
useEffect(
() =>
subscribe(() => {
const newData = selector ? selector(store.state) : { state: store.state }
if (changed(data, newData)) {
console.log('update')
update({})
}
}),
// 这里最好 取消订阅,否则在 selector 变化时会重复订阅
[selector]
)
const dispatch = action => {
setState(reducer(state, action))
}
return <Component {...props} {...data} dispatch={dispatch} />
}
return Wrapper
}
2.8 实现 mapDispatchToProps
connect 的第一个函数中的参数可以包含两个参数,第一个参数为简化语法,第二个参数是一个函数,传入一个 dispatch,返回的是一个方法。
const UserModifier = connect(null, dispatch => {
return {
updateUser: attrs => dispatch({ type: 'updateUser', payload: attrs })
}
})(({ updateUser, state, children }) => {
...
})
// redux.jsx
export const connect = (selector, dispatchSelector) => Component => {
const Wrapper = (props) => {
...
const dispatchers = dispatchSelector ? dispatchSelector(dispatch) : { dispatch }
...
return <Component {...props} {...data} {...dispatchers} />
}
return Wrapper
}
2.9 抽离 connect 的第一个函数调用
connect 的使用方式:
connect(MapStateToProps, MapDispatchToProps)(Component)
connect 的意义就在于能够将第一个函数调用抽离出去,形成一个半成品,后续使用的时候可以直接调用。
import { connect } from "../redux"
const userSelector = state => {
return { user: state.user }
}
const userDispatcher = dispatch => {
return {
updateUser: attrs => dispatch({ type: 'updateUser', payload: attrs })
}
}
export const connectToUser = connect(userSelector, userDispatcher)
const User = connectToUser(({ user }) => {
console.log('User执行了' + Math.random())
// 如果后端返回的数据形式为 state.xxx.yyy.zzz.user.name
return <div>User:{user.name}</div>
})
const UserModifier = connectToUser(({ updateUser, user, children }) => {
console.log('UserModifier执行了' + Math.random())
const onChange = e => {
updateUser({ name: e.target.value })
}
return (
<div>
{children}
<input value={user.name} onChange={onChange} />
</div>
)
})
2.10 封装 Provider 和 createStore
// redux.jsx
const store = {
state: null,
reducer: null,
...
}
export const createStore = (reducer, initialState) => {
store.state = initialState
store.reducer = reducer
return store
}
...
export const Provider = ({ children, store }) => {
return <appContext.Provider value={store}>{children}</appContext.Provider>
}
// App.jsx
const initialState = {
user: { name: 'frank', age: 18 },
group: { name: '前端组' }
}
const reducer = (state, { type, payload }) => {
switch (type) {
case 'updateUser':
return {
...state,
user: {
...state.user,
...payload
}
}
default:
return state
}
}
const store = createStore(reducer, initialState)
...
const App = () => {
return (
<Provider store={store}>
<大儿子 />
<二儿子 />
<幺儿子 />
</Provider>
)
}
...
2.11 重构 redux 文件
import { createContext, useContext, useEffect, useState } from 'react'
const innerStore = {
state: undefined,
reducer: undefined,
listeners: [],
setState(newState) {
innerStore.state = newState
// 在每次更新的时候通知所有订阅者
innerStore.listeners.forEach(fn => fn(innerStore.state))
}
}
const store = {
getState() {
return innerStore.state
},
dispatch(action) {
innerStore.setState(innerStore.reducer(innerStore.state, action))
},
subscribe(fn) {
innerStore.listeners.push(fn)
// 在订阅完成之后希望可以删除此调用
return () => {
const index = innerStore.listeners.indexOf(fn)
innerStore.listeners.splice(index, 1)
}
}
}
const dispatch = store.dispatch
export const createStore = (reducer, initialState) => {
innerStore.state = initialState
innerStore.reducer = reducer
return store
}
const changed = (oldState, newState) => {
let changed = false
for (const key in oldState) {
if (oldState[key] !== newState[key]) {
changed = true
break
}
}
return changed
}
export const connect = (selector, dispatchSelector) => Component => {
const Wrapper = props => {
const { subscribe } = useContext(appContext)
const [, update] = useState({})
const data = selector ? selector(innerStore.state) : { state: innerStore.state }
const dispatchers = dispatchSelector ? dispatchSelector(dispatch) : { dispatch }
useEffect(
() =>
subscribe(() => {
const newData = selector ? selector(innerStore.state) : { state: innerStore.state }
if (changed(data, newData)) {
update({})
}
}),
// 这里最好 取消订阅,否则在 selector 变化时会重复订阅
[selector]
)
return <Component {...props} {...data} {...dispatchers} />
}
return Wrapper
}
const appContext = createContext(null)
// eslint-disable-next-line react/prop-types
export const Provider = ({ children, store }) => {
return <appContext.Provider value={store}>{children}</appContext.Provider>
}
2.12 实现异步 action
上面所写的代码只能支持形如 fetchUser(dispatch) 的函数方式,但是如果想要写成 dispatch(fetchUser),需要对自己封装的 dispatch 改写。
// redux.jsx
let dispatch = store.dispatch
const prevDispatch = dispatch
dispatch = action => {
if (typeof action === 'function') {
action(dispatch)
} else {
prevDispatch(action)
}
}
2.13 实现 promise 形式的异步
const prevDispatch2 = dispatch
dispatch = action => {
if ( action.payload instanceof Promise) {
action.payload.then(data => {
dispatch({...action, payload: data})
})
} else {
prevDispatch2(action)
}
}
上述代码具体地址:github.com/fogjoe/my-r…