redux

Posted by Youzi on July 30, 2021

redux 入门

前置概念:

  • 纯函数:同样的输入一定会得到同样的输出;
    • 不能改变参数;
    • 不能调用系统 I/O 的 api;
    • 不能调用Date.now | Math.random等不纯的方法,因为每次都可能得到不同的结果;
  • 中间件:指对封装好的函数进行二次封装改造的一个高阶函数;
    • 一般是为了添加一些新的功能,但又不破坏现有功能;
    • 接收一个函数,并返回一个带有新功能的新函数。

何时使用 redux

在多组件化开发的过程中,各组件间的通信,一直是个痛点,考虑以下场景:

  • 某个组件自身的state,需要共享给其他组件;
  • 某个state需要在任何地方都可以获取到;
  • 某个组件需要可以改变某些全局的状态;
  • 一个组件需要改变另一个组件的状态;

当项目中的组件层级很深,且包含了很多不同类型的组件,且出现上述的场景,那么我们就需要对组件内部的某些状态,做集中化的管理。

换句话说,如果应用没有那么复杂,其实可能就不需要用到 redux,甚至可以用localstorage来替代都行。

设计思想

  1. web 应用是一个状态机,视图与状态是一一对应的
  2. 所有的状态,都保存在一个对象里

原理

redux 里包含了 4 个主要的对象和函数;

  • 负责存储数据的仓库store,是一个对象,挂载了所有的状态,它本身不能被直接修改;
  • 负责标识要如何修改store对象的action,是一个对象,通常用来告诉reducer如何操作storeaction-creator负责批量生成action,是一个函数,专门用来生成action对象;
  • 负责分发actiondispatch函数,它接收一个action作为参数,在store对象内部分发一个动作,触发reducer去操作store
  • 负责真正操作store的函数reducer,它接收原来的state,和一个action对象,返回经过操作后的新的state

大致流程是,在组件里创建一个action对象(这一步可以通过手动创建普通对象,也可以通过action-creator函数创建),通过,是一个存放全局状态的对象,不能直接操作store对象来改变它的值,而是需要通过dispatch函数,来分发一个action,这个action标识了如何操作store对象,而真正操作store的,是reducer

store

一个 redux 实例中,只能存在一个store,所有状态都挂载在这一个store中;通常通过getState方法,从 store 中获取状态的最新快照;

1
2
3
4
5
import { createStore } from 'redux';

// 这个fn后面会讲到,是一个reducer函数
const store = createStore(fn);
const state = store.getState();

dispatch

dispatch 是一个在store实例上的函数,接收一个action,其内部自动调用了reducer函数;所以实际上我们在createStore的同时,就得传入reducer函数,不然在真正调用dispatch的时候,内部找不到reducer,就无从谈起执行更新store的操作了;

1
2
store.dispatch(action);
store.dispatch(increaseCreator(10));

subscribe

监听store中状态的变化,一旦其中状态发生变化,就会自动执行subscribe传入的回调函数;

为什么要有这个函数呢,因为对于redux来说,它只负责管理状态的改变,而不去关注状态改变后,还要进行什么操作(比如引起视图更新等等),所以它就暴露了一个接口,让调用者可以检测到状态更新,从而自定义的做出其他后续操作。

1
2
3
4
import { createStore } from 'redux';
const store = createStore(reducer);

store.subscribe(listener);

对应的也有取消监听的方法,上述subscribe函数的返回值,就是取消监听的方法:

1
2
let unsubscribe = store.subscribe(() => console.log(store.getState()));
unsubscribe();

如果我们希望在react应用中只用redux实现状态管理,我们可以用subscribe函数包裹render方法,当状态值发生改变时,会触发根组件再次调用ReactDOM.render方法,而且由于DOM diff算法的作用,不会对真实 DOM 进行大面积重绘,不会特别影响效率:

1
2
3
store.subscribe(() => {
  ReactDOM.render(<App />, document.getElementById('root'));
});

action / action creator

action是一个对象,一般形式为:

1
2
3
4
5
6
const action = {
  type: 'INCREASE_COUNT',
  payload: 1
};
// 分发一个action普通对象
store.dispatch(action);

action-creator是一个函数,一般接收一个payload作为参数,返回值是一个action

1
2
3
4
5
6
const increaseCreator = payload => ({
  type: 'INCREASE_COUNT',
  payload
});
// 分发一个通过`action-creator`创建的action
store.dispatch(increaseCreator(10));

reducer

reducer 是一个函数,其作用是初始化store中的状态,以及加工返回新的store状态,参数接收原来是store的一个快照,和需要执行的action,返回新的快照;

1
2
3
4
5
6
7
8
9
const defaultState = 0
const reducer = (state = defaultState, action) => {
  switch(action.type) {
    case 'INCREASE_COUNT':
      return state + action.payload
    default
      return state
  }
}

而在上面也提到过,一般应用中不需要手动调用reducer函数,可以直接调用store.dispatch来分发;所以我们在初始化store时,预先传入reducer函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createStore } from 'redux'

const defaultState = 0
const reducer = (state = defaultState, action) => {
  switch(action.type) {
    case 'INCREASE_COUNT':
      return state + action.payload
    default
      return state
  }
}
const store = createStore(reducer)
const state = store.getState()

