redux 基本概念
Store
人如其名,Store就是一个仓库,它存储了所有的状态(State),还提供了一些操作他的API,我们后续的操作其实都是在操作这个仓库。假如我们的仓库是用来放牛奶的,初始情况下,我们的仓库里面一箱牛奶都没有。那Store的状态(State)就是:
{
milk: 0
}
Actions
一个Action就是一个动作,这个动作的目的是更改Store中的某个状态,Store还是上面的那个仓库,现在我想往仓库放一箱牛奶,那”我想往仓库放一箱牛奶”就是一个Action。代码就是这样:
{
type: "PUT_MILK",
count: 1
}
Reducers
前面”我想往仓库放一箱牛奶”只是想了,还没操作,具体操作要靠Reducer,Reducer就是根据接收的Action来改变Store中的状态,比如我接收了一个PUT_MILK,同时数量count是1,那放进去的结果就是milk增加了1,从0变成了1,代码就是这样:
1
2
3
4
5
6
7
8
9
10
11
12
const initState = {
milk: 0
}
function reducer(state = initState, action) {
switch (action.type) {
case 'PUT_MILK':
return {...state, milk: state.milk + action.count}
default:
return state
}
}
可以看到Redux本身就是一个单纯的状态机,Store存放了所有的状态,Action是一个改变状态的通知,Reducer接收到通知就更改Store中对应的状态。
通过手写redux理解redux
API:createStore
这里我们给出一个使用redux的例子 在后面我们替换为自己手写的redux
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { createStore } from 'redux';
const initState = {
milk: 0
};
function reducer(state = initState, action) {
switch (action.type) {
case 'PUT_MILK':
return {...state, milk: state.milk + action.count};
case 'TAKE_MILK':
return {...state, milk: state.milk - action.count};
default:
return state;
}
}
let store = createStore(reducer);
// subscribe其实就是订阅store的变化,一旦store发生了变化,传入的回调函数就会被调用
// 如果是结合页面更新,更新的操作就是在这里执行
store.subscribe(() => console.log(store.getState()));
// 将action发出去要用dispatch
store.dispatch({ type: 'PUT_MILK' }); // milk: 1
store.dispatch({ type: 'PUT_MILK' }); // milk: 2
store.dispatch({ type: 'TAKE_MILK' }); // milk: 1
在上文的例子中 我们只用到了一个API
createStore:这个API接受reducer方法作为参数,返回一个store,主要功能都在这个store上。
看看store上我们都用到了啥:
store.subscribe: 订阅state的变化,当state变化的时候执行回调,可以有多个subscribe,里面的回调会依次执行。
store.dispatch: 发出action的方法,每次dispatch action都会执行reducer生成新的state,然后执行subscribe注册的回调。
store.getState:一个简单的方法,返回当前的state。
看到subscribe注册回调,dispatch触发回调. 这就是典型的发布订阅模式.后面聊聊发布订阅模式.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function createStore() {
let state; // state记录所有状态
let listeners = []; // 保存所有注册的回调
function subscribe(callback) {
listeners.push(callback); // subscribe就是将回调保存下来
}
// dispatch就是将所有的回调拿出来依次执行就行
function dispatch(action) {
/**
* reducer的作用是在发布事件的时候改变state,
* 所以我们的dispatch在执行回调前应该先执行reducer,
* 用reducer的返回值重新给state赋值
*/
state = reducer(state, action);
// 改变状态后执行订阅的callback
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
// getState直接返回state
function getState() {
return state;
}
// store包装一下前面的方法直接返回
const store = {
subscribe,
dispatch,
getState
}
return store;
}
API:combineReducers
combineReducers也是使用非常广泛的API,当我们应用越来越复杂,如果将所有逻辑都写在一个reducer里面,最终这个文件可能会有成千上万行,所以Redux提供了combineReducers,可以让我们为不同的模块写自己的reducer,最终将他们组合起来。比如我们最开始那个牛奶仓库,由于我们的业务发展很好,我们又增加了一个放大米的仓库,我们可以为这两个仓库创建自己的reducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { createStore, combineReducers } from 'redux';
const initMilkState = {
milk: 0
};
function milkReducer(state = initMilkState, action) {
switch (action.type) {
case 'PUT_MILK':
return {...state, milk: state.milk + action.count};
case 'TAKE_MILK':
return {...state, milk: state.milk - action.count};
default:
return state;
}
}
const initRiceState = {
rice: 0
};
function riceReducer(state = initRiceState, action) {
switch (action.type) {
case 'PUT_RICE':
return {...state, rice: state.rice + action.count};
case 'TAKE_RICE':
return {...state, rice: state.rice - action.count};
default:
return state;
}
}
// 使用combineReducers组合两个reducer
const reducer = combineReducers({milkState: milkReducer, riceState: riceReducer});
let store = createStore(reducer);
store.subscribe(() => console.log(store.getState()));
// 操作🥛的action
store.dispatch({ type: 'PUT_MILK', count: 1 }); // milk: 1
store.dispatch({ type: 'PUT_MILK', count: 1 }); // milk: 2
store.dispatch({ type: 'TAKE_MILK', count: 1 }); // milk: 1
// 操作大米的action
store.dispatch({ type: 'PUT_RICE', count: 1 }); // rice: 1
store.dispatch({ type: 'PUT_RICE', count: 1 }); // rice: 2
store.dispatch({ type: 'TAKE_RICE', count: 1 }); // rice: 1
在上面 我们新建了两个state,通过combineReducers接收这两个reducer并返回一个新的reducer。让我们尝试实现一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function combineReducers(reducerMap) {
const reducerKeys = Object.keys(reducerMap); // 先把参数里面所有的键值拿出来
// 返回值是一个普通结构的reducer函数
const reducer = (state = {}, action) => {
const newState = {};
for(let i = 0; i < reducerKeys.length; i++) {
// reducerMap里面每个键的值都是一个reducer,我们把它拿出来运行下就可以得到对应键新的state值
// 然后将所有reducer返回的state按照参数里面的key组装好
// 最后再返回组装好的newState就行
const key = reducerKeys[i];
const currentReducer = reducerMap[key];
const prevState = state[key];
newState[key] = currentReducer(prevState, action);
}
return newState;
};
return reducer;
}
API:applyMiddleware
middleware是Redux里面很重要的一个概念,Redux的生态主要靠这个API接入,比如我们想写一个logger的中间件可以这样写(这个中间件来自于官方文档):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// logger是一个中间件,注意返回值嵌了好几层函数
// 我们后面来看看为什么这么设计
function logger(store) {
return function(next) {
return function(action) {
console.group(action.type);
console.info('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
console.groupEnd();
return result
}
}
}
// 在createStore的时候将applyMiddleware作为第二个参数传进去
const store = createStore(
reducer,
applyMiddleware(logger)
)
在这里createStore传入了第二个参数 官方管这个叫做 enhancer ,enhancer作为一个函数接收一个storeCreater并返回一个storeCreater。
让我们更改一下我们的createStore使之可以处理第二个参数 也就是enhancer。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function createStore(reducer, enhancer) { // 接收第二个参数enhancer
// 先处理enhancer
// 如果enhancer存在并且是函数
// 我们将createStore作为参数传给他
// 他应该返回一个新的createStore给我
// 我再拿这个新的createStore执行,应该得到一个store
// 直接返回这个store就行
if(enhancer && typeof enhancer === 'function'){
const newCreateStore = enhancer(createStore);
const newStore = newCreateStore(reducer);
return newStore;
}
// 如果没有enhancer或者enhancer不是函数,直接执行之前的逻辑
// 下面这些代码都是之前那版
// 省略n行代码
// .......
const store = {
subscribe,
dispatch,
getState
}
return store;
}
最后我们再来梳理下Redux的核心流程,注意单纯的Redux只是个状态机,是没有View层的。
redux,react-redux
单纯的Redux只是一个状态机,是没有UI呈现的,所以一般我们使用的时候都会配合一个UI库,比如在React中使用Redux就会用到React-Redux这个库。这个库的作用是将Redux的状态机和React的UI呈现绑定在一起,当你dispatch action改变state的时候,会自动更新页面。本文还是从它的基本使用入手来自己写一个React-Redux,然后替换官方的NPM库,并保持功能一致。
react-redux 的用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement, reset } from './actions';
function Counter(props) {
const {
count,
incrementHandler,
decrementHandler,
resetHandler
} = props;
return (
<>
<h3>Count: {count}</h3>
<button onClick={incrementHandler}>计数+1</button>
<button onClick={decrementHandler}>计数-1</button>
<button onClick={resetHandler}>重置</button>
</>
);
}
const mapStateToProps = (state) => {
return {
count: state.count
}
}
const mapDispatchToProps = (dispatch) => {
return {
incrementHandler: () => dispatch(increment()),
decrementHandler: () => dispatch(decrement()),
resetHandler: () => dispatch(reset()),
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
react-redux 核心api:
-
Provider: 用来包裹根组件的组件,作用是注入Redux的store。
-
connect:用来将state和dispatch注入给需要的组件,返回一个新组件,他其实是个高阶组件。
React-Redux的Provider其实就是包装了Context.Provider,而传递的参数就是redux store,而React-Redux的connectHOC其实就是包装的Context.Consumer或者useContext
假如没有这两个API,只用Redux可以吗?当然是可以的!其实我们用Redux的目的不就是希望用它将整个应用的状态都保存下来,每次操作只用dispatch action去更新状态,然后UI就自动更新了吗?那我从根组件开始,每一级都把store传下去不就行了吗?每个子组件需要读取状态的时候,直接用store.getState()就行了,更新状态的时候就store.dispatch,这样其实也能达到目的。但是,不优雅。
而state是组件中可自行管理的状态,这意味着React并没有让数据回溯的能力,数据只能单向向下分发,或者自行内部处理.
所以最好有个东西能够将store全局的注入组件树,而不需要一层层作为props传递,这个东西就是Provider.
Provider
Context.js
1
2
3
4
5
import React from 'react';
const ReactReduxContext = React.createContext();
export default ReactReduxContext;
Provider.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import ReactReduxContext from './Context';
function Provider(props) {
const {store, children} = props;
// 这是要传递的context
const contextValue = { store };
// 返回ReactReduxContext包裹的组件,传入contextValue
// 里面的内容就直接是children,我们不动他
return (
<ReactReduxContext.Provider value={contextValue}>
{children}
</ReactReduxContext.Provider>
)
}
connect
connenct并不会改变它“连接”的组件,而是提供一个经过包裹的connect组件。 conenct接受4个参数,分别是mapStateToProps,mapDispatchToProps,mergeProps,options(使用时注意参数位置顺序)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React, { useContext } from 'react';
import ReactReduxContext from './Context';
function childPropsSelector(store, wrapperProps) {
const state = store.getState(); // 拿到state
// 执行mapStateToProps和mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
return Object.assign({}, stateProps, dispatchProps, wrapperProps);
}
// 第一层函数接收mapStateToProps和mapDispatchToProps
function connect(mapStateToProps, mapDispatchToProps) {
// 第二层函数是个高阶组件,里面获取context
// 然后执行mapStateToProps和mapDispatchToProps
// 再将这个结果组合用户的参数作为最终参数渲染WrappedComponent
// WrappedComponent就是我们使用connext包裹的自己的组件
return function connectHOC(WrappedComponent) {
function ConnectFunction(props) {
// 复制一份props到wrapperProps
const { ...wrapperProps } = props;
const context = useContext(ReactReduxContext);
const { store } = context;
const actualChildProps = childPropsSelector(store, wrapperProps)
// 渲染WrappedComponent
return <WrappedComponent {...actualChildProps}></WrappedComponent>
}
return ConnectFunction;
}
}
export default connect;
这个时候已经可以渲染页面了 但是当我们更新store 页面还不能自动刷新 所以我们需要对store注册回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
store.subscribe(() => {
const newChildProps = childPropsSelector(store, wrapperProps);
// 如果参数变了,记录新的值到lastChildProps上
// 并且强制更新当前组件
// shallowEqual 浅比较
if(!shallowEqual(newChildProps, lastChildProps.current)) {
lastChildProps.current = newChildProps;
// 需要一个API来强制更新当前组件
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
mport React, { useContext, useRef, useLayoutEffect, useReducer } from 'react';
import ReactReduxContext from './Context';
import shallowEqual from './shallowEqual';
import Subscription from './Subscription';
function storeStateUpdatesReducer(count) {
return count + 1;
}
function connect(
mapStateToProps = () => {},
mapDispatchToProps = () => {}
) {
function childPropsSelector(store, wrapperProps) {
const state = store.getState(); // 拿到state
// 执行mapStateToProps和mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
return Object.assign({}, stateProps, dispatchProps, wrapperProps);
}
return function connectHOC(WrappedComponent) {
function ConnectFunction(props) {
const { ...wrapperProps } = props;
const contextValue = useContext(ReactReduxContext);
const { store, subscription: parentSub } = contextValue; // 解构出store和parentSub
const actualChildProps = childPropsSelector(store, wrapperProps);
const lastChildProps = useRef();
useLayoutEffect(() => {
lastChildProps.current = actualChildProps;
}, [actualChildProps]);
const [
,
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, 0)
// 新建一个subscription实例
const subscription = new Subscription(store, parentSub);
// state回调抽出来成为一个方法
const checkForUpdates = () => {
const newChildProps = childPropsSelector(store, wrapperProps);
// 如果参数变了,记录新的值到lastChildProps上
// 并且强制更新当前组件
if(!shallowEqual(newChildProps, lastChildProps.current)) {
lastChildProps.current = newChildProps;
// 需要一个API来强制更新当前组件
forceComponentUpdateDispatch();
// 然后通知子级更新
subscription.notifyNestedSubs();
}
};
// 使用subscription注册回调
subscription.onStateChange = checkForUpdates;
subscription.trySubscribe();
// 修改传给子级的context
// 将subscription替换为自己的
const overriddenContextValue = {
...contextValue,
subscription
}
// 渲染WrappedComponent
// 再次使用ReactReduxContext包裹,传入修改过的context
return (
<ReactReduxContext.Provider value={overriddenContextValue}>
<WrappedComponent {...actualChildProps} />
</ReactReduxContext.Provider>
)
}
return ConnectFunction;
}
}
export default connect;
源码中还有很多代码 大部分是使用Subscription类自己管理了一套通知流程,保证父子更新顺序的. 这里先略过不讲了….
总结:
- React-Redux是连接React和Redux的库,同时使用了React和Redux的API。
- React-Redux主要是使用了React的context api来传递Redux的store。
- Provider的作用是接收Redux store并将它放到context上传递下去。
- connect的作用是从Redux store中选取需要的属性传递给包裹的组件。
- connect会自己判断是否需要更新,判断的依据是需要的state是否已经变化了。
- connect在判断是否变化的时候使用的是浅比较,也就是只比较一层,所以在mapStateToProps和mapDispatchToProps中不要反回多层嵌套的对象。
- 为了解决父组件和子组件各自独立依赖Redux,破坏了React的父级->子级的更新流程,React-Redux使用Subscription类自己管理了一套通知流程。
- 只有连接到Redux最顶级的组件才会直接注册到Redux store,其他子组件都会注册到最近父组件的subscription实例上。
- 通知的时候从根组件开始依次通知自己的子组件,子组件接收到通知的时候,先更新自己再通知自己的子组件。
发布订阅模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
var Event = (function() {
var clientList = {};
listen,
trigger,
remove;
listen = function(key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
};
trigger = function() {
var key = Array.prototype.shift.call(arguments),
fns = clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
};
remove = function(key, fn) {
var fns = clientList[key];
if (!fns) {
return false;
}
if (!fn) {
fns && (fns.length = 0);
} else {
for (var l = fns.length - 1; l >= 0; l--) {
var _fn = fns[l];
if (_fn === fn) {
fns.splice(l, 1);
}
}
}
};
return {
listen,
trigger,
remove,
}
})();
Event.listen('squareMeter88', function(price) {
console.log(`价格${price}`);
})
Event.trigger('squareMeter88', 20000);