Skip to content

Commit 4ac6750

Browse files
committed
206
1 parent d2044c4 commit 4ac6750

2 files changed

Lines changed: 250 additions & 1 deletion

File tree

readme.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
前端界的好文精读,每周更新!
88

9-
最新精读:<a href="./前沿技术/205.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20with%20%E8%AF%AD%E6%B3%95%E3%80%8B.md">205.精读《JS with 语法》</a>
9+
最新精读:<a href="./前沿技术/206.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%80%E7%A7%8D%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88%E3%80%8B.md">206.精读《一种 Hooks 数据流管理方案》</a>
1010

1111
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
1212

@@ -163,6 +163,7 @@
163163
- <a href="./前沿技术/202.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%2018%E3%80%8B.md">202.精读《React 18》</a>
164164
- <a href="./前沿技术/204.%E7%B2%BE%E8%AF%BB%E3%80%8A%E9%BB%98%E8%AE%A4%E3%80%81%E5%91%BD%E5%90%8D%E5%AF%BC%E5%87%BA%E7%9A%84%E5%8C%BA%E5%88%AB%E3%80%8B.md">204.精读《默认、命名导出的区别》</a>
165165
- <a href="./前沿技术/205.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20with%20%E8%AF%AD%E6%B3%95%E3%80%8B.md">205.精读《JS with 语法》</a>
166+
- <a href="./前沿技术/206.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%80%E7%A7%8D%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88%E3%80%8B.md">206.精读《一种 Hooks 数据流管理方案》</a>
166167