另外值得注意的一点是,在store初始化时,也会调用一次reducer,此时传入的参数为:reducer(undefined, {type: '@@REDUX/INIT,xxx...'});传入的初始的stateundefined,初始的actiontype一般是形如@@REDUX/INIT后面加一些随机字符的字符串;

reducer注意事项

reducer规则:

  • 仅使用 stateaction 参数计算新的状态值
  • 禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 不可变更新(immutable updates)。
  • 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码

总的来说就是reducer应该是一个纯函数。

reducer中,不允许更改state原始对象

1
2
3
4
5
6
7
8
9
10
11
12
// 禁止这样更改state
const reducer = (state = defaultState, action) => {
  return state.value = 10
}

// 应该这样更改state
const reducer = (state, action) => {
  return {
    ...state,
    value:10
  }
}

这样操作会导致一些bug,比如UI没有正确更新,更难理解状态更新的原因和方式,编写测试用例更难了,打破了时间旅行调试的能力(时间旅行即意味着可以回溯之前的操作,也可以从当前操作节点向后退的能力);由于reducer的规则限制,需要开发者手动编写扩展运算符这样的代码,但这看起来很蠢,而且在对象嵌套层数比较多的时候就不好编写了,所以在redux/toolkit引入createSlice后,就可以直接改变state了,详情在后续的章节。

reducer拆分

类似Vuex的module,当state是一个大对象时,reducer需要进行合理的模块拆分;redux提供了一个combineReducers方法,用于对reducer的拆分,只需要定义各个子reducer函数,然后用这个方法就能合并成一个大的reducer

1
2
3
4
5
6
7
8
import { combineReducers } from 'redux';

const reducer = combineReducers({
  stateA: stateAReducer,
  stateB: stateBReducer
})

export default reducer;

在 redux 里使用中间件

由于 redux 本身有些功能缺失,所以 redux 库自身提供了applyMiddlewares方法,用于应用中间件;该方法接收多个中间件作为参数,目的是将所有传入的参数组成一个函数并依次执行。

对于一个中间件来说,给原有 redux 添加了功能后,新的功能作为API会接收dispatch, getState作为函数参数,看官方文档的说明:https://redux.js.org/understanding/thinking-in-redux/glossary#middleware

1
2
3
import { applyMiddlewares } from 'redux';
import thunk from 'redux-thunk';
createStore(reducer, applyMiddlewares(thunk));

thunk函数是用于提供redux异步创建action方法的中间件。

如何发送异步 action

前面介绍过action-creator只能创建一个action对象,而引入了redux-thunk中间件后,我们就可以通过action-creator返回一个函数,并传给store.dispatch作为参数,实现创建异步action的需求,官方文档:https://redux.js.org/understanding/thinking-in-redux/glossary#async-action

看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 普通的action-creator
const increaseCreator = payload => ({
  type: 'INCREASE_COUNT',
  payload
});
// 异步的action-creator,其内部会调用普通的action-creator
const asyncAction = payload => {
  return (dispatch, getState) => {
    setTimeout(() => {
      dispatch(increaseCreator(payload));
    }, 1000);
  };
};
store.dispatch(asyncAction(10));

这样就能实现通过action-creator返回一个参数,并提供给dispatch调用,如果不引入redux-thunk,则会报错,提示dispatch仅支持普通的action对象作为参数传入。

redux-promise

上面提到的redux-thunk中间件,可以实现action-creator返回函数,dispatch可以接受函数;而redux-promise可以使得action-creator返回两种情况:

  • 返回promise对象;
  • 返回对象,但对象的payload是一个promise对象。

第一种写法:

1
2
3
4
5
6
7
8
9
10
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';

const store = createStore(
  reducer,
  applyMiddleware(promiseMiddleware)
); 

const actionCreator = (dispatch)

redux/toolkit

在redux中文网里看到几个很实用的工具,reduxjs/toolkit

configureStore

configureStore类似combineReducers的一个工具函数,入参接受一个对象,需要有一个reducer对象,返回值是store对象,使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'

export default configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer,
    comments: commentsReducer
  }
})

createSlice

一个类似Vuex.module的方法createSlice,可以快速的创建store模块,该函数生成action.type字符串、action creator函数、action对象,使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer

createSlice接受一个对象,name属性作为action.type字符串的第一部分,每个reducer函数的key作为第二部分,组合起来就是{type: 'counter/increment'}createSlice返回一个对象,对象包含actions, reducer属性,可以直接通过slice.actions.xxx获取到对应的action,返回的reducer也可以直接用于combineReducers

上面的几个reducer看起来是不是有点问题,因为reducer应该是个纯函数;这是因为createSlice内部做了处理。

createSlice内部使用了Immer的JS库,这个库用了proxy来包装state数据,如果在reducer函数内部尝试修改传入的state时,immer库会跟踪所有的修改,然后返回一个新的安全的,不可变的更新值。神奇,这个库代替了手动返回新的state,就像这样,甚至都不用return

1
2
3
4
5
6
7
8
9
10
11
12
// 普通的reducer
function reducer(state,action) {
  return {
    ...state,
    test: '1'
  }
}

// 使用immer库的reducer
function reducer(state,action) {
  state.test = 1
}

参考文章