前言
Redux
也是我列在 THE LAST TIME
系列中的一篇,由于现在正在着手探究关于我目前正在开发的业务中状态管理的方案。所以,这里打算先从 Redux
中学习学习,从他的状态中取取经。毕竟,成功总是需要站在巨人的肩膀上不是。
话说回来,都 2020 年了还在写 Redux
的文章,真的是有些过时了。不过呢,当时 Redux
孵化过程中一定也是回头看了 Flux
、CQRS
、ES
等。
本篇先从 Redux
的设计理念到部分源码分析。下一篇我们在注重说下 Redux
的 Middleware
工作机制。至于手写,推荐砖家大佬的:完全理解 redux(从零实现一个 redux)
Redux
Redux
并不是什么特别 Giao 的技术,但是其理念真的提的特别好。
说透了,它就是一个提供了 setter
、getter
的大闭包,。外加一个 pubSub
。。。另外的什么 reducer
、middleware
还是 action
什么的,都是基于他的规则和解决用户使用痛点而来的,仅此而已。下面我们一点点说。。。
设计思想
在 jQuery 时代的时候,我们是面向过程开发,随着 react 的普及,我们提出了状态驱动 UI 的开发模式。我们认为: Web 应用就是状态与 UI 一一对应的关系。
但是随着我们的 web 应用日趋的复杂化,一个应用所对应的背后的 state 也变的越来越难以管理。
而 Redux
就是我们 Web 应用的一个状态管理方案。
一一对应
如上图所示,store 就是 Redux
提供的一个状态容器。里面存储着 View 层所需要的所有的状态(state)。每一个 UI 都对应着背后的一个状态。Redux
也同样规定。一个 state 就对应一个 View。只要 state 相同,View 就相同。(其实就是 state 驱动 UI)。
为什么要使用 Redux
如上所说,我们现在是状态驱动 UI,那么为什么需要 Redux
来管理状态呢?react 本身就是 state drive view 不是。
原因还是由于现在的前端的地位已经愈发的不一样啦,前端的复杂性也是越来越高。通常一个前端应用都存在大量复杂、无规律的交互。还伴随着各种异步操作。
任何一个操作都可能会改变 state,那么就会导致我们应用的 state 越来越乱,且被动原因愈发的模糊。我们很容易就对这些状态何时发生、为什么发生、怎么发生而失去控制。
如上,如果我们的页面足够复杂,那么view
背后state
的变化就可能呈现出这个样子。不同的 component
之间存在着父子、兄弟、子父、甚至跨层级之间的通信。
而我们理想中的状态管理应该是这个样子的:
单纯的从架构层面而言,UI 与状态完全分离,并且单向的数据流确保了状态可控。
而 Redux
就是做这个的!
- 每一个
State
的变化可预测 - 动作和状态统一管理
下面简单介绍下 Redux
中的几个概念。其实初学者往往就是对其概念而困惑。
store
保存数据的地方,你可以把它看成一个容器,整个应用只能有一个
Store
。
State
某一个时刻,存储着的应用状态值
Action
View 发出的一种让
state
发生变化的通知
Action Creator
可以理解为
Action
的工厂函数
dispatch
View 发出
Action
的媒介。也是唯一途径
reducer
根据当前接收到的Action
和State
,整合出来一个全新的State
。注意是需要是纯函数
三大原则
Redux
的使用,基于以下三个原则
单一数据源
单一数据源这或许是与 Flux 最大的不同了。在 Redux
中,整个应用的 state
都被存储到一个object
中。当然,这也是唯一存储应用状态的地方。我们可以理解为就是一个 Object tree
。不同的枝干对应不同的 Component
。但是归根结底只有一个根。
也是受益于单一的 state tree
。以前难以实现的“撤销/重做”甚至回放。都变得轻松了很多。
State 只读
唯一改变 state
的方法就是 dispatch
一个 action
。action
就是一个令牌而已。normal Object
。
任何 state
的变更,都可以理解为非 View
层引起的(网络请求、用户点击等)。View
层只是发出了某一中意图。而如何去满足,完全取决于 Redux
本身,也就是 reducer。
`store.dispatch({
type:'FETCH_START',
params:{
itemId:233333
}
})`
使用纯函数来修改
所谓纯函数,就是你得纯,别变来变去了。书面词汇这里就不做过多解释了。而这里我们说的纯函数来修改,其实就是我们上面说的 reducer
。
Reducer
就是纯函数,它接受当前的 state
和 action
。然后返回一个新的 state
。所以这里,state
不会更新,只会替换。
之所以要纯函数,就是结果可预测性。只要传入的 state
和 action
一直,那么就可以理解为返回的新 state
也总是一样的。
总结
Redux
的东西远不止上面说的那么些。其实还有比如 middleware、actionCreator 等等等。其实都是使用过程中的衍生品而已。我们主要是理解其思想。然后再去源码中学习如何使用。
源码分析
Redux 源码本身非常简单,限于篇幅,我们下一篇再去介绍compose
、combineReducers
、applyMiddleware
目录结构
Redux
源码本身就是很简单,代码量也不大。学习它,也主要是为了学习他的编程思想和设计范式。
当然,我们也可以从 Redux
的代码里,看看大佬是如何使用 ts 的。所以源码分析里面,我们还回去花费不少精力看下 Redux
的类型说明。所以我们从 type 开始看
src/types
看类型声明也是为了学习Redux
的 ts 类型声明写法。所以相似声明的写法形式我们就不重复介绍了。
actions.ts
类型声明也没有太多的需要去说的逻辑,所以我就写注释上吧
`// Action的接口定义。type 字段明确声明
export interface Action
type: T
}
export interface AnyAction extends Action {
// 在 Action 的这个接口上额外扩展的另外一些任意字段(我们一般写的都是 AnyAction 类型,用一个“基类”去约束必须带有 type 字段)
[extraProps: string]: any
}
export interface ActionCreator {
// 函数接口,泛型约束函数的返回都是 A
(...args: any[]): A
}
export interface ActionCreatorsMapObject {
// 对象,对象值为 ActionCreator
[key: string]: ActionCreator
}
`
reducers.ts
`// 定义的一个函数,接受 S 和继承 Action 默认为 AnyAction 的 A,返回 S
export type Reducer = (
state: S | undefined,
action: A
) => S
// 可以理解为 S 的 key 作为ReducersMapObject的 key,然后 value 是 Reducer的函数。in 我们可以理解为遍历
export type ReducersMapObject = {
[K in keyof S]: Reducer
}
`
上面两个声明比较简单直接。下面两个稍微麻烦一些
`export type StateFromReducersMapObject
any,
any
? { [P in keyof M]: M[P] extends Reducer? S : never }
: never
export type ReducerFromReducersMapObject= M extends {
[P in keyof M]: infer R
}
? R extends Reducer
? R
: never
: never
`
上面两个声明,咱们来解释其中第一个吧(稍微麻烦些)。
-
StateFromReducersMapObject
添加另一个泛型M
约束 -
M
如果继承ReducersMapObject
则走{ [P in keyof M]: M[P] extends Reducer
的逻辑? S : never } - 否则就是
never
。啥也不是 -
{ [P in keyof M]: M[P] extends Reducer
很明显,这就是一个对象,? S : never } key
来自M
对象里面,也就是ReducersMapObject
里面传入的S
。key
对应的value
就是需要判断M[P]
是否继承自Reducer
。否则也啥也不是 -
infer
关键字和extends
一直配合使用。这里就是指返回Reducer
的这个State
的类型
其他
types
目录里面其他的比如 store
、middleware
都是如上的这种声明方式,就不再赘述了,感兴趣的可以翻阅翻阅。然后取其精华的应用到自己的 ts 项目里面
src/createStore.ts
不要疑惑上面函数重载的写法~
可以看到,整个createStore.ts 就是一个createStore 函数。
createStore
三个参数:
-
reducer
:就是 reducer,根据 action 和 currentState 计算 newState 的纯 Function -
preloadedState
:initial State -
enhancer
:增强store的功能,让它拥有第三方的功能
createStore
里面就是一些闭包函数的功能整合。
INIT
`// A extends Action
dispatch({ type: ActionTypes.INIT } as A)
`
这个方法是Redux
保留用的,用来初始化State
,其实就是dispatch
走到我们默认的 switch case default
的分支里面获取到默认的 State
。
return
`const store = ({
dispatch: dispatch as Dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
} as unknown) as Store
`
ts 的类型转换语法就不说了,返回的对象里面包含dispatch
、subscribe
、getState
、replaceReducer
、[$$observable]
.
这里我们简单介绍下前三个方法的实现。
getState
`` function getState(): S {
if (isDispatching) {
throw new Error(
我 reducer 正在执行,newState 正在产出呢!现在不行
)
}
return currentState as S
}
``
方法很简单,就是 return currentState
subscribe
subscribe
的作用就是添加监听函数listener
,让其在每次dispatch action
的时候调用。
返回一个移除这个监听的函数。
使用如下:
`const unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
unsubscribe();
`
``function subscribe(listener: () => void) {
// 如果 listenter 不是一个 function,我就报错(其实 ts 静态检查能检查出来的,但!那是编译时,这是运行时)
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
// 同 getState 一个样纸
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://Redux
.js.org/api/store#subscribelistener for more details.'
)
}
let isSubscribed = true
ensureCanMutateNextListeners()
// 直接将监听的函数放进nextListeners里
nextListeners.push(listener)
return function unsubscribe() {// 也是利用闭包,查看是否以订阅,然后移除订阅
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://Redux
.js.org/api/store#subscribelistener for more details.'
)
}
isSubscribed = false//修改这个订阅状态
ensureCanMutateNextListeners()
//找到位置,移除监听
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}
``
一句话解释就是在 listeners 数据里面添加一个函数
再来说说这里面的ensureCanMutateNextListeners
,很多 Redux
源码都么有怎么提及这个方法的作用。也是让我有点困惑。
这个方法的实现非常简单。就是判断当前的监听数组里面是否和下一个数组相等。如果是!则 copy 一份。
` let currentListeners: (() => void)[] | null = []
let nextListeners = currentListeners
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
`
那么为什么呢?这里留个彩蛋。等看完 dispatch
再来看这个疑惑。
dispatch
` function dispatch(action: A) {
// action必须是个普通对象
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
// 必须包含 type 字段
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
// 同上
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
// 设置正在 dispatch 的 tag 为 true(解释了那些判断都是从哪里来的了)
isDispatching = true
// 通过传入的 reducer 来去的新的 state
// let currentReducer = reducer
currentState = currentReducer(currentState, action)
} finally {
// 修改状态
isDispatching = false
}
// 将 nextListener 赋值给 currentListeners、listeners (注意回顾 ensureCanMutateNextListeners )
const listeners = (currentListeners = nextListeners)
// 挨个触发监听
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
`
方法很简单,都写在注释里了。这里我们再回过头来看ensureCanMutateNextListeners
的意义
ensureCanMutateNextListeners
` let currentListeners: (() => void)[] | null = []
let nextListeners = currentListeners
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
function subscribe(listener: () => void) {
// ...
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}
function dispatch(action: A) {
// ...
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
// ...
return action
}
`
从上,代码看起来貌似只要一个数组来存储listener
就可以了。但是事实是,我们恰恰就是我们的 listener
是可以被 unSubscribe
的。而且 slice
会改变原数组大小。
所以这里增加了一个 listener
的副本,是为了避免在遍历listeners
的过程中由于subscribe
或者unsubscribe
对listeners
进行的修改而引起的某个listener
被漏掉了。
最后
限于篇幅,就暂时写到这吧~
其实后面打算重点介绍的 Middleware
,只是中间件的一种更规范,甚至我们可以理解为,它并不属于 Redux
的。因为到这里,你已经完全可以自己写一份状态管理方案了。
而 combineReducers
也是我认为是费巧妙的设计。所以这些篇幅,就放到下一篇吧~
参考链接
学习交流
- 关注公众号【全栈前端精选】,每日获取好文推荐
- 添加微信号:is_Nealyang(备注来源) ,入群交流
公众号【全栈前端精选】
个人微信【is_Nealyang】
本文使用 mdnice 排版