4-3、React 状态管理

状态管理:本质对一个[全局唯一、具有响应式]变量的管理
因为是全局的,那为了流转/使用上的不混乱/冲突等,所以会对其制定流转规则,让变化变得可预测。

Redux 基本原理(手撸简版)

createStore 发布订阅者模式

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

/**
* 创建 Redux store
*
* @param reducers 用于处理 action 的 reducer 函数
* @param initialValue 初始状态值
* @returns 返回一个包含 getState、dispatch 和 subscribe 方法的对象
*/
export function createStore(reducers, initialValue) {
// 初始化状态为初始值
let state = initialValue;

// 存储监听器的数组
const listeners = [];

// 获取当前状态的方法
const getState = () => state;

// 订阅监听器的函数
const subscribe = (fn) => {
// 将监听器添加到监听器数组中
listeners.push(fn);
};

// 派发动作的函数
const dispatch = (action) => {
// 使用reducers函数计算出新的状态
const nextState = reducers(state, action);

// 更新状态为新的状态
state = nextState;

// 遍历监听器数组,并调用每个监听器函数
listeners.forEach((fn) => fn());
};

// 返回一个包含getState、dispatch和subscribe的对象
return { getState, dispatch, subscribe };
}

combineReduces

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
/**
* 组合多个 reducer 函数
*
* @param reducers 一个包含多个 reducer 函数的对象
* @returns 返回一个 reducer 函数,将多个 reducer 函数组合在一起
*/
export function combineReducers(reducers) {
// 获取所有reducer的键名
const keys = Object.keys(reducers);

// 返回一个新的reducer函数
return (state, action) => {
// 创建一个新的状态对象
const nextState = {};

// 遍历所有reducer的键名
keys.forEach((key) => {
// 获取对应键名的reducer函数
const reducer = reducers[key];

// 获取当前状态中对应键名的值
const prve = state[key];

// 调用reducer函数,传入当前值和action,得到下一个状态的值
const next = reducer(prve, action);

// 将下一个状态的值添加到新的状态对象中
nextState[key] = next;
});

// 返回新的状态对象
return nextState;
};
}

connect

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
/**
* 连接函数,用于将组件与 Redux store 连接起来
*
* @param mapStateToProps 将 Redux store 中的 state 映射到组件的 props
* @param mapDispatchToProps 将 Redux store 中的 dispatch 方法映射到组件的 props
* @returns 返回一个高阶组件,该高阶组件将传入的组件与 Redux store 连接起来
*/
export function connect(mapStateToProps, mapDispatchToProps) {
// 返回一个函数,该函数接收一个组件作为参数
return function (Component) {
// 返回一个函数,该函数接收props作为参数
return function (props) {
// 使用useContext钩子获取Redux的store
const store = useContext(ReduxContext);
// 使用useState钩子创建一个状态变量,并初始化为false
const [, setBool] = useState(false);

// 定义一个forceUpdate函数,用于强制更新组件
const forceUpdate = () => setBool((val) => !val);

// 在组件挂载后,将forceUpdate函数作为监听器订阅store的更新
useEffect(() => store.subscribe(forceUpdate), []);

// 返回一个JSX元素,该元素渲染传入的组件,并传入props和绑定的state和dispatch
return (
<Component
{...props}
{...mapStateToProps(store.getState())}
{...mapDispatchToProps(store.dispatch)}
/>
);
};
};
}

完整手写(可运行的)

src/store/redux.js

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { useEffect } from "react";
import { createContext, useContext, useState } from "react";

/**
* 创建 Redux store
*
* @param reducers 用于处理 action 的 reducer 函数
* @param initialValue 初始状态值
* @returns 返回一个包含 getState、dispatch 和 subscribe 方法的对象
*/
export function createStore(reducers, initialValue) {
// 初始化状态为初始值
let state = initialValue;

// 存储监听器的数组
const listeners = [];

// 获取当前状态的方法
const getState = () => state;

// 订阅监听器的函数
const subscribe = (fn) => {
// 将监听器添加到监听器数组中
listeners.push(fn);
};

// 派发动作的函数
const dispatch = (action) => {
// 使用reducers函数计算出新的状态
const nextState = reducers(state, action);

// 更新状态为新的状态
state = nextState;

// 遍历监听器数组,并调用每个监听器函数
listeners.forEach((fn) => fn());
};

// 返回一个包含getState、dispatch和subscribe的对象
return { getState, dispatch, subscribe };
}

/**
* 组合多个 reducer 函数
*
* @param reducers 一个包含多个 reducer 函数的对象
* @returns 返回一个 reducer 函数,将多个 reducer 函数组合在一起
*/
export function combineReducers(reducers) {
// 获取所有reducer的键名
const keys = Object.keys(reducers);

// 返回一个新的reducer函数
return (state, action) => {
// 创建一个新的状态对象
const nextState = {};

// 遍历所有reducer的键名
keys.forEach((key) => {
// 获取对应键名的reducer函数
const reducer = reducers[key];

// 获取对应键名的当前状态值
const prve = state[key];

// 调用reducer函数,传入当前值和action,得到下一个状态的值
const next = reducer(prve, action);

// 将下一个状态的值添加到新的状态对象中
nextState[key] = next;
});

// 返回新的状态对象
return nextState;
};
}

export const ReduxContext = createContext();

