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
}