本来想写一篇redux吐槽的,整篇都在阴阳怪气,但是后来发现没什么意思,怎么写也超不过500字,就修改重写了,改成了redux入门和redux的原理实现
1 redux核心思想
在redux
中,有3个核心思想需要了解
- Store:存储数据的地方(仓库)
- Reducer:改变数据的地方
- Action:告诉
Reducer
如何改变数据
2 redux入门
2.1 Action
首先了解一下Action
,Action
就是一个对象,类型定义如下。在Reducer
中通过Action
的type
来判断要如何更新数据
interface Action<T = any> {
type: T
}
但是大多数情况下,一个type
是不够的(增删改),还需要给action
携带一个数据,扩展如下
interface MyAction<T = any, D = any> extends Action<T> {
data: D
}
2.2 Reducer
Reducer
类型定义如下,Reducer
就是一个方法,此方法第一个参数就是存储在Store
中的数据,第二个参数就是Action
,返回的S
就是修改后的数据
type Reducer<S = any, A extends Action> = (
state: S | undefined,
action: A
) => S
Reducer
工作流程如下
- 第一步:开发者想要更新
Store
,创建一个对象,这个对象在redux
中就是Action
- 第二步:调用
redux
提供的方法,把Action
传进去 - 第三步:
redux
内部调用Reducer
,Reducer
内部根据传入的Action
,修改第一个参数,并把修改后的值return
出来 - 第四步:
redux
内部接收到Reducer
的返回值,更新数据
之后为了解耦,Reducer
可以拆分成无限个函数方便管理,但是这意味着,每次更新的时候,redux
内部需要把所有的Reducer
全部执行一遍用于获取新的状态,有没有感觉这和React
的思想非常相似
2.3 Store
上面两个说的都是概念,需要配合Store
一起使用才有意义
2.3.1 createStore
那么就需要引出redux
的核心方法createStore
。此方法就是用来创建“数据”管理的工具,先看下面这个简化后的类型定义
interface Dispatch<A extends Action> {
<T extends A>(action: T): T
}
interface Store<S = any, A extends Action = Action> {
// 获取当前存储在Store中的数据
getState(): S
// 修改数据
dispatch: Dispatch<A>
// 用于订阅数据变化
subscribe(listener: () => void): () => void
// 用于替换reducer,这里不实现
// replaceReducer
}
type createStore = <S, A extends Action>(
// reducer
reducer: Reducer<S, A>
) => Store<S, A>
接着就可以根据上面的类型定义写一个Store
import { createStore } from 'redux'
import type { Action } from 'redux'
function reducer(count = 0, action: Action<'increase' | 'decrease'>) {
switch (action.type) {
case 'increase': return ++count
case 'decrease': return --count
default: return count
}
}
const store = createStore(reducer)
“非常简单”的一个仓库就创建好了,接下来要干的事情无非就下面三个
- 获取仓库中存储的数据
- 修改仓库中存储的数据
- 监听仓库中存储的数据的变化
正好这三个需要对应了createStore
的三个返回值 getState
,dispatch
,subscribe
2.3.2 getState
getState
是用来获取仓库中存储的数据的方法
store.getState() // 0
2.3.3 dispatch
此方法用来修改仓库中存储的数据,它的参数就是Action
store.getState() // 0
store.dispatch({ type: 'increase' })
store.getState() // 1
store.dispatch({ type: 'increase' })
store.getState() // 2
2.3.4 subscribe
此方法用来订阅数据变化
store.subscribe(()=>{
console.log('updated') // 调用两次
})
store.dispatch({ type: 'increase' })
store.dispatch({ type: 'increase' })
并且subscribe
还会返回一个函数,用于取消订阅
const unsubscribe = store.subscribe(()=>{})
unsubscribe() // 取消监听
2.4 combineReducers
如果整个项目中只能存在一个Reducer
,那这个Reducer
一定非常大,可以对它进行拆分,拆分完成之后,再通过 redux
提供的 combineReducers
方法进行整合
function reducer1(count = 0, action: Action<'increase' | 'decrease'>) {
switch (action.type) {
case 'increase': return ++count
case 'decrease': return --count
default: return count
}
}
function reducer2(count = 1, action: Action<'increase' | 'decrease'>) {
switch (action.type) {
case 'increase': return ++count
case 'decrease': return --count
default: return count
}
}
const store = createStore(combineReducers({
count1: reducer1,
count2: reducer2
}))
store.getState() // {count1: 0, count2: 1}
当我们调用dispatch
想要修改count1
的时候,就会发现下面的问题,count2
也一起更改了
console.log(store.getState()) // {count1: 0, count2: 1}
store.dispatch({ type: 'increase' })
console.log(store.getState()) // {count1: 1, count2: 2}
这是因为redux
内部会执行所有的Reducer
,increase
这个type
两个Reducer
中都使用到了,所以就会引发问题,为了解决这个问题,有两种方式解决
- 使用
symbol
来当做key
- 约定好
type
,不能重复
2.5 bindActionCreators
每次修改数据的时候,都必须做两件事情
- 寻找要修改数据的那个
type
- 调用
dispatch
很麻烦,所以redux
提供了bindActionCreators
方法,能够把上面两个步骤合并成一件事情
import { createStore, bindActionCreators } from 'redux'
function reducer(count = 0, action: Action<'increase' | 'decrease'>) {
switch (action.type) {
case 'increase': return ++count
case 'decrease': return --count
default: return count
}
}
const store = createStore(reducer)
const actions = bindActionCreators(
{
increase: () => ({ type: 'increase' }),
decrease: () => ({ type: 'decrease' })
},
store.dispatch
)
actions.increase()
actions.increase()
2.6 中间件
redux
还提供了中间件,在redux
中的中间件就是一个方法,只是这个方法有点特殊,如下所示,为了方便理解,在定义方法的时候,使用a,b,c为子函数命名
那么a函数中的参数api
的类型定义如下,其中包含两个函数,dispatch
和 getState
interface MiddlewareAPI<D extends Dispatch = Dispatch, S = any> {
dispatch: D
getState(): S
}
你没有猜错,getState
与createStore
返回的getState
表现性质一致,但是dispatch
却不同,在redux
源码中,此处的dispatch
如果调用它的话,只会得到一个警告错误,所以请永远不要在中间件中调用api.dispatch
那么b函数中的参数next
是什么?其实这个next
就是"dispatch
方法",只不过此dispatch
并不是上面理解的dispatch
方法。在redux
中,需要使用 applyMiddleware
组合所有的中间件,如下
const store = createStore(
reducer,
applyMiddleware(
middleware1,
middleware2
)
)
因此,想要知道第二个参数的next
是啥,就得去了解一下 applyMiddleware
与createStore
的关系,如下图所示
从上图可以看到,创建Store
的这个操作其实是在applyMiddleware
中进行的,有点套娃的意思,那么现在需要搞懂的自然就是调用createStore
与end
之前发生了什么?
第一步,applyMiddleware
内部定义了一个dispatch
函数,并与createStore
返回的getState
方法进行了一个组装,这个组装好的对象就是传递给中间件的参数
let dispatch: Dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch
}
第二步,调用所有的中间件,并获取中间件的返回值(b函数)
const chain = middlewares.map(middleware => middleware(middlewareAPI))
第三步,applyMiddleware
把所有的chain
都传入到了 compose
方法中
dispatch = compose(chain)(store.dispatch)
先不看 compose(chain)
后面的(store.dispatch)
,先看 compose
是做什么的,核心代码如下(为了好理解做了部分修改)
function compose(funcs: Function[]) {
return funcs.reduce((a, b) => {
return (...args: any) => {
return a(b(...args))
}
})
}
为了理解,看下面这个例子。比如此时,funcs
的值是
[ b1, b2, b3, b4, b5, b6 ]
分别对应middleware1
,middleware2
...的返回值,那么在reduce
的循环中,第一次调用回调的形参a和b分别是b1
和b2
,那么返回值可以看成
(...args) => {
return b1(b2(...args))
}
第二次循环中,形参a为上一次循环的返回值,b
为b3
,那么返回值可以看成
(...args) => {
return b1(b2(b3(...args)))
}
以此类推,最后compose
就会返回一个这样的函数
(...args) => {
return b1(b2(b3(b4(b5(b6(...args))))))
}
接着,把store.dispatch
传入到此函数中,那么效果如下
middleware6的b函数的next形参就是store.dispatch
middleware5的b函数的next形参就是middleware6的b函数的返回值c函数
middleware4的b函数的next形参就是middleware5的b函数的返回值c函数
middleware3的b函数的next形参就是middleware4的b函数的返回值c函数
middleware2的b函数的next形参就是middleware3的b函数的返回值c函数
middleware1的b函数的next形参就是middleware2的b函数的返回值c函数
重新生成的dispatch就是middleware1的b函数的返回值c函数
那么最后一步,把store
和新的dispatch
函数组装,返回
return {
...store,
dispatch
}
那么到此为止,中间件中第三个参数action
其实就很明确了,就是在创建好Store
后,调用dispatch
传递的参数,如果所有的中间件都在c函数中调用下面这句话
next(action)
那么就会形成一个链式调用,所有的action
都会“流过”中间件,最后到达redux
调用reducers
,并触发更新订阅
这也意味着,只要有一个中间件中止流动,后面的所有行为都不会触发
3 写一个redux
接下来就自己写一个redux
,当然这个redux
是个”残废版“,只实现核心逻辑,具体的参数验证都不做了,具体代码托管到了git
3.1 createStore
如果你看过redux
的源码,其中大部分的代码都是在验证数据,那么createStore
的核心逻辑只有获取数据,更新数据,订阅数据变化的逻辑,40行就可以实现
function createStore(reducer) {
// 记录当前的state
let currentState
// 记录订阅的所有函数
let currentListeners = new Set()
const getState = () => currentState
const subscribe = (listener) => {
// 添加到散列表中
currentListeners.add(listener)
// 避免重复移除订阅
let isSubscribed = true
// 取消订阅
return function unsubscribe() {
if (!isSubscribed) return
isSubscribed = false
currentListeners.delete(listener)
}
}
const dispatch = (action) => {
// 获取新state
currentState = reducer(currentState, action)
// 触发订阅
for (let listener of currentListeners) {
listener()
}
}
// 初始化调用一次reducer,获取初始数据
dispatch({ type: null })
return { getState, subscribe, dispatch }
}
3.2 combineReducers
combineReducers
用于组装reducers
,那么只需要做一个遍历执行所有reducers
就能获取下一次状态了,在redux
中,做了一个浅比较,如果state
相对于上次没有发生变化的话,还返回上次的state
function combineReducers(reducers) {
return (state = {}, action) => {
// 准备一个标识,判断数据是否发生变化
let hasChanged = false
// 存储下一次状态
const nextState = {}
// 遍历执行所有reducer
for (let key in reducers) {
const reducer = reducers[key]
const previousStateForKey = state[key]
nextState[key] = reducer(previousStateForKey, action)
if(!hasChanged && nextState[key] !== previousStateForKey){
hasChanged = true
}
}
// 返回下一次状态
return hasChanged ? nextState : state
}
}
3.3 bindActionReducers
bindActionReducers
就更简单了,通过遍历就可以实现
function bindActionCreators(actions, dispatch) {
const cb = {}
for(let key in actions){
cb[key] = function () {
dispatch(actions[key](...arguments))
}
}
return cb
}
3.4 applyMiddleware
需要先在createStore
中把创建Store
的主动权交给appleMiddleware
function createStore(reducer, enhancer) {
// 交给中间件创建Store
if (enhancer) return enhancer(createStore, reducer)
...
}
接着applyMiddleware
按照中间件那节讲述的逻辑实现
function applyMiddleware(...middlewares) {
return function (createStore, reducer) {
const store = createStore(reducer)
const middlewareAPI = {
dispatch: () => { throw new Error('不要在初始化的时候调用dispatch') },
getState: store.getState
}
const chain = middlewares.map(f => f(middlewareAPI))
store.dispatch = compose(chain)(store.dispatch)
return store
}
}
function compose(funcs) {
if (!funcs.length) return f => f
if (funcs.length === 1) return funcs[0]
return funcs.reduce((a, b) => {
return function () {
return a(b(...arguments))
}
})
}
4 redux的问题
如果你一直使用redux
作为状态管理工具,那么想要增加一个状态的时候,必然会执行下面的步骤
- 定义一个不重复的
type
- 定义
action
类型 - 编写
Reducer
这其实是非常不直观的,直观的方法是直接定义一个变量,直接修改它就好了
let data = 1
data = 2
但这是做不到响应式,如果能做的到的话,尤雨溪就不要用各种骚操作去解决ref.value
的问题了,所以只能无限的逼近这个体验,我们现在来看看同为数据管理的mobx
是怎么解决这个问题的
import { makeAutoObservable, action, observable, autorun } from 'mobx'
class Store {
count = 0
constructor(){
makeAutoObservable(this)
}
increase() {
this.count++
}
}
const store = new Store()
autorun(() => {
console.log('updated')
})
store.increase()
// 控制台打印:updated
console.log(store.count) // 控制台打印:1
如果你没有学过 mobx
,看上面的代码,也能瞬间上手,不需要去管内部的实现逻辑,对象怎么用,它就怎么用
第一节就说过redux
的核心思想就是存数据,使用action
对象标记怎么更新数据,再由Reducer
来进行更改数据,描述成流程图如下
而在mobx
中,Action
是用来修改数据的方法, 就绕过了reduder
,借助Proxy
的能力,开发者可以直接通过Action
修改Store
上的数据
而redux
中修改数据这个操作全部由Reducer
完成,而Reducer
恰恰是心智负担最重的地方,这也是我觉得redux
难用的点