什么是状态?
状态(State)是指应用程序在某个时刻的内部状态。状态可以包括应用程序的当前数据、用户输入、用户操作等。状态通常由一个对象或变量来表示,它可以帮助开发人员管理应用程序的行为和响应用户输入的变化。例如,在处理用户输入时,开发人员需要将用户输入的状态转换为应用程序可以使用的格式。
为什么使用Redux?
现实问题
随着前端应用开发日趋复杂,需要管理的状态越来越多,状态变化越来越频繁,我们很容易就对这些状态何时发生、为什么发生以及怎么发生的失去控制;组件之间的通信和协调也变得困难。尤其是跨越多个层级的组件。因此,如何更好地进行状态管理是理我们需要思考的问题。
状态管理库
为了应对日益复杂的 State 状态,行业内提出不同的解决方案,目前比较常见的状态管理库有redux、zustand、xstate、mobx、jotai、recoil等。从 npm trends 看各个状态管理库近一年的下载量趋势:
可以看到 Redux 作为 React 状态管理的老大哥,下载量上依然瑶瑶领先其他小兄弟。那么Redux有什么魔力使得自己成为绝大多数开发者状态管理库的首选。
Redux的优点
- 可预测性,严格遵循单向数据流,所有的状态变更都通过分发 action,使得状态变更变得可预测,容易理解和调试。可预测性是 Reudx 设计的最大亮点;
- 繁荣的社区,拥有庞大的社区和成熟的支持,以及大量的教程、实例;
- 可扩展性高,中间件模式提供了灵活的扩展能力,让你可以随心所欲的武装你的 dispatch;
- 良好的开发体验,拥有成熟的开发调试工具 redux devtools;
Redux能带来什么
Redux 帮你管理"全局"状态,提供的模式使你更容易理解应用程序中的状态何时、何地、为什么、state如何被更新,以及当这些更改发生时你的应用程序逻辑将如何更新。指导你编写可预测和可测试的代码。
如果我们的页面特别复杂,如上左图所示,存在父子组件,子父组件,兄弟组件之间的通信,数据也存在正向,单向,跨层的数据流动,在未使用任何状态管理库之前,维护起来就会非常困难;在使用 Redux 状态管理库之后,如上右图所示,所有组件间的通信都通过分发 action,一次一个 action,针对应用内唯一的 store 进行更改,然后再通过订阅更新组件的UI,所有的数据流向都是单向的,确保整个流程更加清晰。
认识Flux
Redux 基于 Flux 单向数据流的思想,在深入了解 Redux 之前我们有必要先了解下 Flux。
Flux 是由 Facebook 提出的一种应用架构模式,作为主流的JavaScript MVC模式的替代品被开发。传统MVC模式中多采用双向数据绑定,视图的改变会更新相应的模型,模型的更改也会更新相应的视图,模型(model)、视图(view)和控制器(controller)之间的数据流可能会难以理解追寻。Flux试图解决状态的不可预测性,以及模型与视图紧密耦合的架构的脆弱性。废弃了双向数据绑定模型,转而采用单项数据流。Flux要求对状态的所有修改遵循单一的路径,而不允许每个视图与对应的模型进行交互。
在 Flux 架构中,一个应用将被拆分为以下 4 个部分
-
View:用户界面。
-
Action:可以理解为视图层发出的"消息",它会触发应用状态的改变
-
Dispatcher:派发器,负责对 action 进行分发;
-
Store:数据层,存储应用状态的"仓库",此外还会定义修改状态的逻辑。store 的变化最终会映射到 view 层上去
Flux工作流如下图所示:
用户与 View 之间产生交互,通过 View 发起一个 action,Dispatcher 会把这个 Action 派发给 Store,通知 Store 进行相应的状态更新。Store 状态更新完成后,会进一步通知 View 去更新界面。
Redux 的介绍
Redux 是JavaScrip应用程序的可预测状态的容器。作为React的状态管理层,主要目标是为应用程序中的数据带来一致性与可预测性。Redux将状态管理的职责划分为一下几个独立的单元:
-
Store:将应用程序的所有状态都存储在单个对象中(对象树)
-
Action:描述实现的对象,只能使用action更新store
-
Reducer:指定了如何转换应用程序的状态,它接收store中的当前状态和一个action,然后返回更新后的下一个状态
Redux工作流如下图所示:
三大原则
Redux中的状态由单一的可信数据源(single source of truth)表示,是只读的,并且只能用纯函数进行修改。这其中蕴含了Redux的三大原则,即
- 单一数据源:整个应用的 state 被存储在一个对象树中,并且只存在于唯一的 store 中;
- 状态是只读的:不能直接去修改 state,只能通过触发 action 来返回一个新的 state;
- 改变由纯函数进行:要使用纯函数来修改 state;
Redux与Flux的区别
Redux 是一种基于 Flux 思想的实现方式,它在Flux基础上进行了改进。Redux采用了单向数据流的概念,与Flux最大的区别在于 Redux 只有一个 Store,而 Flux 允许多个 Store 存在。相较于Flux,Redux的单一Store更加清晰和易于管理。在Flux中,会有多个Store用于存储应用数据,并在Store中执行更新逻辑,当Store发生变化时,再通知controller-view来更新数据。而Redux将这些Store整合成一个完整的Store,并且可以通过这个Store推导出整个应用的State。
另外,Redux的更新逻辑不再放在Store中执行,而是放在Reducer中。单一Store带来的好处是,所有的数据结果都集中在一处,操作起来更加便利。只要将它传递给最外层组件,内层组件就不需要维持自己的State,只需要通过props从父级组件传递下来即可。这使得子组件变得异常简单。
Redux使用示例
我们先来看下Redux的示例代码
javascript
import { createStore } from 'redux';
// store 至少需要一个 reducer 函数(counterReducer)
function counterReducer(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
return state;
}
// 使用 reducer 创建 store
const store = createStore(counterReducer);
// 读取 store 的当前状态
console.log(store.getState());
// store订阅一个更新函数,待dispatch之后,执行这个更新函数,获取新的值
store.subscribe(() -> {
console.log('current state: ', store.getState());
});
const render = () => ReactDOM.render(
<div>
<span>{store.getState()}</span>
// 向 reducer 发送一个 action 来更新 store
<button onClick=={() => store.dispatch({ type: 'INCREMENT' })}>INCREMENT</button>
<button onClick=={() => store.dispatch({ type: 'DECREMENT' })}>DECREMENT</button>
</div>,
root
)
render()
这个例子演示了如何使用Redux来实现点击按钮增减数字的效果。
-
首先,我们使用 reducer 创建一个 store,以便与 Redux 进行交互。
-
通过调用store.getState()方法获取当前的数字,初始值为0。
-
使用store.subscribe()方法订阅一个用于更新页面的函数。每当reducer返回新的值时,该函数就会被调用
-
当点击按钮时,我们通过调用dispatch方法告诉Redux我们是要增加还是减少数字(同时传入相应的action)。
-
dispatch方法内部会调用我们定义的reducer函数,结合当前的state和action,返回新的state。
-
在返回新的state之后,我们调用订阅的更新函数,以更新页面。
总结一下,我们所有的操作都是通过store进行的,通过createStore方法创建唯一的 store ,提供getSate、dispatch、subscribe等方法。
Redux 的实现
现在让我们来看一下Redux内部的实现逻辑。Redux源码的src目录,主要包含6个核心文件,主要包括index.js、createStore.js、compose.js、combineRuducers.js、bindActionCreators.js和applyMiddleware.js文件
- index.js:Redux入口文件,暴露出来主要接口
- createStore.js:会创建一个 store 及其相应的 getSate、dispatch 和 subscribe 操作
- combineReducers.js:合并多个 reducer 为一个总的 reducer
- bindActionCreators.js:返回包裹 dispatch 的函数可以直接使用。 一般用在 mapDispatchToProps 里
- applyMiddleware.js:提供中间件,如 redux-thunk、redux-logger
- compose.js:combineReducers 中会用到的工具函数
下面我们就逐一对核心源码实现进行解读
createStore(reducer)
createStore 函数是Redux的核心,接受reducer状态更新函数,最终返回对象store,用来存放应用中的state,应用中仅有一个store,包含dispatch、subscribe、getState、replaceReducer等方法。简化后的代码如下
javascript
export function createStore(reducer, preloadedState, enhancer) {
// 省略异常处理
// 当前的reducer
let currentReducer = reducer
// 当前的state
let currentState = preloadedState
let currentListeners: Map<number, ListenerCallback> | null = new Map()
let nextListeners = currentListeners
let listenerIdCounter = 0
// 是否正在dispatch aciton,想当于一把锁
let isDispatching = false
// 对当前订阅进行浅拷贝,防止在dispatch时订阅/取消订阅产生bug
function ensureCanMutateNextListeners() {}
// 读取由store管理的状态树
function getState(): S {}
// 注册监听事件,维护存放所有订阅函数的数组,并返回取消订阅的函数
function subscribe(listener: () => void) {}
// 分发action,这是触发State状态树变化的唯一方式
function dispatch(action: A) {}
// 替换当前store的reducer,并重新初始化State状态树
function replaceReducer(nextReducer: Reducer<S, A>): void {}
function observable() {}
// store被创建后,分发'INIT'的action,初始State状态树
dispatch({ type: ActionTypes.INIT } as A)
// 组装并返回store对象
const store = {
dispatch: dispatch as Dispatch<A>,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
} as unknown as Store<S, A, StateExt> & Ext
return store
}
store.dispatch(action)
dispatch函数分发action,是修改state的唯一方式
javascript
function dispatch(action) {
// 省略异常处理
try {
isDispatching = true
// 调用当前reducer,传入当前state和action,并更新state状态树
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
// 依次执行注册的lister监听事件
listeners.forEach(listener => {
listener()
})
// 返回action
return action
}
store.getState()
getState函数比较简单,直接返回当前状态树
csharp
function getState() {
// 省略异常处理
// 返回当前状态树
return currentState as S
}
store.replaceReducer(nextReducer)
replaceReducer函数用于替换当前store对象的reducer
php
function replaceReducer(nextReducer) {
// 省略异常处理
// 替换当前reducer
currentReducer = nextReducer
// 分发'REPLACE'的action,重新初始化State状态树
dispatch({ type: ActionTypes.REPLACE } as A)
}
store.subscribe(lister)
subscribe接收监听函数,主要做了两件事,一是订阅监听函数,存放在数组中,在store.dispatch(action)时遍历执行监听函数,二是返回取消订阅函数
csharp
function subscribe(listener: () => void) {
// 省略异常处理
let isSubscribed = true
ensureCanMutateNextListeners()
// 自增订阅函数id
const listenerId = listenerIdCounter++
// 将订阅函数放入store内部维护的集合中
nextListeners.set(listenerId, listener)
// 返回取消订阅的函数
return function unsubscribe() {
if (!isSubscribed) {
return
}
isSubscribed = false
ensureCanMutateNextListeners()
nextListeners.delete(listenerId)
currentListeners = null
}
}
combineReducers()
combineReducers 函数用于将多个reducer函数合并成一个总的reducer,每次Reducer返回新的state时会和老的state对比,如果发生改变,hasChanged为true,使用新state触发页面更新;反之则不做处理。
typescript
export default function combineReducers(reducers) {
// 获取所有reducers的key,组成数组
const reducerKeys = Object.keys(reducers)
// 获取最终有效的reducers,组成字典finalReducers
const finalReducers: { [key: string]: Reducer<any, any, any> } = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
// This is used to make sure we don't warn about the same
// keys multiple times.
let unexpectedKeyCache: { [key: string]: true }
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
let shapeAssertionError: unknown
try {
// 用于检查每个reducer有没有默认返回的state
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// 这个函数的核心是循环遍历finalReducers,获取每个reducer对应的旧状态,并根据当前dispatch的action,生成新的sate状态,最终将所有新的state做为值,reducer的name为键,生成做最终的state
return function combination(
state: StateFromReducersMapObject<typeof reducers> = {},
action: Action
) {
if (shapeAssertionError) {
throw shapeAssertionError
}
// 是否state发生了变化
let hasChanged = false
const nextState: StateFromReducersMapObject<typeof reducers> = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
// 获取每个reducer的name
const key = finalReducerKeys[i]
// 获取每个reducer
const reducer = finalReducers[key]
// 获取每个reducer的旧state状态
const previousStateForKey = state[key]
// 获取每个reducer,根据这个reducer的旧state状态和当前action生成新state状态
const nextStateForKey = reducer(previousStateForKey, action)
// 以每个reducer的name为key,新state状态为值,更新最终的state
nextState[key] = nextStateForKey
// 判断是否state的值发生了变化
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length
// state发生了变化,返回新的sate,否则返回旧的sate
return hasChanged ? nextState : state
}
}
bindActionCreator()
bindActionCreator函数是将单个actionCreator绑定到dispatch上,bindActionCreators就是将多个actionCreators绑定到dispatch上。
javascript
function bindActionCreator(actionCreator, dispatch) {
return function (this, ...args) {
return dispatch(actionCreator.apply(this, args))
}
}
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
// 省略异常处理
const boundActionCreators: ActionCreatorsMapObject = {}
// 遍历整个actionCreators,如果item是function类型,就调用bindActionCreator将actionCreator绑定到dispatch上
for (const key in actionCreators) {
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
applyMiddleware()
applyMiddleware是一个增强器,组合多个中间件,最终增强store.dispatch()函数。reducer的设计理念是纯函数,如果需要一些副作用(处理异步逻辑、日志记录等),中间件的作用就体现出来了。
javascript
export default function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer<any> {
return createStore => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState)
// 省略异常处理
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
// 遍历中间件,将middlewareAPI传进去,得到的chain是一个数组
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 利用compose函数依次执行中间件函数得到加强后的dispatch
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
总结
本文主要介绍了Redux的设计理念和源码解析。正如任何工具都有适用与非适用的场景,在使用Redux时也需要做权衡,Reudx的创始人之一Dan Abramov也发表过"你可能不需要Redux(You Might Not Need Redux)"。Redux适用于应用程序中存在大量状态,且状态变更逻辑复杂频繁的场景,对于没有复杂状态的较小程序,使用Redux产生的模板代码,反而使得不如直接使用React来的方便。
hi, 我是快手社交的 键盘破风手
快手社交技术团队 正在招贤纳士🎉🎉🎉! 我们是公司的核心业务线, 这里云集了各路高手, 也充满了机会与挑战. 伴随着业务的高速发展, 团队也在快速扩张. 欢迎各位高手加入我们, 一起创造世界级的产品~
热招岗位: Android/iOS 高级开发, Android/iOS 专家, Java 架构师, 产品经理, 测试开发... 大量 HC 等你来呦~ 内部推荐请发简历至 >>>我们的邮箱: social-tech-team@kuaishou.com <<<, 备注我的花名成功率更高哦~ 😘