本来想写一篇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难用的点