/**
* 连接函数,用于将组件与 Redux store 连接起来
*
* @param mapStateToProps 将 Redux store 中的 state 映射到组件的 props
* @param mapDispatchToProps 将 Redux store 中的 dispatch 方法映射到组件的 props
* @returns 返回一个高阶组件,该高阶组件将传入的组件与 Redux store 连接起来
*/
export function connect(mapStateToProps, mapDispatchToProps) {
// 返回一个函数,该函数接收一个组件作为参数
return function (Component) {
// 返回一个函数,该函数接收props作为参数
return function (props) {
// 使用useContext钩子获取Redux的store
const store = useContext(ReduxContext);
// 使用useState钩子创建一个状态变量,并初始化为false
const [, setBool] = useState(false);

// 定义一个forceUpdate函数,用于强制更新组件
const forceUpdate = () => setBool((val) => !val);

// 在组件挂载后,将forceUpdate函数作为监听器订阅store的更新
useEffect(() => store.subscribe(forceUpdate), []);

// 返回一个JSX元素,该元素渲染传入的组件,并传入props和绑定的state和dispatch
return (
<Component
{...props}
{...mapStateToProps(store.getState())}
{...mapDispatchToProps(store.dispatch)}
/>
);
};
};
}

src/store/index.js

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
import { createStore, combineReducers, connect } from "./redux";

function countReducers(count, action) {
switch (action.type) {
case "addCount":
return count + 1;
default:
return count;
}
}

function infoReducers(info, action) {
switch (action.type) {
case "addAge":
return { ...info, age: info.age + 1 };
default:
return info;
}
}

const initialValue = {
count: 23,
info: {
name: "张三",
age: 27,
},
};

const reducers = combineReducers({ count: countReducers, info: infoReducers });

export const store = createStore(reducers, initialValue);

function mapStateToProps(state) {
return { count: state.count, info: state.info };
}

function mapDispatchToProps(dispatch) {
return {
addCount() {
dispatch({ type: "addCount" });
},
addAge() {
dispatch({ type: "addAge" });
},
};
}

export const connected = connect(mapStateToProps, mapDispatchToProps);

某个 .jsx 文件内

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 from "react";
import { store, connected } from "../../store";
import { ReduxContext } from "../../store/redux";

const Child = connected(({ count, info, addCount, addAge }) => {
return (
<div>
I am Child
<div>
<span>count:{count}</span>
<button onClick={addCount}>addCount</button>
</div>
<div>
<span>
info:{info.name},{info.age}
<button onClick={addAge}>addAge</button>
</span>
</div>
</div>
);
});

const Parent = () => (
<div>
I am Parent
<Child />
</div>
);

export default function Store() {
return (
<ReduxContext.Provider value={store}>
<Parent />
</ReduxContext.Provider>
);
}

Mobx 基本原理(手撸简版)

可以发现跟Vue的响应式有点相似,采用的是观察者模式

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
// 具体实现:

let effect = null;
const deps = [];

function handle() {
// 返回一个对象,包含两个方法:get 和 set
return {
// get 方法用于获取目标对象的属性值
get(target, key, desc) {
// 如果存在 effect,则将其添加到 deps 数组中
if (effect) deps.push(effect);

// 调用 Reflect.get 方法获取目标对象的属性值
return Reflect.get(target, key, desc);
},
// set 方法用于设置目标对象的属性值
set(target, value, key, desc) {
// 调用 Reflect.set 方法设置目标对象的属性值
Reflect.set(target, value, key, desc);

// 遍历 deps 数组,依次执行其中的函数
deps.forEach((dep) => dep());
},
};
}
/**
* 遍历数据,递归处理对象和数组中的值
*
* @param data 要遍历的数据
* @returns 返回处理后的数据
*/
function walk(data) {
// 如果数据为空或者数据类型不是对象,则直接返回数据
if (data === null || typeof data !== "object") return data;

// 遍历对象的所有键值对
Object.entries(data).forEach(([key, value]) => {
// 递归调用 walk 函数,处理每个键值对的值,并将处理后的值重新赋值给对应的键
data[key] = walk(value);
});

// 使用 Proxy 对象包装数据,并调用 handle 函数处理代理对象
return new Proxy(data, handle());
}

/**
* 将给定的数据转换为可观察对象
*
* @param data 要转换的数据
* @returns 返回可观察对象
*/
function observable(data) {
// 调用 walk 函数处理 data 参数,并返回处理结果
return walk(data);
}

/**
* 自动运行函数
*
* @param _effect 需要运行的函数
*/
function autorun(_effect) {
// 将传入的参数_effect赋值给全局变量effect
effect = _effect;

// 调用全局变量effect对应的函数
effect();

// 将全局变量effect置为null
effect = null;
}

// 具体使用:
const data = { count: 1 };

const store = observable(data);

autorun(() => {
console.log("autorun store.count:", store.count);
});

store.count = 2; // 自动执行一次 autorun
store.count = 3; // 自动执行一次 autorun

补充知识

Redux 中间件的 compose 原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// compose 函数接收多个函数作为参数,并通过 reduce 方法将这些函数组合起来
function compose(...funcs) {
return funcs.reduce((a, b) => {
// a 和 b 分别代表当前正在组合的两个函数,reduce 的每次迭代中,
// a 是之前已经组合好的函数,
// b 是当前需要与之组合的新函数

// 返回一个新的函数,该函数接受一组参数 args,
// 首先应用 b 函数处理 args,然后将结果传递给 a 函数继续处理
return (...args) => a(b(args));
});
}

// 使用 compose 函数时,传入一系列函数 fn1、fn2、fn3
const composedFn = compose(fn1, fn2, fn3);

// 当调用这个组合后的函数时,它会按从右到左的顺序执行函数
const result = composedFn(args);

// 这样 composedFn(args) 的效果就等同于下面的嵌套调用
// fn1(fn2(fn3(args)))

4-3、React 状态管理
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/4-3、React 状态管理/
作者
黄智强
发布于
2024年1月13日
许可协议