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:待修改的仓库stateactionType:执行的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模板写法。