redux 入门
前置概念:
- 纯函数:同样的输入一定会得到同样的输出;
- 不能改变参数;
- 不能调用系统 I/O 的 api;
- 不能调用
Date.now | Math.random等不纯的方法,因为每次都可能得到不同的结果;
- 中间件:指对封装好的函数进行二次封装改造的一个高阶函数;
- 一般是为了添加一些新的功能,但又不破坏现有功能;
- 接收一个函数,并返回一个带有新功能的新函数。
何时使用 redux
在多组件化开发的过程中,各组件间的通信,一直是个痛点,考虑以下场景:
- 某个组件自身的
state,需要共享给其他组件; - 某个
state需要在任何地方都可以获取到; - 某个组件需要可以改变某些全局的状态;
- 一个组件需要改变另一个组件的状态;
当项目中的组件层级很深,且包含了很多不同类型的组件,且出现上述的场景,那么我们就需要对组件内部的某些状态,做集中化的管理。
换句话说,如果应用没有那么复杂,其实可能就不需要用到 redux,甚至可以用localstorage来替代都行。
设计思想
- web 应用是一个状态机,视图与状态是一一对应的
- 所有的状态,都保存在一个对象里
原理
redux 里包含了 4 个主要的对象和函数;
- 负责存储数据的仓库
store,是一个对象,挂载了所有的状态,它本身不能被直接修改; - 负责标识要如何修改
store对象的action,是一个对象,通常用来告诉reducer如何操作store;action-creator负责批量生成action,是一个函数,专门用来生成action对象; - 负责分发
action的dispatch函数,它接收一个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...'});传入的初始的state是undefined,初始的action的type一般是形如@@REDUX/INIT后面加一些随机字符的字符串;
reducer注意事项
reducer规则:
- 仅使用
state和action参数计算新的状态值 - 禁止直接修改
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
}