redux 源码分析二:从零开始搭建 redux 源码
# 0.前言
本篇博客为 redux项目仓库 (opens new window) 笔记。
# 1.从零开始搭建 redux
源码
# 1.1.初始化项目
// 创建全局的状态
const appContext = React.createContext(null)
// 使用Provider函数包括组件结构
const App = () => {
const [appState, setAppState] = useState({
user: { name: "王家盛", age: 18 }
})
const contextValue = { appState, setAppState }
return (
<appContext.Provider value={contextValue}>
<Child1></Child1> // 兄弟组件1 - User信息显示
<Child2></Child2> // 兄弟组件2 - UserModifier
<Child3></Child3> // 兄弟组件3
</appContext.Provider>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
需要达到的目标:
- 使用
<UserModifier/>
修改<User/>
组件- 避免重复渲染,修改
user
相关的属性时,只有<Child1>
和<Child2>
发生了渲染,<Child3>
不发生渲染。
# 1.2.使用 UserModifier
修改 user
信息,并且 User
组件也跟着变化
使用 Hooks
提供默认的状态修改函数 setAppState
对 AppState
的状态进行更新。
const appContext = React.createContext(null)
const { appState, setAppState } = useContext(appContext)
const onChange = (e) => {
appState.user.name = e.target.value
setAppState({...appState})
}
2
3
4
5
6
在上一次的技术分享中已经阐述了,React
虽然并没有强制要求开发者在开发过程中一定要保持数据的不可变性,但是涉及到 shouldComponentUpdate
这种生命周期函数时,或者 React.memo
等时由于监听不到props
的变化,而无法区分 new
值和 old
值。
于是 redux
对这数据的可变性方面进行约束,要求每次必须使用 newState
进行更新(immutable
特性)。
# 1.3. createNewState
函数:即 reducer
函数
ps.函数的命名方面的说明: 在案例中,偏好使用
createNewState
函数去替代redux
中提供的reducer
函数名。因为,reducer
本质就是规范化状态更新的作用,createNewState
这种命名可更直接体现这一特性。
const { appState, setAppState } = useContext(appContext)
const onChange = (e) => {
// 通过newState创建出来的变量,符合 React 对状态Immutable的要求
const newState = createNewState(appState, "updateUser", { name: e.target.value })
setAppState(newState)
}
2
3
4
5
6
createNewState
:用于生成一个 NewState
const createNewState = (state, actionType, payload) => {
if (actionType === "updateUser") {
return {
...state,
user: {
...state.user,
...payload
}
}
} else {
return state
}
}
2
3
4
5
6
7
8
9
10
11
12
13
在 redux
中修改一个状态需要接受三个参数:
state
:待修改的仓库state
actionType
:执行的action
,目的是对state
中的哪儿个属性执行什么样的操作。payload
:负载,可传可不传。
# 1.4.将 state
与组件
分离
由上可知,创造一个 NewState
需要接受三个值:state
| actionType
|payload
实际上,对于用开发者而言,不希望在使用的时候频繁输入: 全局的 state
状态仓库。
于是,通过封装了 dispatch
将输入参数进一步缩小为只需要输入: actionType
| payload
于是做了两件事:
- 使用
Wrapper
函数将appState
从<UserModifier>
中抽离出来。 - 使用
dispatch
函数,减少用户重复输入参数state
。
const Wrapper = () => {
// 通过 HOC :只做一件事,就是将这个组件与全局状态库链接起来。
const { appState, setAppState } = useContext(appContext)
const dispatch = (actionType, payload) => {
const newState = createNewState(appState, actionType, payload)
setAppState(newState)
}
return <UserModifier dispatch={dispatch} state={appState} />
}
2
3
4
5
6
7
8
9
Wrapper
本质上就是一个 HOC
组件,作用是将 <UserModifier>
与全局的 state
链接起来。
而 <UserModifier>
则直接通过 props
接受并使用从 HOC
传递过来的 state
和 dispatch
。
# 1.5.使用 createWrapper
函数生成 HOC
组件
上面代码存在一个问题,如果每一个需要与全局 state
链接的组件需要重复书写上面这段代码,太过繁琐。
于是可以通过构造 createWrapper
函数,封装 Wrapper
代码的生成过程,代码如下:
这部分也可以通过装饰器的方式实现
// 使用 createWrapper 批量化生成 HOC 组件 ,即connect
const createWrapper = (Component) => {
return (props) => {
const { appState, setAppState } = useContext(appContext)
const dispatch = (actionType, payload) => {
const newState = createNewState(appState, actionType, payload)
setAppState(newState)
}
return <Component dispatch={dispatch} state={appState} />
// return <Component {...props} dispatch={dispatch} state={appState} />
}
}
// 使用时,直接传入需要链接的组件 <UserModifier />
const Wrapper2 = createWrapper(UserModifier)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
进一步,封装的 Wrapper
还需将自身的 props
传递给 <UserModifier>
return <Component {...props} dispatch={dispatch} state={appState} />
# 1.6.store
:解决重复渲染问题
上述过程中,我们是使用全局状态提供的方法对状态进行修改的:
const { appState, setAppState } = useContext(appContext)
但是在 React-Hooks
中渲染的原则是:一旦调用了setXXXX
方法,对应的 Appstate
就会进行渲染,导致所有依赖此 state
(被包裹在useContext.Provider
下)的组件都会被重复渲染。
解决方案:创建一个store
对象,自己来维护全局state
以及修改这个 state
的方法,以此来替代 的 react-Hooks
提供的setAppState
函数。
const store = {
appState: {
user: { name: "王家盛", age: 18 }
},
setAppState(newState) {
console.log('newState', newState);
store.appState = newState
}
}
2
3
4
5
6
7
8
9
仅是上面这段代码是无法对视图进行更新的,需要手动触发更新:
ps.这一点和
React
相同,需要通过setState
方法对view
进行更新,无法通过this.props.state.xxx=xxx
。
// 显式地调用 setXXXX 方法,达到主动触发 视图刷新 的作用
const [, update] = useState({})
const dispatch = (actionType, payload) => {
setAppState(createNewState(appState, actionType, payload))
// 在 dispatch 后刷新
update({})
}
2
3
4
5
6
7
# 1.7.全局状态订阅
通过 [,update] = useState()
刷新视图的方法存在一个问题:
createWrapper(connect)
会单独生成一个 dispatch
函数, 于是每一个connect
的组件,只会刷新自己的状态,而无法把state
的变化 映射到 所有依赖这个state
的组件中。
解决方法: 使用 eventhub
,订阅 state
的变化。一旦某个state
变化,就将全局订阅 state
的组件依次进行渲染。
const store = {
appState: {
user: { name: "王家盛", age: 18 }
},
setAppState(newState) {
console.log('newState', newState);
store.appState = newState
store.listeners.map(fn => fn(store.appState))
},
listeners: [], // 简易版的 EventHub
subscribe(fn) {
store.listeners.push(fn)
return () => {
const index = store.listeners.indexOf(fn)
store.listeners.splice(index, 1)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
改造后,可在 HOC
将自己组件的 update()
加入订阅:(只要 store
变化,即刷新页面)
const createWrapper = (Component) => {
return (props) => {
const { appState, setAppState } = useContext(appContext)
// 显式地调用 setXXXX 方法,达到精准的控制 视图刷新 的功能
const [, update] = useState({})
useEffect(() => {
store.subscribe(() => {
update({})
})
}, [])
const dispatch = (actionType, payload) => {
setAppState(createNewState(appState, actionType, payload))
}
return <Component {...props} dispatch={dispatch} state={appState} />
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1.8.封装 redux
组件
接下来稍微梳理下需要被抽离的函数:
createWrapper
:即,connect
函数部分功能createNewState
:即,reducer
函数(规范化state
的创建流程)- 提供
store
对象,其中封装了以下内容:appState
:全局的状态setAppState
:自己实现修改state
的方法,目的是代替React-Hooks
提供的setXXX
函数。EventHub
:简易版的事件发布订阅函数。
# 1.9. mapStateToProps
封装
使用方法说明:
改造 connect
函数,使其能够接收两个函数(selector,dispatchSelector)
,改造后可以对 state
和 dispatch
方法进行过滤或者拦截。即,将 state
和 dispatch
进行一层封装,不再直接提供状态和方法,而是封装后弹出。
在这小节主要实现的是mapStateToProps
,也就是connect
函数接收的第一个参数,本质上实现的是 state
的selector
。
使用时可以直接获取深层数据,如:state.xxx.yyy.zzz
store = {
state:{
xxx:{
yyy:{
zzz:"123"
}
}
}
}
2
3
4
5
6
7
8
9
mapStateToProps
函数中可以定义映射规则:
const mapStateToProps = (state) =>{
return { zzz: state.xxx.yyy.zzzz }
}
2
3
映射后,将属性作为 props
传递到被包裹的 Component
中:
connect(mapStateToProps,null)(({zzz})=>{
.....
})
2
3
实现原理:
修改前:
return <Component {...props} state={appState} dispatch={dispatch}>
修改后: appState
会先经过一层 selector
后传递到 Component
的 props
const data = selector ? selector(appState) : { appState: appState }
return <Component {...props} {...data} {...dispatch} />
2
# 1.10.精准渲染
在这一层对属性进行过滤比较,并解决非依赖属性的重复渲染问题。
本质上:就是做了一层 props.state diff
的操作。
// 使用 connect 批量化生成 HOC 组件 ,即connect
export const connect = (selector) => (Component) => {
return (props) => {
const { appState, setAppState } = useContext(appContext)
const [, update] = useState({})
+ const data = selector ? selector(appState) : { appState: appState }
useEffect(() => {
store.subscribe(() => {
+ const newData = selector ? selector(store.appState) : { appState: store.appState }
+ if (changed(data, newData)) {
// 这里可以对state进行精准控制 diff
update({})
}
})
// 可以尝试下将 [selector] 设置成 [selector,appState] ,观察“视图真实update”的执行数量:2——>4——>6
+ }, [selector])
const dispatch = (actionType, payload) => {
setAppState(reducer(appState, actionType, payload))
}
return <Component {...props} dispatch={dispatch} {...data} />
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1.11. mapDispatchToProps
实现
+ export const connect = (selector, dispatchSelector) => (Component) => {
return (props) => {
const dispatch = (actionType, payload) => {
setAppState(reducer(appState, actionType, payload))
}
const { appState, setAppState } = useContext(appContext)
const [, update] = useState({})
const data = selector ? selector(appState) : { appState: appState }
+ const dispatchers = dispatchSelector ? dispatchSelector(dispatch) : { dispatch: dispatch }
useEffect(() => {
const unsubscribe = store.subscribe(() => {
const newData = selector ? selector(store.appState) : { appState: store.appState }
if (changed(data, newData)) {
// 这里可以对state进行精准控制
update({})
}
})
return unsubscribe
// 此时依赖改为 [selector,appState] 也不会重复订阅。每次值变化的时,先执行return的unsubscribe函数。
}, [selector])
+ return <Component {...props} {...dispatchers} {...data} />
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1.12. connect 接受 selector
的意义
在上述步骤中,connect
主要实现了以下功能:
- 是一个
HOC
,目的是与全局的store
进行,本质上就是一个createWrapper
。 - 接受两个参数:
mapStateToProps
,mapDispatchToProps
,可以实现深层数据的快速获取
,以及选中属性的精确渲染
。
除了以上的功能外,可以通过单独构建connections
文件,实现对全局状态的拆分。
// 创建一个专门修改 User 对象的环境(即,只暴露全局与User有关的属性和修改属性的方法)
const mapStatetoProps = state => {
return { group: state.group }
}
const mapDispatchToProps = dispatch => {
return {
updateUser: (attrs) => dispatch("updateUser", attrs)
}
}
const connectToUser = connect(mapStatetoProps, mapDispatchToProps)
2
3
4
5
6
7
8
9
10
11
12
在开发阶段,我们可以直接使用 connectToUser
代替 connect
函数进行状态获取。
// 导入 connecters
import { connectToUser } from "./connecters/connectToUser"
2
综上,connect
的意义见下图所示:
- 通过
mapStateToProps
函数store
可被进一步划分,返回的结果是只与User
状态项链接的一个createWrapper
函数,可用于生成HOC
(Wrapper
) 高阶组件。 - 不同颜色的
Wrapper
在状态发生变化的时候,只会渲染与自己相关联的Wrapper
。 - 在状态封装方面,下图左侧部分的一般可由经验丰富的程序员预先封装,而业务开发的时候可直接调用这些封装好的函数进行
HOC
的生成。
# 1.13. 封装 createStore
至此 redux
的源码的核心原理基本上结束了,下面开始都是封装技巧。
之前为了演示方便,直接将 reducer
和 state
是写死在redux.js
文件中,而这两个部分应由开发人员编写后,显示传递到store
中。
构造 createStore
函数:
// 将 reducer 和 state 中从 `store` 中抽出,由外部传入。
const store = {
appState: undefined,
reducer: undefined,
.........
}
export const createStore = (reducer, initState) => {
store.reducer = reducer
store.appState = initState
return store
}
2
3
4
5
6
7
8
9
10
11
12
# 1.14. 封装 Provider
函数
就是将appContext.Provider
<appContext.Provider value={store}>
<Child1></Child1>
<Child2></Child2>
<Child3></Child3>
</appContext.Provider>
2
3
4
5
改造为:
<Provider store={store}>
<Child1></Child1>
<Child2></Child2>
<Child3></Child3>
</Provider>
2
3
4
5
代码:
// 封装 Provider
export const Provider = ({ store, children }) => {
return (
<appContext.Provider value={store}>
{children}
</appContext.Provider>
);
}
2
3
4
5
6
7
8
# 2.大致总结
reducer
函数:本质上就是createNewState
函数,需要接受三个参数才能完成状态state
的修改。dispatch
函数:是对createNewState
函数进行封装的,抽离出全局的state
,简化成输入两个参数(即,action:{actionType,payload}
)。createWrapper
函数:connect
函数的前身,主要用于批量化生成HOC
组件。(核心中的核心)store
对象的封装:- 全局
state
存储。 subscribe
函数,即eventHub
的on
函数。listener
监听器,用于存放订阅函数update({})
。
- 全局
connect
函数封装:- 接受两个参数:
mapStateToProps
、mapDispatchToProps
,本质就是selector
函数。 - 用于和全局状态进行链接,
connect(null,null)(Component)
等价于createWrapper
函数 - 如果
mapStateToProps
有值,则createWrapper
可以和特定state
进行链接,并且可以做到对筛选state
精确渲染。
- 接受两个参数:
Provider
与createStore
函数只是在原有函数的基础上进行封装。ActionTypes
、ActionCreators
等都属于action
模板写法。