个人愚见: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和setStateconst 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:这个好理解,就是初始的stateaction:变动的描述。因为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,否则就进入下一个函数
谢谢阅读 :)