167168
### 设计模式
168169

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
维护大型项目 OR UI 组件模块时,一定会遇到全局数据传递问题。
2+
3+
维护项目时,像全局用户信息、全局项目配置、全局功能配置等等,都是跨模块复用的全局数据。
4+
5+
维护 UI 组件时,调用组件的入口只有一个,但组件内部会继续拆模块,分文件,对于这些组件内模块而言,入口文件的参数也就是全局数据。
6+
7+
这时一般有三种方案:
8+
9+
1. props 透传。
10+
2. 上下文。
11+
3. 全局数据流。
12+
13+
props 透传方案,因为任何一个节点掉链子都会导致参数传递失败,因此带来的维护成本与心智负担都特别大。
14+
15+
上下文即 `useContext` 利用上下文共享全局数据,带来的问题是更新粒度太粗,同上下文中任何值的改变都会导致重渲染。有一种较为 Hack 的解决方案 [use-context-selector](https://github.com/dai-shi/use-context-selector),不过这个和下面说到的全局数据流很像。
16+
17+
全局数据流即利用 `react-redux` 等工具,绕过 React 更新机制进行全局数据传递的方案,这种方案较好解决了项目问题,但很少有组件会使用。以前也有过不少利用 Redux 做局部数据流的方案,但本质上还是全局数据流。现在 `react-redux` 支持了局部作用域方案:
18+
19+
```javascript
20+
import { shallowEqual, createSelectorHook, createStoreHook } from 'react-redux'
21+
22+
const context = React.createContext(null)
23+
const useStore = createStoreHook(context)
24+
const useSelector = createSelectorHook(context)
25+
const useDispatch = createDispatchHook(context)
26+
```
27+
28+
因此是机会好好梳理一下数据流管理方案,做一个项目、组件通用的数据流管理方案。
29+
30+
## 精读
31+
32+
对项目、组件来说,数据流包含两种数据:
33+
34+
1. 可变数据。
35+
2. 不可变数据。
36+
37+
对项目来说,可变数据的来源有:
38+
39+
1. 全局外部参数。
40+
2. 全局项目自定义变量。
41+
42+
不可变数据来源有:
43+
44+
1. 操作数据或行为的函数方法。
45+
46+
> 全局外部参数指不受项目代码控制的,比如登陆用户信息数据。全局项目自定义变量是由项目代码控制的,比如定义了一些模型数据、状态数据。
47+
48+
对组件来说,可变数据的来源有:
49+
50+
1. 组件被调用时的传参。
51+
2. 全局组件自定义变量。
52+
53+
不可变数据来源有:
54+
55+
1. 组件被调用时的传参。
56+
2. 操作数据或行为的函数方法。
57+
58+
对组件来说,被调用时的传参既可能是可变数据,也可能是不可变数据。比如传入的 `props.color` 可能就是可变数据,而 `props.defaultValue``props.onChange` 就是不可变数据。
59+
60+
当梳理清楚项目与组件到底有哪些全局数据后,我们就可以按照注册与调用这两步来设计数据流管理规范了。
61+
62+
### 数据流调用
63+
64+
首先来看调用。为了同时保证使用的便捷与应用程序的性能,我们希望使用一个统一的 API `useXXX` 来访问所有全局数据与方法,并满足:
65+
66+
1. `{} = useXXX()` 只能引用到不可变数据,包括变量与方法。
67+
2. `{ value } = useXXX(state => ({ value: state.value }))` 可以引用到可变数据,但必须通过选择器来调用。
68+
69+
比如一个应用叫 `gaea`,那么 `useGaea` 就是对这个应用全局数据的唯一调用入口,我可以在组件里这么调用数据与方法:
70+
71+
```typescript
72+
const Panel = () => {
73+
// appId 是应用不可变数据,所以即使是变量也可以直接获取,因为它不会变化,也不会导致重渲染
74+
// fetchData 是取数函数,内置发送了 appId,所以绑定了一定上下文,也属于不可变数据
75+
const { appId, fetchData } = useGaea()
76+
77+
// 主题色可能在运行时修改,只能通过选择器获取
78+
// 此时这个组件会额外在 color 变化时重渲染
79+
const { color } = useGaea(state => ({
80+
color: state.theme?.color
81+
}))
82+
}
83+
```
84+
85+
比如一个组件叫 `Menu`,那么 `useMenu` 就是这个组件的全局数据调用入口,可以这么使用:
86+
87+
```typescript
88+
// SubMenu 是 Menu 组件的子组件,可以直接使用 useMenu
89+
const SubMenu = () => {
90+
// defaultValue 是一次性值,所以处理时做了不可变处理,这里已经是不可变数据了
91+
// onMenuClick 是回调函数,不管传参引用如何变化,这里都处理成不可变的引用
92+
const { defaultValue, onMenuClick } = useMenu()
93+
94+
// disabled 是 menu 的参数,需要在变化时立即响应,所以是可变数据
95+
const { disabled } = useMenu(state => ({
96+
disabled: state.disabled
97+
}))
98+
99+
// selectedMenu 是 Menu 组件的内部状态,也作为可变数据调用
100+
const { selectedMenu } = useMenu(state => ({
101+
selectedMenu: state.selectedMenu
102+
}))
103+
}
104+
```
105+
106+
可以发现,在整个应用或者组件的使用 Scope 中,已经做了一层抽象,即不关心数据是怎么来的,只关心数据是否可变。这样对于组件或应用,随时可以将内部状态开放到 API 层,而内部代码完全不用修改。
107+
108+
### 数据流注册
109+
110+
数据流注册的时候,我们只要定义三种参数:
111+
112+
1. `dynamicValue`: 动态参数,通过 `useInput(state => state.xxx)` 才能访问到。
113+
2. `staticValue`: 静态参数,引用永远不会改变,可以直接通过 `useInput().xxx` 访问到。
114+
3. 自定义 hooks,入参是 `staticValue` `getState` `setState`,这里可以封装自定义方法,并且定义的方法都必须是静态的,可以直接通过 `useInput().xxx` 访问到。
115+
116+
```typescript
117+
const { useState: useInput, Provider } = createHookStore<{
118+
dynamicValue: {
119+
fontSize: number
120+
}
121+
staticValue: {
122+
onChange: (value: number) => void
123+
}
124+
}>(({ staticValue }) => {
125+
const onCustomChange = React.useCallback((value: number) => {
126+
staticValue.onChange(value + 1)
127+
}, [staticValue])
128+
129+
return React.useMemo(() => ({
130+
onCustomChange
131+
}), [onCustomChange])
132+
})
133+
```
134+
135+
上面的方法暴露了 `Provider``useInput` 两个对象,我们首先需要在组件里给它传输数据。比如我写的是组件 `Input`,就可以这么调用:
136+
137+
```jsx
138+
function Input({ onChange, fontSize }) {
139+
return (
140+
<Provider dynamicValue={{fontSize}} staticValue={{onChange}}>
141+
<InputComponent />
142+
</Provider>
143+
)
144+
}
145+
```
146+
147+
如果对于某些动态数据,我们只想赋初值,可以使用 `defaultDynamicValue`
148+
149+
```jsx
150+
function Input({ onChange, fontSize }) {
151+
return (
152+
<Provider dynamicValue={{fontSize}} defaultDynamicValue={{count: 1}}>
153+
<InputComponent />
154+
</Provider>
155+
)
156+
}
157+
```
158+
159+
这样 `count` 就是一个动态值,必须通过 `useInput(state => ({ count: state.count }))` 才能取到,但又不会因为外层组件 Rerender 而被重新赋值为 `1`。所有动态值都可以通过 `setState` 来修改,这个后面再说。
160+
161+
这样所有 Input 下的子组件就可以通过 `useInput` 访问到全局数据流的数据啦,我们有三种访问数据的场景。
162+
163+
一:访问传给 `Input` 组件的 `onChange`
164+
165+
因为 `onChange` 是不可变对象,因此可以通过如下方式访问:
166+
167+
```typescript
168+
function InputComponent() {
169+
const { onChange } = useInput()
170+
}
171+
```
172+
173+
二:访问我们自定义的全局 Hooks 函数 `onCustomChange`
174+
175+
```typescript
176+
function InputComponent() {
177+
const { onCustomChange } = useInput()
178+
}
179+
```
180+
181+
三:访问可能变化的数据 `fontSize`。由于我们需要在 `fontSize` 变化时让组件重渲染,又不想让上面两种调用方式受到 `fontSize` 的影响,需要通过如下方式访问:
182+
183+
```typescript
184+
function InputComponent() {
185+
const { fontSize } = useInput(state => ({
186+
fontSize: state.fontSize
187+
}))
188+
}
189+
```
190+
191+
最后在自定义方法中,如果我们想修改可变数据,都要通过 `updateStore` 封装好并暴露给外部,而不能直接调用。具体方式是这样的,举个例子,假设我们需要定义一个应用状态 `status`,其可选值为 `edit``preview`,那么可以这么去定义:
192+
193+
```jsx
194+
const { useState: useInput, Provider } = createHookStore<{
195+
dynamicValue: {
196+
isAdmin: boolean
197+
status: 'edit' | 'preview'
198+
}
199+
}>(({ getState, setState }) => {
200+
const toggleStatus = React.useCallback(() => {
201+
// 管理员才能切换应用状态
202+
if (!getState().isAdmin) {
203+
return
204+
}
205+
206+
setState(state => ({
207+
...state,
208+
status: state.status === 'edit' ? 'preview' : 'edit'
209+
}))
210+
}, [getState, setState])
211+
212+
return React.useMemo(() => ({
213+
toggleStatus
214+
}), [toggleStatus])
215+
})
216+
```
217+
218+
下面是调用:
219+
220+
```jsx
221+
function InputComponent() {
222+
const { toggleStatus } = useInput()
223+
224+
return (
225+
<button onClick={toggleStatus} />
226+
)
227+
}
228+
```
229+
230+
而且整个链路的类型定义也是完全自动推导的,这套数据流管理方案到这里就讲完了。
231+
232+
## 总结
233+
234+
对全局数据的使用,最方便的就是收拢到一个 `useXXX` API,并且还能区分静态、动态值,并在访问静态值时完全不会导致重渲染。
235+
236+
而之所以动态值 `dynamicValue` 需要在 `Provider` 里定义,是因为当动态值变化时,会自动更新数据流中的数据,使整个应用数据与外部动态数据同步。而这个更新步骤就是通过 Redux Store 来完成的。
237+
238+
本文特意没有给出实现源码,感兴趣的同学可以自己实现一个试一试。
239+
240+
> 讨论地址是:[精读《一种 Hooks 数据流管理方案》· Issue #345 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/345)
241+
242+
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
243+
244+
> 关注 **前端精读微信公众号**
245+
246+
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
247+
248+
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)

0 commit comments

Comments
 (0)