个人愚见:Redux
的源码特别难看懂,也可能是 Redux
的源码是以实用为目的,看起来没什么头绪;
所以我一步一步实现一个 Redux
其中也有一些坑,最终效果和 Redux
的接口几乎是一致的,跟着以下思路,或许可以更容易理解 Redux
为什么要这么实现,包括每个概念
一、尝鲜使用 Vite4 初始化项目
今天是 Vite4 发布的第二天,我使用上了!
2022-12-13
我已经准备了一个简单的 demo,它们的结构特别简单
- App 中有 3 个子元素,分别是:
Parent
、Son
、Grandson
- Parent 负责
展示 User 数据
,Son 负责修改 User 数据
import * as React from 'react'
const { useState, useContext } = React
const appContext = React.createContext(null)
export const App = () => {
const [appState, setAppState] = useState({
user: { name: 'heycn', age: 22 }
})
const contextValue = { appState, setAppState }
return (
<appContext.Provider value={contextValue}>
<Parent />
<Son />
<Grandson />
</appContext.Provider>
)
}
// Parent 用于展示 User 数据
const Parent = () => <section>Parent<User /></section>
// Son 用于修改 User 数据
const Son = () => <section>Son<UserModifier /></section>
const Grandson = () => <section>Grandson Component</section>
const User = () => {
const contextValue = useContext(appContext)
return <div>UserName:{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>
)
}
二、使用 useContext
来读写数据
1. 数据从哪里来?
代码在上面 demo
- 使用 useState 声明了一个对象,里面包含 user
- 把 appState 和 setAppstate 封装成一个对象:contextValue
- 把 contextValue 放到 appContext.Provider 里
- appContext 是使用 React.createContext 创建的
2. 如何让 User 获取到 user 数据?
- 只需要两句话:
- 使用 useContext:
const contextValue = useContext(appContext)
- 获取
contextValue.appState.user.name
3. 如何修改数据?
- 那么我们就需要调用
setAppState
- 看
UserModifier
里的onChange
方法 - 注意:这个代码非常的不规范,后面会纠正它们!
三、reducer
的由来
reducer
就是用来规范 state 创建流程的一个函数
之前的代码,创建 state
的时候特别的不规范,它直接去修改了原始的 state
那么如何解决呢?
提供一个函数来帮他去创建新的 state
1. reducer
雏形 —— createNewState()
目的:规范了创建流程
// 接受3个参数:state(旧的state), actionType(操作),actionData(新的state)
const createNewState = (state, actionType, actionData) => {
if (actionType === 'update') {
return {
// 首先拷贝 user 之外的属性,然后创建一个 user
...state,
user: {
...state.user, // 把之前user的其他属性拷贝过来
...actionData // 把给我额外对user的修改放在这里
}
}
} else {
return state
}
}
2. 如何使用?
const UserModifiler = () => {
const { appState, setAppState } = useContext(appContext)
const onChange = e => {
- appState.user.name = e.target.value
- setAppState({ ...appState })
+ setAppState(createNewState(appState, 'updateUser', {name: e.target.value}))
}
3. reducer 接收两个参数!
那也简单
把 actionType
和 actionData
统一成一个叫 action
的东西,接受一个 type
和一个 payload
payload
其实就是 data
的意思
-const createNewState = (state, actionType, actionData) => {
+const createNewState = (state, {type, payload}) => {
- if (actionType === 'update') {
+ if (type === 'update') {
return {
...state,
user: {
...state.user,
- ...actionData
+ ...payload
}
}
} else {
return state
}
}
3. reducer
使用
const UserModifiler = () => {
const { appState, setAppState } = useContext(appContext)
const onChange = e => {
- setAppState(createNewState(appState, 'updateUser', {name: e.target.value}))
+ setAppState(createNewState(appState, {type: 'updateUser', payload: {name: e.target.value}}))
}
这样的话,reducer
就写出来了,是不是特别的简单呢?
这一点就只需记住一句话:reducer
是用来规范 state
创建流程的一个函数
四、dispatch
如何使用
dispatch
来规范setState
的流程
1. 太多重复的代码
首先来看一下我们之前是如何 setState
的:
setAppState(reducer(appState, {type: 'updateUser', payload: {name: e.target.value}}))
那如果我们要改 user
的 age
和 height
该怎么改?
setAppState(reducer(appState, {type: 'updateAge', payload: {age: e.target.value}}))
setAppState(reducer(appState, {type: 'updateAge', payload: {age: e.target.value}}))
每次都要重复代码,那么下面将对其进行优化
2. 去除重复的代码
dispatch 的由来
第一步:实现 dispatch
我们写一个 dispatch
const dispatch = action => {
setAppState(reducer(appState, action))
}
使用:
const UserModifier = () => {
const {appState, setAppState} = useContext(appContext)
const onChange = e => {
- setAppState(reducer(appState, {type: 'updateUser', payload: {name: e.target.value}}))
+ dispatch({type: 'updateUser', payload: {name: e.target.value}})
}
}
你觉得这样子就可以了吗?
并不能,因为 UserModifier
里的 dispatch
是没有办法访问到 setAppState
和 appState
的
React 规定:只能在组件内使用 hooks
至于出现这种情况,是由于我们把 state
放在了 context
里,如果 state
不在 context
里,那就好办了,但是那种改动太大了
那我们想想,就以现在的办法如何实现:让 dispatch
访问到 state
和 setState
第二步:实现让 dispatch
访问到 state
和 setState
思路:我们用一个组件来包住 dispatch
,然后把 dispatch
再给需要使用的组件
// 使用 Wrapper 来包住 UserModifier
// 注意之前使用到 <UserModifier /> 要改为 <Wrapper />
const Wrapper = () => {
return <UserModifier />
}
const Wrapper = () => {
const { appState, setAppState } = useContext(appContext) // 使 dispatch 可以使用上下文
// 把 dispatch 放到 Wrapper 里
const dispatch = action => {
setAppState(reducer(appState, action))
}
// 把 dispatch 和 state 传给 UserModifier
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>
)
}
想要读数据,就从 props
里面读 state
,想要写数据就从 props
里使用 dispatch
目前,我们就完成了 UserModifier
一个组件的封装,它可以通过 props
来读写全局数据
所有人,直接调 dispatch
,不要用去多写那三个单词了
实际上这个功能不是由 redux
实现的,是由 react-redux
实现的,但是大家用的时候都是一起用的,这里就不做区分了
五、高阶组件 connect
让组件与全局状态连接起来
原理:函数里接收一个组件,返回一个新的组件
在上面代码,我们是把 UserModifier
包装成了 Wrapper
,用的时候我们是一定要使用 Wrapper
,因为如果直接使用 UserModifier
是得不到 dispatch
和 state
,因此,我们任何一个组件想要读取全局 state
,都需要封装成一个 Wrapper
,那如果有 100 个组件,难道都要重新写 100 遍吗?当然不是这样子的
所以我们需要声明一个函数来实现,用来自动创建之前的 Wrapper
// 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, children }) => {
const onChange = e => {
dispatch({
type: 'updateUser',
payload: { name: e.target.value }
})
}
return (
<div>
{children}
<input value={state.user.name} onChange={onChange} />
</div>
)
})
如果你看 redux
提供的 connect
,你会发现它接收的参数比我上面的组件还多,后面我们接着实现!
六、避免多余的 render
我们在之前的代码里每个组件都加上 log
const Brother = () => {
console.log('Brother render!')
return (
<section>
Brother
<User />
</section>
)
}
const Sister = () => {
console.log('Sister render!')
return (
<section>
Sister
<UserModifier />
</section>
)
}
const Cousin = () => {
console.log('Cousin render!')
return <section>Cousin</section>
}
const User = () => {
const contextValue = useContext(appContext)
console.log('User render!')
return <div>UserName:{contextValue.appState.user.name}</div>
}
const UserModifier = connect(({ dispatch, state, children }) => {
const onChange = e => {
dispatch({
type: 'updateUser',
payload: { name: e.target.value }
})
}
console.log('UserModifier render!')
return (
<div>
{children}
<input value={state.user.name} onChange={onChange} />
</div>
)
})
我们会发现:我们只改一个组件,而上面5个组件都会重新render,这样子我们只要改动 state
中的一小点,就会导致整个应用的重新执行
我们希望用到的时候,才 render
我们来看下问题是如何产生的:
- 当我们改变
input
的值的时候,它会调用setAppState
- 是通过
dispatch
调用到的 dispatch
是由context
来的- 而
context
最初是从AppContext
拿到的 - 根据 React 规定:只要调用到这个组件的
setState
,并且给setState
,传的是一个新对象,那么这个组件就一定会重新渲染
首先我们想到的是使用 useMemo
,这样能避免组件重新执行,但是,这么写太麻烦了,那么 redux
会设计一种机制:只有用到 state
里某个属性的地方,在这个属性变化的时候,再重新执行
实现思路及过程:
- 首先我们把
setState
移除掉,因为它必然会导致组件执行 - 我们创建一个对象
store
,里面有state
和setState
const store = { state: { // 用于存放数据 user: { name: 'heycn', age: 22 } }, setState(newState) { // 用于修改数据 store.state = newState } }
- 把之前用到
useState
的地方都改为store
的state
和setState
- 目前展示没问题,但是修改无法显示,其实数据已经在
store
改变了,只是我们没有调用react
的useState
,那么我们让他强制刷新 - 在
connect
里使用const [, update] = useState({})
,它的值为一个空对象,然后再dispatch
里调用update({})
,这样的话,被connect
的组件就会强制刷新 - 但是这样的话,其他使用到的
state
的组件,就无法更新,所以我们需要去订阅一下变化 - 在
store
里创建subscribe
函数,我们可以让每个组件订阅state
的变化const store = { state: { user: { name: 'heycn', age: 22 } }, setState(newState) { store.state = newState // 每次 setState 就告诉订阅者 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) } } }
- 在
connect
中使用useEffect
,只在组件第一次渲染时订阅,调用useState
的update()
以下是完整代码
import * as React from 'react'
const { useState, useEffect, useContext } = React
const appContext = React.createContext(null)
const connect = Component => {
return props => {
const { state, setState } = useContext(appContext)
const [_, forceUpdate] = useState({})
useEffect(() => {
store.subscribe(() => {
forceUpdate({})
})
}, [])
const dispatch = action => {
setState(reducer(state, action))
}
return <Component {...props} dispatch={dispatch} state={state} />
}
}
const store = {
state: {
user: { name: 'heycn', 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 App = () => {
return (
<appContext.Provider value={store}>
<Brother />
<Sister />
<Cousin />
</appContext.Provider>
)
}
// Brother 用于展示 User 数据
const Brother = () => {
console.log('Brother render!')
return (
<section>
Brother
<User />
</section>
)
}
// Sister 用于修改 User 数据
const Sister = () => {
console.log('Sister render!')
return (
<section>
Sister
<UserModifier />
</section>
)
}
const Cousin = () => {
console.log('Cousin render!')
return <section>Cousin</section>
}
const User = connect(({ state, dispatch }) => {
console.log('User render!')
return <div>UserName:{state.user.name}</div>
})
const reducer = (state, { type, payload }) => {
if (type === 'updateUser') {
return {
...state,
user: {
...state.user,
...payload
}
}
} else {
return state
}
}
const UserModifier = connect(({ dispatch, state }) => {
const onChange = e => {
dispatch({
type: 'updateUser',
payload: { name: e.target.value }
})
}
console.log('UserModifier render!')
return (
<div>
<input value={state.user.name} onChange={onChange} />
</div>
)
})
七、Redux 雏形
将代码抽离,把 redux
有关的代码放在同一个文件
八、让 connect 支持 selector
react-redux
提供的selector
这是一个选择函数,比如:
const User = connect(state => {
return {user: state.user}
})(({user}) => {
return <div>Use: {user.name}</div>
})
以下是 api 的实现步骤:
- 来到
redux.js
里 的connect
- 我们给他添加一个参数,表示先接受一个参数,再接受第二个参数
- const connect = Component => { + const connect = selector => Component => { return props => { const { state, setState } = useContext(appContext) const [_, forceUpdate] = useState({}) + const data = selector ? selector(state) : {state} useEffect(() => { store.subscribe(() => { forceUpdate({}) }) }, []) const dispatch = action => { setState(reducer(state, action)) } - return <Component {...props} dispatch={dispatch} state={state} /> + return <Component {...props} {...data} dispatch={dispatch} /> } }
我们通过一些简单的代码就实现了 selector
,他还有其他非常重要的作用,请看下面!
九、实现精准渲染
使用
selector
来实现精准渲染
组件只在自己的数据变化时render
问题:
-
我们在
store
里添加:state: { user: { name: 'heycn', age: 22 }, + educational: { school: 'Tsinghua University' } }
-
然后在
Cousin
读取新添加的数据-const Cousin = () => { +const Cousin = connect(state => { + return { educational: state.educational } +})(() => { return ( <section> <h1>Cousin</h1> + <div>educational: {educational.school}</div> </section> ) })
-
然后让我们修改
user
时,Cousin
也会重新渲染,而Cousin
里只使用到educational
如何解决
-
那我可以在
Cousin
做一个检查:如果Cousin
没有更新,我们就不去重新渲染Cousin
-
但是这是存在一个逻辑悖论的,因为:如果
Cousin
要去做检查,那么这个时候Cousin
就已经执行了,我们可以在connect
里做手脚 -
在
connect
里我们返回一个组件,我们叫它为Wrapper
,这个Wrapper
的作用很大,我们可以在Wrapper
里面做检查: -
如果被选择的
selectedState
没有改变,我们就不去做渲染+const changed = (oldState, newState) => { + let changed = false + for (let key in oldState) { + if (oldState[key] !== newState[key]) { + changed = true + break + } + } + return changed +} export const connect = selector => Component => { return props => { const { state, setState } = useContext(appContext) const [_, forceUpdate] = useState({}) const selectedState = selector ? selector(state) : { state } useEffect(() => { store.subscribe(() => { + const selectedState = selector ? selector(state) : { state } + if (changed(selectedState, newSelectedState)) { forceUpdate({}) + } }) - }, []) + }, [selector]) const dispatch = action => { setState(reducer(state, action)) } return <Component {...props} {...selectedState} dispatch={dispatch} /> } }
-
但是,我们需要取消订阅,不然可能在意想不到的时候不停地订阅,所以需要进行取消订阅,由于订阅里面返回了取消订阅,所以只需要这么做:
export const connect = selector => Component => { return props => { const { state, setState } = useContext(appContext) const [_, forceUpdate] = useState({}) const selectedState = selector ? selector(state) : { state } useEffect(() => { + return { store.subscribe(() => { const selectedState = selector ? selector(state) : { state } if (changed(selectedState, newSelectedState)) { forceUpdate({}) } }) + } }, [selector]) const dispatch = action => { setState(reducer(state, action)) } return <Component {...props} {...selectedState} dispatch={dispatch} /> } }
这样就实现精准渲染:组件只在自己的数据变化时 render
!
十、connect
的第二个参数:mapDispatchToProps
api 设计
我们期望 api 是这么使用的:
const UserModifier = connect(null, dispatch => {
return {
updateUser: attrs => dispatch({ type: 'updateUser', payload: attrs })
}
})(({ updateUser, state }) => {
const onChange = e => {
updateUser({ name: e.target.value })
}
console.log('UserModifier render!')
return (
<div>
<input value={state.user.name} onChange={onChange} />
</div>
)
})
代码实现
export const connect = selector => Component => {
return props => {
const { state, setState } = useContext(appContext)
const [_, forceUpdate] = useState({})
+ const dispatch = action => {
+ setState(reducer(state, action))
+ }
const selectedState = selector ? selector(state) : { state }
+ const selectedDispatches = mapDispatchToProps ? mapDispatchToProps(dispatch) : { dispatch }
useEffect(() => (
store.subscribe(() => {
const newSelectedState = selector ? selector(store.state) : { state: store.state }
if (changed(selectedState, newSelectedState)) {
forceUpdate({})
}
})
), [selector])
- const dispatch = action => {
- setState(reducer(state, action))
- }
- return <Component {...props} {...selectedState} dispatch={dispatch} />
+ return <Component {...props} {...selectedState} {...selectedDispatches} />
}
}
十一、connect 的意义
我们会发现 connect
函数的调用形式很奇怪,我们来看看究竟是在考虑什么!
看这里代码的 diff 就明白了:代码链接
mapStateToProps
是用来封装写,mapDispatchToProps
是用来封装读,所以 connect
是用来封装 读
和 写
,也就是封装一个资源,你可以对这个资源进行读写,然后只要再传一个组件就行了,之所以要分成两次调用,就是为了方便:你先调用一次得到一个 “半成品”,这个 “半成品” 可以跟任何组件相结合,它会把 读
、写
接口传给任何组件,然后等你想用一个组件的时候,就可以调不同的组件
这就是 connect
的意义
十二、封装 Provider 和 createStore
createStore
它接受两个参数,一个 reducer
一个 initState
看代码 diff
即可知道如何封装:代码链接
封装 Provider
redux 官方的使用方式是这样子的:<Provider store={store}></Provider>
,那我们只需要分装成一个组件即可:
export const Provider = ({ store, children }) => {
return (
<appContext.Provider value={store}>
{children}
</appContext.Provider>
)
}
目前为止,目前我们的封装的 redux
和 官方的 redux
的接口,几乎是一致的,可能会有一些细微的区别,通过手写 redux
,我们基本可以理解 redux
的实现,让我们总结一下
十三、Redux 概念总结(精髓)
让我们来了解,redux
和 react-redux
的主要思路
请配合代码阅读
主要思路
- 首先我们有一个
App
组件,里面包含很多组件,我们需要让每一个组件都可以访问到一个全局的state
state
是我们的第一个概念:state
放在哪里呢?redux
是把他放在store
里的- 让组件和
store
的state
连接起来:react-redux
提供的一个 api ——connect
,用于连接组件和state
store
的state
连接之后做什么:连接之后就是读
和写
读
操作:从组件的属性里面取state
,如果想读得更精确,可以传一个mapStateToProps
写
操作:从组件的属性里面取dispatch
,如果想写得更精确,可以传一个mapDispatchToProps
,可以用来封装api
,你可以对这个api
的资源进行读写,然后只要再传一个组件就行了
回顾 connect
作用
connect
实际上是对组件进行一封装,我称这个组件为Wrapper
,然后把Wrapper
返回出去,主要做了3件事情- 第一件事
获取读写接口
:从上下文拿到state
和setState
(是store
的),也就是拿到读
和写
接口,实际上不用上下文也行,直接从store
也行 - 第二件事
封装读写接口
:进行封装,比如根据mapStateToProps
得到具体的数据和具体的mapDispatchToProps
- 第三件事
订阅store更新
:在恰当的时候进行更新,对store
进行订阅,只要store
变化,就会在数据变更的情况下调用forceUpdate
进行强制更新组件,这里的forceUpdate
是我做的一个小技巧 - 最后就返回
Wrapper
这个组件
简单来讲,就是:
- 获取读写接口
- 封装读写接口
- 订阅
store
更新,如果store
更新了,就更新组件 - 返回这个组件
总结
现在我们已经知道 store
、state
、dispatch
、connect
、Provider
的概念了
dispatch
这里又可以分出几个概念,比如:
reducer
:这个很难具体的解释,但我把他认为是规范创建state
的过程,因为每次更新state
我们不能改原来的state
,要创建新的state
,所以他是创建state
的过程initState
:这个好理解,就是初始的state
action
:变动的描述。因为reducer
是接受一个state
,一个action
然后返回一个新的state
;所以是这一次变动的描述;比如action
的类型、payload
可以是store
的具体的各种信息
到这里,redux 的大部分概念我们都彻底的了解了
十四、API 封装技巧
看代码 diff
十五、让 Redux 支持函数 Action
目前我们的 redux 是不支持异步 Action
的,我们来看下如果要做异步的 Action
的话,我们应该怎么做,只做了以下几步
let { dispatch } = store
const preDispatch = dispatch
dispatch = action => {
action instanceof Function
? action(dispatch)
: preDispatch(action)
}
十六、让 Redux 支持 PromiseAction
api 设计
dispatch({ type: 'updateUser', payload: ajax('/user').then(response => response.data) })
代码实现
const asyncDispatch = dispatch
dispatch = action => {
action.payload instanceof Promise
? action.payload.then(data => {
dispatch({ ...action, payload: data })
})
: asyncDispatch(action)
}
十七、中间件 redux-thunk
和 redux-promise
原理
一个 Middleware
就是一个函数,这个函数可以去修改 dispatch
这两个中间件可以让 redux
支持异步 action
redux-thunk
如果 action
是一个函数,就调用它,否则就进入下一个函数
redux-promise
如果 payload
是一个函数,就在 Promise
后面接上一个 then
和 catch
,否则就进入下一个函数
谢谢阅读 :)