Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84ed47dbdf | ||
|
|
252980724a | ||
|
|
225aab476d | ||
|
|
f065539266 | ||
|
|
f8d81fc3f5 | ||
|
|
43e264cc07 | ||
|
|
957737959f | ||
|
|
000e6511e6 | ||
|
|
a652284bbc | ||
|
|
1b88e5ac06 | ||
|
|
79ba9ec6a1 | ||
|
|
a4acf09d8b | ||
|
|
872ef2e346 | ||
|
|
8795ae0aaa | ||
|
|
23c4190d7e | ||
|
|
75d0109102 | ||
|
|
893bb2d97a | ||
|
|
0c2c8c6c03 | ||
|
|
d8f2e0977d | ||
|
|
f4e75f5e21 |
@@ -48,7 +48,7 @@ function App() {
|
||||
|
||||
### <Route> 升级
|
||||
|
||||
在 v5 版本立,想要给组件传参数是不太直观的,需要利用 RenderProps 的方式透传 `routeProps`:
|
||||
在 v5 版本里,想要给组件传参数是不太直观的,需要利用 RenderProps 的方式透传 `routeProps`:
|
||||
|
||||
```jsx
|
||||
import Profile from './Profile';
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
## 1 引言
|
||||
|
||||
React Hooks 渐渐被国内前端团队所接受,但基于 Hooks 的数据流方案却还未固定,我们有 “100 种” 类似的选择,却各有利弊,让人难以取舍。
|
||||
|
||||
本周笔者就深入谈一谈对 Hooks 数据流的理解,相信读完文章后,可以从百花齐放的 Hooks 数据流方案中看到本质。
|
||||
|
||||
## 2 精读
|
||||
|
||||
基于 React Hooks 谈数据流,我们先从最不容易产生分歧的基础方案说起。
|
||||
|
||||
### 单组件数据流
|
||||
|
||||
单组件最简单的数据流一定是 `useState`:
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
const [count, setCount] = useState();
|
||||
}
|
||||
```
|
||||
|
||||
`useState` 在组件内用是毫无争议的,那么下个话题就一定是跨组件共享数据流了。
|
||||
|
||||
### 组件间共享数据流
|
||||
|
||||
跨组件最简单的方案就是 `useContext`:
|
||||
|
||||
```jsx
|
||||
const CountContext = createContext();
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState();
|
||||
return (
|
||||
<CountContext.Provider value={{ count, setCount }}>
|
||||
<Child />
|
||||
</CountContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Child() {
|
||||
const { count } = useContext(CountContext);
|
||||
}
|
||||
```
|
||||
|
||||
用法都是官方 API,显然也是毫无争议的,但问题是数据与 UI 不解耦,这个问题 [unstated-next](https://github.com/jamiebuilds/unstated-next) 已经为你想好解决方案了。
|
||||
|
||||
### 数据流与组件解耦
|
||||
|
||||
[unstated-next](https://github.com/jamiebuilds/unstated-next) 可以帮你把上面例子中,定义在 `App` 中的数据单独出来,形成一个自定义数据管理 Hook:
|
||||
|
||||
```jsx
|
||||
import { createContainer } from "unstated-next";
|
||||
|
||||
function useCounter() {
|
||||
const [count, setCount] = useState();
|
||||
return { count, setCount };
|
||||
}
|
||||
|
||||
const Counter = createContainer(useCounter);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Counter.Provider>
|
||||
<Child />
|
||||
</Counter.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Child() {
|
||||
const { count } = Counter.useContainer();
|
||||
}
|
||||
```
|
||||
|
||||
数据与 `App` 就解耦了,这下 `Counter` 再也不和 `App` 绑定了,`Counter` 可以和其他组件绑定作用了。
|
||||
|
||||
这个时候性能问题就慢慢浮出了水面,首当其冲的就是 `useState` 无法合并更新的问题,我们自然想到利用 `useReducer` 解决。
|
||||
|
||||
### 合并更新
|
||||
|
||||
`useReducer` 可以让数据合并更新,这也是 React 官方 API,毫无争议:
|
||||
|
||||
```jsx
|
||||
import { createContainer } from "unstated-next";
|
||||
|
||||
function useCounter() {
|
||||
const [state, dispath] = useReducer(
|
||||
(state, action) => {
|
||||
switch (action.type) {
|
||||
case "setCount":
|
||||
return {
|
||||
...state,
|
||||
count: action.setCount(state.count),
|
||||
};
|
||||
case "setFoo":
|
||||
return {
|
||||
...state,
|
||||
foo: action.setFoo(state.foo),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
return state;
|
||||
},
|
||||
{ count: 0, foo: 0 }
|
||||
);
|
||||
|
||||
return { ...state, dispatch };
|
||||
}
|
||||
|
||||
const Counter = createContainer(useCounter);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Counter.Provider>
|
||||
<Child />
|
||||
</Counter.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Child() {
|
||||
const { count } = Counter.useContainer();
|
||||
}
|
||||
```
|
||||
|
||||
这下即便要同时更新 `count` 和 `foo`,我们也能通过抽象成一个 `reducer` 的方式合并更新。
|
||||
|
||||
然而还有性能问题:
|
||||
|
||||
```jsx
|
||||
function ChildCount() {
|
||||
const { count } = Counter.useContainer();
|
||||
}
|
||||
|
||||
function ChildFoo() {
|
||||
const { foo } = Counter.useContainer();
|
||||
}
|
||||
```
|
||||
|
||||
更新 `foo` 时,`ChildCount` 和 `ChildFoo` 同时会执行,但 `ChildCount` 没用到 `foo` 呀?这个原因是 `Counter.useContainer` 提供的数据流是一个引用整体,其子节点 `foo` 引用变化后会导致整个 Hook 重新执行,继而所有引用它的组件也会重新渲染。
|
||||
|
||||
此时我们发现可以利用 Redux `useSelector` 实现按需更新。
|
||||
|
||||
### 按需更新
|
||||
|
||||
首先我们利用 Redux 对数据流做一次改造:
|
||||
|
||||
```jsx
|
||||
import { createStore } from "redux";
|
||||
import { Provider, useSelector } from "react-redux";
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case "setCount":
|
||||
return {
|
||||
...state,
|
||||
count: action.setCount(state.count),
|
||||
};
|
||||
case "setFoo":
|
||||
return {
|
||||
...state,
|
||||
foo: action.setFoo(state.foo),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Child />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Child() {
|
||||
const { count } = useSelector(
|
||||
(state) => ({ count: state.count }),
|
||||
shallowEqual
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`useSelector` 可以让 `Child` 在 `count` 变化时才更新,而 `foo` 变化时不更新,这已经接近较为理想的性能目标了。
|
||||
|
||||
但 `useSelector` 的作用仅仅是计算结果不变化时阻止组件刷新,但并不能保证返回结果的引用不变化。
|
||||
|
||||
### 防止数据引用频繁变化
|
||||
|
||||
对于上面的场景,拿到 `count` 的引用是不变的,**但对于其他场景就不一定了**。
|
||||
|
||||
举个例子:
|
||||
|
||||
```jsx
|
||||
function Child() {
|
||||
const user = useSelector((state) => ({ user: state.user }), shallowEqual);
|
||||
|
||||
return <UserPage user={user} />;
|
||||
}
|
||||
```
|
||||
|
||||
**假设 `user` 对象在每次数据流更新引用都会发生变化**,那么 `shallowEqual` 自然是不起作用,那我们换成 `deepEqual`深对比呢?结果是引用依然会变,只是重渲染不那么频繁了:
|
||||
|
||||
```jsx
|
||||
function Child() {
|
||||
const user = useSelector(
|
||||
(state) => ({ user: state.user }),
|
||||
// 当 user 值变化时才重渲染
|
||||
deepEqual
|
||||
);
|
||||
|
||||
// 但此处拿到的 user 引用还是会变化
|
||||
|
||||
return <UserPage user={user} />;
|
||||
}
|
||||
```
|
||||
|
||||
是不是觉得在 `deepEqual` 的作用下,没有触发重渲染,`user` 的引用就不会变呢?答案是会变,因为 `user` 对象在每次数据流更新都会变,`useSelector` 在 `deepEqual` 作用下没有触发重渲染,但因为全局 reducer 隐去组件自己的重渲染依然会重新执行此函数,此时拿到的 `user` 引用会不断变化。
|
||||
|
||||
因此 `useSelector` `deepEqual` 一定要和 `useDeepMemo` 结合使用,才能保证 `user` 引用不会频繁改变:
|
||||
|
||||
```jsx
|
||||
function Child() {
|
||||
const user = useSelector(
|
||||
(state) => ({ user: state.user }),
|
||||
// 当 user 值变化时才重渲染
|
||||
deepEqual
|
||||
);
|
||||
|
||||
const userDeep = useDeepMemo(() => user, [user]);
|
||||
|
||||
return <UserPage user={userDeep} />;
|
||||
}
|
||||
```
|
||||
|
||||
当然这是比较极端的情况,只要看到 `deepEqual` 与 `useSelector` 同时作用了,就要问问自己其返回的值的引用会不会发生意外变化。
|
||||
|
||||
### 缓存查询函数
|
||||
|
||||
对于极限场景,即便控制了重渲染次数与返回结果的引用最大程度不变,还是可能存在性能问题,这最后一块性能问题就处在查询函数上。
|
||||
|
||||
上面的例子中,查询函数比较简单,但如果查询函数非常复杂就不一样了:
|
||||
|
||||
```jsx
|
||||
function Child() {
|
||||
const user = useSelector(
|
||||
(state) => ({ user: verySlowFunction(state.user) }),
|
||||
// 当 user 值变化时才重渲染
|
||||
deepEqual
|
||||
);
|
||||
|
||||
const userDeep = useDeepMemo(() => user, [user]);
|
||||
|
||||
return <UserPage user={userDeep} />;
|
||||
}
|
||||
```
|
||||
|
||||
我们假设 `verySlowFunction` 要遍历画布中 1000 个组件的 n 3 次方次,那组件的重渲染时间消耗与查询时间相比完全不值一提,我们需要考虑缓存查询函数。
|
||||
|
||||
一种方式是利用 [reselect](https://github.com/reduxjs/reselect) 根据参数引用进行缓存。
|
||||
|
||||
想象一下,如果 `state.user` 的引用不频繁变化,但 `verySlowFunction` 非常慢,理想情况是 `state.user` 引用变化后才重新执行 `verySlowFunction`,但上面的例子中,`useSelector` 并不知道还能这么优化,只能傻傻的每次渲染重复执行 `verySlowFunction`,哪怕 `state.user` 没有变。
|
||||
|
||||
此时我们要告诉引用,`state.user` 是否变化才是重新执行的关键:
|
||||
|
||||
```jsx
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
const userSelector = createSelector(
|
||||
(state) => state.user,
|
||||
(user) => verySlowFunction(user)
|
||||
);
|
||||
|
||||
function Child() {
|
||||
const user = useSelector(
|
||||
(state) => userSelector(state),
|
||||
// 当 user 值变化时才重渲染
|
||||
deepEqual
|
||||
);
|
||||
|
||||
const userDeep = useDeepMemo(() => user, [user]);
|
||||
|
||||
return <UserPage user={userDeep} />;
|
||||
}
|
||||
```
|
||||
|
||||
在上面的例子中,通过 `createSelector` 创建的 `userSelector` 会一层层进行缓存,当第一个参数返回的 `state.user` 引用不变时,会直接返回上一次执行结果,直到其应用变化了才会继续往下执行。
|
||||
|
||||
> 这也说明了函数式保持幂等的重要性,如果 `verySlowFunction` 不是严格幂等的,这种缓存也无法实施。
|
||||
|
||||
看上去很美好,然而实战中你可能发现没有那么美好,因为上面的例子都建立在 **Selector 完全不依赖外部变量**。
|
||||
|
||||
### 结合外部变量的缓存查询
|
||||
|
||||
如果我们要查询的用户来自于不同地区,需要传递 `areaId` 加以识别,那么可以拆分为两个 Selector 函数:
|
||||
|
||||
```jsx
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
const areaSelector = (state, props) => state.areas[props.areaId].user;
|
||||
|
||||
const userSelector = createSelector(areaSelector, (user) =>
|
||||
verySlowFunction(user)
|
||||
);
|
||||
|
||||
function Child() {
|
||||
const user = useSelector(
|
||||
(state) => userSelector(state, { areaId: 1 }),
|
||||
deepEqual
|
||||
);
|
||||
|
||||
const userDeep = useDeepMemo(() => user, [user]);
|
||||
|
||||
return <UserPage user={userDeep} />;
|
||||
}
|
||||
```
|
||||
|
||||
所以为了不在组件函数内调用 `createSelector`,我们需要尽可能将用到外部变量的地方抽象成一个通用 Selector,并作为 `createSelector` 的一个先手环节。
|
||||
|
||||
但 `userSelector` 提供给多个组件使用时缓存会失效,原因是我们只创建了一个 Selector 实例,因此这个函数还需要再包装一层高阶形态:
|
||||
|
||||
```jsx
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
const userSelector = () =>
|
||||
createSelector(areaSelector, (user) => verySlowFunction(user));
|
||||
|
||||
function Child() {
|
||||
const customSelector = useMemo(userSelector, []);
|
||||
|
||||
const user = useSelector(
|
||||
(state) => customSelector(state, { areaId: 1 }),
|
||||
deepEqual
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
所以对于外部变量结合的环节,还需要 `useMemo` 与 `useSelector` 结合使用,`useMemo` 处理外部变量依赖的引用缓存,`useSelector` 处理 Store 相关引用缓存。
|
||||
|
||||
## 3 总结
|
||||
|
||||
基于 Hooks 的数据流方案不能算完美,我在写作这篇文章时就感觉到这种方案属于 “浅入深出”,简单场景还容易理解,随着场景逐步复杂,方案也变得越来越复杂。
|
||||
|
||||
但这种 Immutable 的数据流管理思路给了开发者非常自由的缓存控制能力,只要透彻理解上述概念,就可以开发出非常 “符合预期” 的数据缓存管理模型,只要精心维护,一切就变得非常有秩序。
|
||||
|
||||
> 讨论地址是:[精读《React Hooks 数据流》 · Issue #242 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/242)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,141 @@
|
||||
## 1 引言
|
||||
|
||||
从 [@types/react](https://unpkg.com/browse/@types/react@16.9.34/index.d.ts) 源码中挖掘一些 Typescript 使用技巧吧。
|
||||
|
||||
## 2 精读
|
||||
|
||||
### 泛型 extends
|
||||
|
||||
泛型可以指代可能的参数类型,但指代任意类型范围太模糊,当我们需要对参数类型加以限制,或者确定只处理某种类型参数时,就可以对泛型进行 extends 修饰。
|
||||
|
||||
问题:`React.lazy` 需要限制返回值是一个 `Promise<T>` 类型,且 `T` 必须是 React 组件类型。
|
||||
|
||||
方案:
|
||||
|
||||
```typescript
|
||||
function lazy<T extends ComponentType<any>>(
|
||||
factory: () => Promise<{ default: T }>
|
||||
): LazyExoticComponent<T>;
|
||||
```
|
||||
|
||||
`T extends ComponentType` 确保了 T 这个类型一定符合 `ComponentType` 这个 React 组件类型定义,我们再将 T 用到 `Promise<{ default: T }>` 位置即可。
|
||||
|
||||
## 泛型 extends + infer
|
||||
|
||||
如果有一种场景,需要拿到一个类型,这个类型是当某个参数符合某种结构时,这个结构内的一种子类型,就需要结合 泛型 extends + infer 了。
|
||||
|
||||
问题:`React.useReducer` 第一个参数是 Reducer,第二个参数是初始化参数,其实第二个参数的类型是第一个参数中回调函数第一个参数的类型,那我们怎么将这两个参数的关系联系到一起呢?
|
||||
|
||||
方案:
|
||||
|
||||
```typescript
|
||||
function useReducer<R extends Reducer<any, any>, I>(
|
||||
reducer: R,
|
||||
initializerArg: I & ReducerState<R>,
|
||||
initializer: (arg: I & ReducerState<R>) => ReducerState<R>
|
||||
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
|
||||
|
||||
type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any>
|
||||
? S
|
||||
: never;
|
||||
```
|
||||
|
||||
`R extends Reducer<any, any>` 的意思在上面已经提过了,也就是 R 必须符合 `Reducer` 结构,也就是 `reducer` 必须符合这个结构,之后重点来了:`initializerArg` 利用 `ReducerState` 这个类型直接从 `reducer` 的类型 `R` 中将第一个回调参数挖了出来并返回。
|
||||
|
||||
`ReducerState` 定义中 `R extends Reducer<infer S, any> ? S : never` 的含义是:如果 R 符合 `Reducer<infer S, any>` 类型,则返回类型 `S`,这个 `S` 是 `Reducer<infer S>` 也就是 State 位置的类型,否则返回 `never` 类型。
|
||||
|
||||
所以 infer 表示待推断类型,是非常强大的功能,可以指定在任意位置代指其类型,并配合 extends 判断是否符合结构,可以使类型推断具备一定编程能力。
|
||||
|
||||
要用 extends 的另一个原因是,只有 extends 才能将结构描述出来,我们才能精确定义 infer 指代类型的位置。
|
||||
|
||||
### 类型重载
|
||||
|
||||
当一个类型拥有多种使用可能性时,可以采用类型重载定义复数类型,Typescript 作用时会逐个匹配并找到第一个满足条件的。
|
||||
|
||||
问题:`createElement` 第一个参数支持 FunctionComponent 与 ClassComponent,而且传入参数不同,返回值的类型也不同。
|
||||
|
||||
方案:
|
||||
|
||||
```typescript
|
||||
function createElement<P extends {}>(
|
||||
type: FunctionComponent<P>,
|
||||
props?: (Attributes & P) | null,
|
||||
...children: ReactNode[]
|
||||
): FunctionComponentElement<P>;
|
||||
function createElement<P extends {}>(
|
||||
type: ClassType<
|
||||
P,
|
||||
ClassicComponent<P, ComponentState>,
|
||||
ClassicComponentClass<P>
|
||||
>,
|
||||
props?: (ClassAttributes<ClassicComponent<P, ComponentState>> & P) | null,
|
||||
...children: ReactNode[]
|
||||
): CElement<P, ClassicComponent<P, ComponentState>>;
|
||||
```
|
||||
|
||||
将 `createElement` 写两遍及以上,并配合不同的参数类型与返回值类型即可。
|
||||
|
||||
### 自定义类型收窄
|
||||
|
||||
我们可以通过 `typeof` 或 `instanceof` 做一些类型收窄工作,但有些类型甚至自定义类型的收窄判断函数需要自定义,我们可以通过 `is` 关键字定义自定义类型收窄判断函数。
|
||||
|
||||
问题:`isValidElement` 判断对象是否是合法的 React 元素,我们希望这个函数具备类型收窄的功能。
|
||||
|
||||
方案:
|
||||
|
||||
```typescript
|
||||
function isValidElement<P>(
|
||||
object: {} | null | undefined
|
||||
): object is ReactElement<P>;
|
||||
|
||||
const element: string | ReactElement = "";
|
||||
|
||||
if (isValidElement(element)) {
|
||||
element; // 自动推导类型为 ReactElement
|
||||
} else {
|
||||
element; // 自动推导类型为 string
|
||||
}
|
||||
```
|
||||
|
||||
基于这个方案,我们可以创建一些很有用的函数,比如 `isArray`,`isMap`,`isSet` 等等,通过 `is` 关键字时其被调用时具备类型收窄的功能。
|
||||
|
||||
### 用 Interface 定义函数
|
||||
|
||||
一般定义函数类型我们用 `type`,但有些情况下定义的函数既可被调用,也有一些默认属性值需要定义,我们可以继续用 Interface 定义。
|
||||
|
||||
问题:`FunctionComponent` 既可以当作函数调用,同时又能定义 `defaultProps` `displayName` 等固定属性。
|
||||
|
||||
方案:
|
||||
|
||||
```typescript
|
||||
interface FunctionComponent<P = {}> {
|
||||
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
|
||||
propTypes?: WeakValidationMap<P>;
|
||||
contextTypes?: ValidationMap<any>;
|
||||
defaultProps?: Partial<P>;
|
||||
displayName?: string;
|
||||
}
|
||||
```
|
||||
|
||||
`(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null` 表示这种类型的变量可以作为函数执行:
|
||||
|
||||
```jsx
|
||||
const App: FunctionComponent = () => <div />;
|
||||
App.displayName = "App";
|
||||
```
|
||||
|
||||
## 3 总结
|
||||
|
||||
看完文章内容,相信你已经可以独立读懂 [@types/react](https://unpkg.com/browse/@types/react@16.9.34/index.d.ts) 这个包的所有类型定义!
|
||||
|
||||
更多基础内容可以阅读 [精读《Typescript2.0 - 2.9》](https://github.com/dt-fe/weekly/blob/7de3c77c3bdd7304c9e4b0c0f70c3ba6968ebd29/058.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript2.0%20-%202.9%E3%80%8B.md) 与 [精读《Typescript 3.2 新特性》](https://github.com/dt-fe/weekly/blob/v2/084.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%203.2%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md),由于 TS 更新频繁,后续 TS 技巧可能继续以阅读源码方式进行,希望这次选用的 React 类型源码可以让你印象深刻。
|
||||
|
||||
> 讨论地址是:[精读《@types/react 值得注意的 TS 技巧》 · Issue #245 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/245)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,133 @@
|
||||
## 1 引言
|
||||
|
||||
Error Boundaries 是 React16 提出来用来捕获渲染时错误的概念,今天我们一起读一读 [A Simple Guide to Error Boundaries in React](https://alligator.io/react/error-boundaries/) 这篇文章,了解一下这个重要机制。
|
||||
|
||||
## 2 概述
|
||||
|
||||
Error Boundaries 可以用来捕获渲染时错误,API 如下:
|
||||
|
||||
```jsx
|
||||
class MyErrorBoundary extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// 更新 state,下次渲染可以展示错误相关的 UI
|
||||
return { error: error };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
// 错误上报
|
||||
logErrorToMyService(error, info);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
// 渲染出错时的 UI
|
||||
return <p>Something broke</p>;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `static getDerivedStateFromError`: 在出错后有机会修改 state 触发最后一次错误 fallback 的渲染。
|
||||
- `componentDidCatch`: 用于出错时副作用代码,比如错误上报等。
|
||||
|
||||
这两种方法中任意一个被定义时,这个组件就会成为 `Error Boundary` 组件,可以阻止子组件渲染时报错。
|
||||
|
||||
最后作者还提出一个建议,建议将 Error Boundary 单独作为一个组件,而不是将错误监听方法与业务组件耦合,一方面考虑到复用,另一方面则因为错误检测只对子组件生效。
|
||||
|
||||
好吧,其实 React 官方文档比这篇文章介绍的详细的多得多,原文介绍到此结束。
|
||||
|
||||
## 3 精读
|
||||
|
||||
[React Error Boundaries 官方文档](https://reactjs.org/docs/error-boundaries.html) 里提到了四种无法 Catch 的错误场景:
|
||||
|
||||
1. 回调事件。由于回调事件执行时机不在渲染周期内,因此无法被 Error Boundary Catch 住,如有必要得自行 try/catch。
|
||||
2. 异步。比如 `setTimeout` 或 `requestAnimationFrame`,和第一条同理。
|
||||
3. 服务端渲染。
|
||||
4. Error Boundary 组件自身触发的错误。因为只能捕获其子组件的错误。
|
||||
|
||||
这也是使用 Error Boundaries 最容易有疑问的地方。除了上面的情况,笔者结合自身经验再列举几种异常边界场景。
|
||||
|
||||
### 无法捕获编译时错误
|
||||
|
||||
很明显,即便是 React 官方 API `Error Boundary` 也只能捕获运行时错误,而对编译时错误无能为力。
|
||||
|
||||
编译时错误包括不限于编译环境错误、运行前的框架错误检查提示、TS/Flow 类型错误等,这些都是 `Error Boundary` 无法捕获的,而且没有更好的办法 Catch 住,遇到编译错误就在编译时解决吧,仅关注运行时错误就好了。
|
||||
|
||||
### 可以作用于 Function Component
|
||||
|
||||
虽然函数式组件无法定义 `Error Boundary`,但 `Error Boundary` 可以捕获函数式组件的错误,因此可以曲线救国:
|
||||
|
||||
```jsx
|
||||
// ErrorBoundary 组件
|
||||
class ErrorBoundary extends React.Component {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 可以捕获所有组件异常,包括 Function Component 的子组件
|
||||
const App = () => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Child />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 对 Hooks 也可生效
|
||||
|
||||
对于 Hooks 中异常也可以生效,比如下面的代码:
|
||||
|
||||
```jsx
|
||||
const Child = (props) => {
|
||||
React.useEffect(() => {
|
||||
console.log(1);
|
||||
props.a.b;
|
||||
console.log(2);
|
||||
}, [props.a.b]);
|
||||
|
||||
return <div />;
|
||||
};
|
||||
```
|
||||
|
||||
要注意的是,出现在 deps 中的错误会立即被 Catch,导致 `console.log(1)` 都无法打印。但如果是下面的代码,则可以打印出 `console.log(1)`,无法打印出 `console.log(2)`:
|
||||
|
||||
```jsx
|
||||
const Child = (props) => {
|
||||
React.useEffect(() => {
|
||||
console.log(1);
|
||||
props.a.b;
|
||||
console.log(2);
|
||||
}, []);
|
||||
|
||||
return <div />;
|
||||
};
|
||||
```
|
||||
|
||||
所以 React 官网的这句话并不是指 `Error Boundary` 对 Hooks 不生效,而是指 `Error Boundary` 无法以 Hooks 方式指定,对功能是没有影响的:
|
||||
|
||||
> componentDidCatch and getDerivedStateFromError: There are no Hook equivalents for these methods yet, but they will be added soon.
|
||||
|
||||
所以这里的理解要注意一下,另外 React 官方文档 [Hooks FAQ](https://reactjs.org/docs/hooks-faq.html#how-do-lifecycle-methods-correspond-to-hooks) 有很多宝藏,建议抽时间逐条阅读。
|
||||
|
||||
## 4 总结
|
||||
|
||||
`Error Boundary` 可以捕获所有子元素渲染时异常,包括 render、各生命周期函数,但也有很多使用限制,希望你可以正确使用它。
|
||||
|
||||
错误捕获也不是万能的,更多时候我们要避免并及时修复错误,通过错误捕获降低出错时对用户体验的影响,并在第一时间内监控起来并快速修复。
|
||||
|
||||
最后,你有明明正确使用了 `Error Boundary` 却依然无法 Catch 住的错误 Case 吗?
|
||||
|
||||
> 讨论地址是:[精读《React Error Boundaries》 · Issue #246 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/246)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,199 @@
|
||||
## 1 引言
|
||||
|
||||
在数据中台做 BI 工具经常面对海量数据的渲染处理,除了组件本身性能优化之外,经常要排查整体页面性能瓶颈点,尤其是维护一些性能做得并不好的旧代码时。
|
||||
|
||||
React 性能调试是面对这种问题的必修课,借助 [Profiling React.js Performance](https://addyosmani.com/blog/profiling-react-js/) 这篇文章一起学习一下这个技能吧。
|
||||
|
||||
## 2 精读
|
||||
|
||||
本文介绍了众多性能检测工具与方法。
|
||||
|
||||
### React Profiler
|
||||
|
||||
`Profiler` 这个 API 是一种运行时 Debug 的补充,可以通过其 callback 拿到组件渲染信息,用法如下:
|
||||
|
||||
```jsx
|
||||
const Movies = ({ movies, addToQueue }) => (
|
||||
<React.Profiler id="Movies" onRender={callback}>
|
||||
<div />
|
||||
</React.Profiler>
|
||||
);
|
||||
|
||||
function callback(
|
||||
id,
|
||||
phase,
|
||||
actualTime,
|
||||
baseTime,
|
||||
startTime,
|
||||
commitTime,
|
||||
interactions
|
||||
) {}
|
||||
```
|
||||
|
||||
这个 callback 会在每次渲染时执行,渲染分为初始化和更新阶段,通过 `phase` 区分,下面是参数详细说明:
|
||||
|
||||
- id: 传入的 id。
|
||||
- phase: "mount" 或 "update",表示更新状态。
|
||||
- actualDuration: 实际渲染耗时。
|
||||
- baseDuration: 没有使用 memo 时的渲染预计耗时。
|
||||
- startTime: 开始渲染的时间。
|
||||
- commitTime: React 提交更新的时间
|
||||
- interactions: 何种原因导致的渲染,比如 `setState` 或 hooks changed 之类。
|
||||
|
||||
注意尽量不要轻易使用 `Profiler` 检测性能,因为 `Profiler` 本身也会消耗性能。
|
||||
|
||||
如果不想获得这么详细的渲染耗时,或者不想提前在代码中埋点,可以利用 DevTools 的 Profiler 查看更直观更简洁的渲染耗时:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1sPAuDuL2gK0jSZPhXXahvXXa-1846-1028.png">
|
||||
|
||||
其中 Ranked 可以展示按照渲染耗时排序后的结果,Interations 需要配合 Tracing API 使用,在后面会提到。
|
||||
|
||||
### Tracing API
|
||||
|
||||
利用 `scheduler/tracing` 提供的 `trace` API,我们可以记录某个动作的耗时,比如 “点击添加按钮收藏一个电影” 耗时多久:
|
||||
|
||||
```jsx
|
||||
import { render } from "react-dom";
|
||||
import { unstable_trace as trace } from "scheduler/tracing";
|
||||
|
||||
class MyComponent extends Component {
|
||||
addMovieButtonClick = (event) => {
|
||||
trace("Add To Movies Queue click", performance.now(), () => {
|
||||
this.setState({ itemAddedToQueue: true });
|
||||
});
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
在 Interations 中可以看到动作触发的耗时:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1XR.FDAY2gK0jSZFgXXc5OFXa-1846-1010.png">
|
||||
|
||||
这个动作还可以是渲染,比如可以记录 ReactDOM 渲染的耗时:
|
||||
|
||||
```jsx
|
||||
import { unstable_trace as trace } from "scheduler/tracing";
|
||||
|
||||
trace("initial render", performance.now(), () => {
|
||||
ReactDom.render(<App />, document.getElementById("app"));
|
||||
});
|
||||
```
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB18hyHfcKfxu4jSZPfXXb3dXXa-1846-740.png">
|
||||
|
||||
甚至还可以追踪异步的耗时:
|
||||
|
||||
```jsx
|
||||
import {
|
||||
unstable_trace as trace,
|
||||
unstable_wrap as wrap,
|
||||
} from "scheduler/tracing";
|
||||
|
||||
trace("Some event", performance.now(), () => {
|
||||
setTimeout(
|
||||
wrap(() => {
|
||||
// 异步操作
|
||||
})
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
有了 `Profiler` 与 `trace` 这两件武器,我们可以监控任意元素的渲染耗时与交互耗时,几乎可以涵盖所有性能监控需要。
|
||||
|
||||
### Puppeteer
|
||||
|
||||
我们还可以利用 Puppeteer 实现自动化操作并打印报告:
|
||||
|
||||
```jsx
|
||||
const puppeteer = require("puppeteer");
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
const navigationPromise = page.waitForNavigation();
|
||||
await page.goto("https://react-movies-queue.glitch.me/");
|
||||
await page.setViewport({ width: 1276, height: 689 });
|
||||
await navigationPromise;
|
||||
|
||||
const addMovieToQueueBtn =
|
||||
"li:nth-child(3) > .card > .card__info > div > .button";
|
||||
await page.waitForSelector(addMovieToQueueBtn);
|
||||
|
||||
// Begin profiling...
|
||||
await page.tracing.start({ path: "profile.json" });
|
||||
// Click the button
|
||||
await page.click(addMovieToQueueBtn);
|
||||
// Stop profliling
|
||||
await page.tracing.stop();
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
```
|
||||
|
||||
首先利用 `puppeteer` 创建一个浏览器,新建一个页面并打开 `https://react-movies-queue.glitch.me/` 这个 URL,等待页面加载完毕后利用 DOM 选择器找到按钮,利用 `page.click` API 模拟点击这个按钮,并在前后利用 `page.tracing` 记录性能变化,并将这个文件上传到 DevTools Performance 面板,就会得到一份自动的性能检测报告:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1623EDxz1gK0jSZSgXXavwpXa-2769-2289.png">
|
||||
|
||||
这张图相当重要,是浏览器综合运行开销分析的利器,最上面分为 4 个部分:
|
||||
|
||||
- FPS:每秒帧数,绿色竖线越高表示 FPS 越高,出现红线则表示出现了卡顿。
|
||||
- CPU:CPU 资源,用面积图展示消耗 CPU 资源的事件。
|
||||
- NET:网络消耗,每条横杠表示一种资源的加载。
|
||||
- HEAP:内存水位,由于短时间内看不出来是否会内存溢出,一般只用来简单看看内存消耗是否符合预期,对于内存溢出的检测需要用持续监控上报的方式。
|
||||
|
||||
下面会有一张 Network 详细图解,比如这张图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1D.wKDxD1gK0jSZFyXXciOVXa-2868-750.png">
|
||||
|
||||
细线表示等待的时间,粗线表示实际加载的情况,其中浅色部分表示服务器等待时间,即从发送下载请求到服务器响应第一个字节的时间。这部分可以看出资源并行加载阻塞情况以及资源服务器响应时间是否存在问题。
|
||||
|
||||
Timings 展示了几个重要时间节点,这里列举一部分:
|
||||
|
||||
- FP:First Paint,第一次绘制。
|
||||
- FCP:First Contentful Paint,第一次内容绘制。
|
||||
- LCP:Largest Contentful Paint,最大内容绘制。
|
||||
- DCL:Document Content Loaded,DOM 内容加载完毕。
|
||||
|
||||
再下面是 JS 计算消耗,用了一张火焰图,火焰图是性能分析的常用可视化工具。以下面这张图为例:
|
||||
|
||||
<img width=350 src="https://img.alicdn.com/tfs/TB1JecIDrr1gK0jSZFDXXb9yVXa-1404-616.png">
|
||||
|
||||
看火焰图首先看跨度最长的函数,也就是最长的那条线,这是最耗时的部分,从左到右是浏览器脚本的调用顺序,从上到下是函数嵌套的顺序。
|
||||
|
||||
我们可以看到鼠标位置的 34 这个函数虽然长,但并不是性能瓶颈,因为下面执行的 n 函数长度和它一样,表示 34 函数的性能几乎无损耗,其性能由其调用的 n 函数决定。
|
||||
|
||||
我们可以利用这种方式一步步排查到叶子结点,找到对性能影响最大的元子函数。
|
||||
|
||||
### User Timing API
|
||||
|
||||
我们还可以利用 `performance.mark` 自定义性能检测节点:
|
||||
|
||||
```jsx
|
||||
// Record the time before running a task
|
||||
performance.mark("Movies:updateStart");
|
||||
// Do some work
|
||||
|
||||
// Record the time after running a task
|
||||
performance.mark("Movies:updateEnd");
|
||||
|
||||
// Measure the difference between the start and end of the task
|
||||
performance.measure("moviesRender", "Movies:updateStart", "Movies:updateEnd");
|
||||
```
|
||||
|
||||
这些节点可以在上面介绍的 Performance 面板中展示出来用于自定义分析。
|
||||
|
||||
## 3 总结
|
||||
|
||||
利用 Performance 进行通用性能分析,利用 React Profiler 进行 React 定制性能分析,这两个结合在一起几乎可以完成任何性能检测。
|
||||
|
||||
一般来说,首先应该用 React Profiler 进行 React 层面的问题筛查,这样更直观,更容易定位问题。如果某些问题跳出了 React 框架范围,或者不再能以组件粒度进行度量,我们可以回到 Performance 面板进行通用性能分析。
|
||||
|
||||
> 讨论地址是:[精读《React 性能调试》 · Issue #247 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/247)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,259 @@
|
||||
## 1 引言
|
||||
|
||||
Deno 是什么?Deno 和 Node 有什么关系?Deno 和我有什么关系?
|
||||
|
||||
Deno 将于 2020-05-13 发布 1.0,如果你还有上面的疑惑,可以和我一起通过 [Deno 1.0: What you need to know](https://blog.logrocket.com/deno-1-0-what-you-need-to-know/) 这篇文章一起了解 Deno 基础知识。
|
||||
|
||||
希望你带着疑问思考,未来 10 年看今天,会不会出现 Deno 官方生态壮大,完全替代 Node 进而影响到 Web 生态的局面呢?这个思考结果会影响到你未来职业发展,你需要学会自己思考,并对这个思考结果负责。
|
||||
|
||||
## 2 介绍 & 精读
|
||||
|
||||
Deno 的作者是 Ryan Dahl,他是 Nodejs 背后的策划者,曾经说过 [我对 Nodejs 感到遗憾的 10 件事](https://www.youtube.com/watch?v=M3BM9TB-8yA)。这也是为什么新开一个坑的原因,但 Deno 并不定位为 Nodejs 的替代品,从整体功能来看,Deno 有更大的野心,据我的推测是想要取代现在陈旧的前后端开发模式,让 Deno 一统前后端开发全流程。
|
||||
|
||||
Nodejs 是由 C++ 写的,而 Deno 则是由 Rust 写的,并选择了 [Tokio](https://tokio.rs/) 这个异步编程框架,并使用 V8 引擎解析 Javascript,并内置了对 Ts 的解析。
|
||||
|
||||
### 安装
|
||||
|
||||
Deno 支持如下安装方式:
|
||||
|
||||
**Shell:**
|
||||
|
||||
```shell
|
||||
curl -fsSL https://deno.land/x/install/install.sh | sh
|
||||
```
|
||||
|
||||
**PowerShell:**
|
||||
|
||||
```shell
|
||||
iwr https://deno.land/x/install/install.ps1 -useb | iex
|
||||
```
|
||||
|
||||
**Homebrew:**
|
||||
|
||||
```shell
|
||||
brew install deno
|
||||
```
|
||||
|
||||
**Chocolatey:**
|
||||
|
||||
```shell
|
||||
choco install deno
|
||||
```
|
||||
|
||||
脚本执行方式为 `deno run`,可以类比为 `node`,但功能不同且支持远程文件,实际上远程依赖是 Deno 的一大特色,也是有争议的地方:
|
||||
|
||||
```shell
|
||||
deno run https://deno.land/std/examples/welcome.ts
|
||||
```
|
||||
|
||||
在 ts 文件中允许用远程脚本加载资源,这个后面还会提到:
|
||||
|
||||
```ts
|
||||
import { serve } from "https://deno.land/std@v0.42.0/http/server.ts";
|
||||
const s = serve({ port: 8000 });
|
||||
console.log("http://localhost:8000/");
|
||||
for await (const req of s) {
|
||||
req.respond({ body: "Hello World\n" });
|
||||
}
|
||||
```
|
||||
|
||||
### 安全性
|
||||
|
||||
Deno 是默认安全的,这体现在默认没有环境、网络访问权限、文件读写权限、运行子进程的能力。所以如果直接运行一个依赖权限的文件会报错:
|
||||
|
||||
```shell
|
||||
deno run file-needing-to-run-a-subprocess.ts
|
||||
|
||||
# error: Uncaught PermissionDenied: access to run a subprocess, run again with the --allow-run flag
|
||||
```
|
||||
|
||||
可以通过参数方式允许权限的执行,有 `--allow-read`、`--allow-write`、`--allow-net` 等:
|
||||
|
||||
```shell
|
||||
deno --allow-read=/etc
|
||||
```
|
||||
|
||||
上面表示 `/etc` 文件夹下的文件拥有文件读权限。
|
||||
|
||||
除了直接加参数调用、Bash 脚本调用外,还可以用 Make 运行,或者使用类似的 [drake](https://deno.land/x/drake/) 启动。
|
||||
|
||||
或者使用 `deno install` 命令,将脚本转化为一个快捷指令:
|
||||
|
||||
```shell
|
||||
deno install --allow-net --allow-read -n serve https://deno.land/std/http/file_server.ts
|
||||
```
|
||||
|
||||
`-n` 表示 `--name`,可以对这个脚本进行重命名,比如上面的例子中,`serve` 命令就等同于 `deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts`。
|
||||
|
||||
### 标准库
|
||||
|
||||
Deno 在标准库上很有特点,对常用功能提供了官方版本,保证可用性与稳定性。原文中列出了一些与 Npm 三方库的对比:
|
||||
|
||||
| Deno Module | Description | npm | Equivalents |
|
||||
| ----------- | --------------------------------------------------------------------------------- | --- | ------------------------ |
|
||||
| colors | Adds color to the terminal | | chalk, kleur, and colors |
|
||||
| datetime | Helps working with the JavaScript Date object | |
|
||||
| encoding | Adds support for external data scructures like base32, binary, csv, toml and yaml | |
|
||||
| flags | Helps working with command line arguments | | minimist |
|
||||
| fs | Helps with manipulation of the file system | |
|
||||
| http | Allows serving local files over HTTP | | http-server |
|
||||
| log | Used for creating logs | | winston |
|
||||
| testing | For unit testing assertion and benchmarking | | chai |
|
||||
| uuid | UUID generation | | uuid |
|
||||
| ws | Helps with creating WebSocket client/server | | ws |
|
||||
|
||||
从这个点上来看,Deno 既做运行环境又做基础生态,缓解了 Npm 生态下选择困难症,这件事需要辩证来看:集成了官方包对功能确定的模块来说是很有必要的,而且提高了底层库的稳定性;但 Deno 生态也有三方库,而且本质上三方库和官方库在功能上没有任何壁垒,因为实现代码都类似,唯一区别是谁能为其稳定性站台,假设微软和 Deno 同时出了基于 Npm 生态与 Deno 生态官方库,都保证会持续维护,你更相信谁呢?官方是否有优势要取决于官方自身的实力。
|
||||
|
||||
### 内置 Typescript
|
||||
|
||||
Deno 内置支持了 TS,因此不需要 `ts-node` 我们就可以用 `deno run test.ts` 运行 Typescript 文件。值得注意的是,Deno 内部也是利用 Typescript 引擎解析为 Js 后交由 V8 引擎解析,因此本质上没太大的变化,只是这样 Deno 的生态会更规范。
|
||||
|
||||
由于内置了 TS 支持,自然也不需要写 `tsconfig.json` 配置了,但你依然可以定制它:
|
||||
|
||||
```shell
|
||||
deno run -c tsconfig.json [file-to-run.ts]
|
||||
```
|
||||
|
||||
Deno 默认还开启了 TS 严格模式,所以看到这里,可以认为 Deno 是为了构建高质量理想库而诞生的运行环境,基于已有的生态来做,但做了更多内置技术选型,这和 Facebook 的 [rome](https://github.com/facebookexperimental/rome) 很像,但做的却更彻底。
|
||||
|
||||
其实从实现上来看,我们基于 Javascript 生态也能写出 `deno run test.ts` 这样类似的引擎,只不过是由 JS 驱动执行,可能编译还会选择 Webpack,但 Deno 本身基于 Rust 实现,并重新实现了一套模块加载标准,可以说从更底层的方式重新解读了 W3C 标准规范,以期望解决 Javascript 生态的各种痛点问题。
|
||||
|
||||
### 支持 Web 标准
|
||||
|
||||
Deno 还支持 W3C 标准规范,因此像 `fetch`、`setTimeout` 等 API 都可以被直接使用,如果你按照 Deno 支持的那几个函数写代码,可以保证在 Deno、Node、Web 三个平台实现跨平台运行。
|
||||
|
||||
虽然距离完全实现 W3C 所有标准规范还有一些路要走,但我们看到了 Deno 兼容规范的决心。
|
||||
|
||||
### ESModule
|
||||
|
||||
模块化是 Deno 的亮点,Deno 使用官方 ESModule 规范,但引用路径必须加上后缀:
|
||||
|
||||
```ts
|
||||
import * as log from "https://deno.land/std/log/mod.ts";
|
||||
import { outputToConsole } from "./view.ts";
|
||||
```
|
||||
|
||||
Deno 不需要申明依赖,代码的引用路径就是依赖申明,会包括完整的路径以及文件后缀,也支持网络资源,可以摆脱 NPM 中心化的包管理模式,因为这个路径可以是任何网络地址。
|
||||
|
||||
### 包管理
|
||||
|
||||
对于 `import * as log from "https://deno.land/std/log/mod.ts";` 这行代码,Deno 会下载到一个缓存文件夹,用户不会感知到这个文件夹与这个过程的存在,也就是说,Deno 环境中是没有 `node_modules` 的。
|
||||
|
||||
也可以通过 `deno --reload` 的方式强制刷新缓存。
|
||||
|
||||
但这里也要辩证的看待 “Deno 去中心化” 这件事,虽然引用了网络源,但会引发下面几个问题:
|
||||
|
||||
1. 实际上还存在一个 "node_modules",只是用户看不到。
|
||||
2. 网络下载速度放到运行时,第一次启动还是很慢。
|
||||
3. 普通模式下无 lock,必须配合 `deps.ts` 使用,这个后面会提到。
|
||||
|
||||
即使被打上 “中心化恶人” 的 npm 也有去中心化的一面,因为 npm 支持私有化部署,无论是速度还是稳定性都可以由公司自己掌控,从稳定性来说还是 npm 拥有压倒性优势。
|
||||
|
||||
### 三方库
|
||||
|
||||
Deno 还有第三方库生态,截止目前共有 [221 个三方库](<[](https://deno.land/x/)>)。
|
||||
|
||||
由于 Deno 走网络资源,我们可以借助 [Pika](https://www.pika.dev/cdn) 提供的 CDN 服务直接引用网络资源包:
|
||||
|
||||
```jsx
|
||||
import * as pkg from "https://cdn.pika.dev/preact@^10.3.0";
|
||||
```
|
||||
|
||||
虽然这样看上去很轻量,但对公司来说还是需要自建一个 “Pika” 保障稳定性,以及做全球 CDN 缓存等的工作。
|
||||
|
||||
### 告别 package.json
|
||||
|
||||
npm 生态下包信息存放在 `package.json`,包含但不限于下面的内容:
|
||||
|
||||
- 项目元信息。
|
||||
- 项目依赖和版本号。
|
||||
- 依赖还进行分类,比如 `dependencies`、`devDependencies` 甚至 `peerDependencies`。
|
||||
- 标记入口,`main` 和 `module`,还有 TS 用的 `types` 与 `typings`,脚手架的 `bin` 等等。
|
||||
- npm scripts。
|
||||
|
||||
随着标准的不断更新,`package.json` 信息已经非常臃肿了。
|
||||
|
||||
对于 Deno 来说,则使用 `deps.ts` 集中管理依赖:
|
||||
|
||||
```ts
|
||||
export { assert } from "https://deno.land/std@v0.39.0/testing/asserts.ts";
|
||||
export { green, bold } from "https://deno.land/std@v0.39.0/fmt/colors.ts";
|
||||
```
|
||||
|
||||
`deps.ts` 就是一个普通文件,只是将项目的依赖精确描述出来,这样其他地方引用 `assert` 时,就可以这么写了:
|
||||
|
||||
```ts
|
||||
// import { assert } from "https://deno.land/std@v0.39.0/testing/asserts.ts";
|
||||
import { assert } from "./deps.ts";
|
||||
```
|
||||
|
||||
如果需要锁定依赖,可以通过 `deno --lock=lock.json` 方式申明。
|
||||
|
||||
### deno doc
|
||||
|
||||
`deno doc <filename>` 命令可以根据文件按照 JS Doc 规则生成文档,同时也支持 TS 语法,比如下面这段代码:
|
||||
|
||||
```ts
|
||||
/** Asynchronously fulfill a response with a file from the local file
|
||||
* system. */
|
||||
export async function send(
|
||||
{ request, response }: Context,
|
||||
path: string,
|
||||
options: SendOptions = { root: "" }
|
||||
): Promise<string | undefined> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
生成文档如下:
|
||||
|
||||
```text
|
||||
function send(_: Context, path: string, options: SendOptions): Promise<string | undefined>
|
||||
Asynchronously fulfill a response with a file from the local file system.
|
||||
```
|
||||
|
||||
deno 本身文档就是用这个命令生成的,可以 [访问官方文档](https://doc.deno.land/) 查看使用效果。
|
||||
|
||||
### 内置工具链
|
||||
|
||||
前端 Javascript 工具链相当混乱,虽然业界已有 Umi 等框架做了开箱即用的封装,但回到 Javascript 设计的初衷就是可以在浏览器直接使用的,包括浏览器对不依赖构建工具的模块化支持,注定了未来 Webpack 一定会被消灭。
|
||||
|
||||
Deno 通过内置一套工具链的方式解决这个问题,包括:
|
||||
|
||||
- 测试:提供 `deno test` 命令与 `Deno.test()` 测试函数。
|
||||
- 格式化:提供 [vscode 插件](https://marketplace.visualstudio.com/items?itemName=axetroy.vscode-deno)。
|
||||
- 编译:提供 `deno bundle` 命令。
|
||||
|
||||
不过值得注意的是,在最重要的编译环节,`deno bundle` 目前提供的能力是相对欠缺的,比如还不支持 Tree Shaking。
|
||||
|
||||
用 Rust 等语言提升构建效率是业界一直在尝试的事,比如 @陈成 就基于 [esbuild](https://github.com/evanw/esbuild) 做了 [@umijs/plugin-esbuild](https://umijs.org/zh-CN/plugins/plugin-esbuild) 插件用于提升 Umi 构建速度,但为了防止生产构建产物与 Webpack 默认规则不一致,仅使用了其压缩(minifier)功能。
|
||||
|
||||
对 deno 来说也一样,目前其实没有任何证据表明 deno 的构建结果可以完美适配 webpack 环境,所以请勿认为 deno 发布了 1.0 版本就等于可以在生产环境使用。
|
||||
|
||||
## 3 总结
|
||||
|
||||
正如原文结尾所说的,Deno 虽然将要发布 1.0 版本,但仍不能完全替代 Nodejs,这背后的原因主要是历史兼容成本,也就是完整支持整个 Node 生态不只是设计的问题,更是一个体力活,需要一个个高地去攻克。
|
||||
|
||||
同样 Deno 对 Web 的支持也让人耳目一新,但仍不能放到生产环境使用,除了官方和三方生态还在逐渐完善外,`deno bundle` 对 Tree Shaking 能力的缺失以及构建产物无法保证与现在的 Webpack 完全相同,这样会导致对稳定性要求极高的大型应用迁移成本非常高。
|
||||
|
||||
最亮眼的改动是模块化部分,依赖完全去中心化从长远来看是一个非常好的设计,只是基础设施和生态要达到一个较为理想的水平。
|
||||
|
||||
最后,让我们站在一个预言者角度思考一下 Deno 到底会不会火吧:
|
||||
|
||||
Deno 做的初心是做一个更好的 Node,但很不幸,对于这种级别的生态底层工具来说,重新做一个并重新火起来的难度,不亚于重新做一个阿里巴巴并取代现在阿里的难度。也就是不同的时间点做同一件事,哪怕后者可以吸取教训,大概率也无法复制以前成功的路线。
|
||||
|
||||
从 Deno 的功能来看,解决了 Node 很多痛点,其中就包括去中心化管理,有点云开发的意思,但在 2020 年,基于 Nodejs 和 Webpack 的云开发都搞出来了,说实话是没有 Deno 什么空间的。从功能上来看,开篇就说了 Deno 基于 V8 解析 Javascript,对于性能和功能都没有革命性提升,从技术上作出突破也几乎不可能了。
|
||||
|
||||
Deno 的思想确实比 Node 先进,但不能说比 Node 好十倍,则无法撼动 Node 的生态,即便是 Node 作者自己可能也不行。
|
||||
|
||||
然而我上面说的可能都是错的。
|
||||
|
||||
> 讨论地址是:[精读《Deno 1.0 你需要了解的》 · Issue #248 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/248)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,453 @@
|
||||
## 1 引言
|
||||
|
||||
与组件生命周期绑定的 Utils 非常适合基于 React Hooks 来做,比如可以将 “发请求” 这个功能与组件生命周期绑定,实现一些便捷的功能。
|
||||
|
||||
这次以 [@umijs/use-request](https://hooks.umijs.org/zh-CN/hooks/async) 为例子,分析其功能思路与源码。
|
||||
|
||||
## 2 简介
|
||||
|
||||
[@umijs/use-request](https://hooks.umijs.org/zh-CN/hooks/async) 支持以下功能:
|
||||
|
||||
- 默认自动请求:在组件初次加载时自动触发请求函数,并自动管理 `loading`, `data` , `error` 状态。
|
||||
- 手动触发请求:设置 `options.manual = true` , 则手动调用 `run` 时才会取数。
|
||||
- 轮询请求:设置 `options.pollingInterval` 则进入轮询模式,可通过 `run` / `cancel` 开始与停止轮询。
|
||||
- 并行请求:设置 `options.fetchKey` 可以对请求状态隔离,通过 `fetches` 拿到所有请求状态。
|
||||
- 请求防抖:设置 `options.debounceInterval` 开启防抖。
|
||||
- 请求节流:设置 `options.throttleInterval` 开启节流。
|
||||
- 请求缓存 & SWR:设置 `options.cacheKey` 后开启对请求结果缓存机制,下次请求前会优先返回缓存并在后台重新取数。
|
||||
- 请求预加载:由于 `options.cacheKey` 全局共享,可以提前执行 `run` 实现预加载效果。
|
||||
- 屏幕聚焦重新请求:设置 `options.refreshOnWindowFocus = true` 在浏览器 `refocus` 与 `revisible` 时重新请求。
|
||||
- 请求结果突变:可以通过 `mutate` 直接修改取数结果。
|
||||
- 加载延迟:设置 `options.loadingDelay` 可以延迟 `loading` 变成 `true` 的时间,有效防止闪烁。
|
||||
- 自定义请求依赖:设置 `options.refreshDeps` 可以在依赖变动时重新触发请求。
|
||||
- 分页:设置 `options.paginated` 可支持翻页场景。
|
||||
- 加载更多:设置 `options.loadMore` 可支持加载更多场景。
|
||||
|
||||
一切 Hooks 的功能拓展都要基于 React Hooks 生命周期,我们可以利用 Hooks 做下面几件与组件相关的事:
|
||||
|
||||
1. 存储与当前组件实例绑定的 mutable、immutable 数据。
|
||||
2. 主动触发调用组件 rerender。
|
||||
3. 访问到组件初始化、销毁时机的钩子。
|
||||
|
||||
上面这些功能就可以基于这些基础能力拓展了:
|
||||
|
||||
**默认自动请求**
|
||||
|
||||
在组件初始时机取数。由于和组件生命周期绑定,可以很方便实现各组件相互隔离的取数顺序强保证:可以利用取数闭包存储 requestIndex,取数结果返回后与当前最新 requestIndex 进行比对,丢弃不一致的取数结果。
|
||||
|
||||
**手动触发请求**
|
||||
|
||||
将触发取数的函数抽象出来并在 CustomHook 中 return。
|
||||
|
||||
**轮询请求**
|
||||
|
||||
在取数结束后设定 `setTimeout` 重新触发下一轮取数。
|
||||
|
||||
**并行请求**
|
||||
|
||||
每次取数时先获取当前请求唯一标识 `fetchKey`,仅更新这个 key 下的状态。
|
||||
|
||||
**请求防抖、请求节流**
|
||||
|
||||
这个实现方式可以挺通用化,即取数调用函数处替换为对应 `debounce` 或 `throttle` 函数。
|
||||
|
||||
**请求预加载**
|
||||
|
||||
这个功能只要实现全局缓存就自然支持了。
|
||||
|
||||
**屏幕聚焦重新请求**
|
||||
|
||||
这个可以统一监听 window action 事件,并触发对应组件取数。可以全局统一监听,也可以每个组件分别监听。
|
||||
|
||||
**请求结果突变**
|
||||
|
||||
由于取数结果存储在 CustomHook 中,直接修改数据 data 值即可。
|
||||
|
||||
**加载延迟**
|
||||
|
||||
有加载延迟时,可以先将 `loading` 设置为 `false`,等延迟到了再设置为 `true`,如果此时取数提前完毕则销毁定时器,实现无 loading 取数。
|
||||
|
||||
**自定义请求依赖**
|
||||
|
||||
利用 `useEffect` 和自带的 deps 即可。
|
||||
|
||||
**分页**
|
||||
|
||||
基于通用取数 Hook 封装,本质上是多带了一些取数参数与返回值参数,并遵循 Antd Table 的 API。
|
||||
|
||||
**加载更多**
|
||||
|
||||
和分页类似,区别是加载更多不会清空已有数据,并且需要根据约定返回结构 `noMore` 判断是否能继续加载。
|
||||
|
||||
## 3 精读
|
||||
|
||||
接下来是源码分析。
|
||||
|
||||
首先定义了一个类 `Fetch`,这是因为一个 `useRequest` 的 `fetchKey` 特性可以通过多实例解决。
|
||||
|
||||
Class 的生命周期不依赖 React Hooks,所以将不依赖生命周期的操作收敛到 Class 中,不仅提升了代码抽象程度,也提升了可维护性。
|
||||
|
||||
```tsx
|
||||
class Fetch<R, P extends any[]> {
|
||||
// ...
|
||||
// 取数状态存储处
|
||||
state: FetchResult<R, P> = {
|
||||
loading: false,
|
||||
params: [] as any,
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
run: this.run.bind(this.that),
|
||||
mutate: this.mutate.bind(this.that),
|
||||
refresh: this.refresh.bind(this.that),
|
||||
cancel: this.cancel.bind(this.that),
|
||||
unmount: this.unmount.bind(this.that),
|
||||
};
|
||||
|
||||
constructor(
|
||||
service: Service<R, P>,
|
||||
config: FetchConfig<R, P>,
|
||||
// 外部通过这个回调订阅 state 变化
|
||||
subscribe: Subscribe<R, P>,
|
||||
initState?: { data?: any; error?: any; params?: any; loading?: any }
|
||||
) {}
|
||||
|
||||
// 此 setState 非彼 setState,作用是更新 state 并通知订阅
|
||||
setState(s = {}) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...s,
|
||||
};
|
||||
this.subscribe(this.state);
|
||||
}
|
||||
|
||||
// 实际取数函数,但下划线命名的带有一些历史气息啊
|
||||
_run(...args: P) {}
|
||||
|
||||
// 对外暴露的取数函数,对防抖和节流做了分发处理
|
||||
run(...args: P) {
|
||||
if (this.debounceRun) {
|
||||
// return ..
|
||||
}
|
||||
if (this.throttleRun) {
|
||||
// return ..
|
||||
}
|
||||
return this._run(...args);
|
||||
}
|
||||
|
||||
// 取消取数,考虑到了防抖、节流兼容性
|
||||
cancel() {}
|
||||
|
||||
// 以上次取数参数重新取数
|
||||
refresh() {}
|
||||
|
||||
// 轮询 starter
|
||||
rePolling() {}
|
||||
|
||||
// 对应 mutate 函数
|
||||
mutate(data: any) {}
|
||||
|
||||
// 销毁订阅
|
||||
unmount() {}
|
||||
}
|
||||
```
|
||||
|
||||
**默认自动请求**
|
||||
|
||||
通过 `useEffect` 零依赖实现,需要:
|
||||
|
||||
1. 有缓存则不需响应,当对应缓存结束后会通知,同时也支持了请求预加载功能。
|
||||
2. 为支持并行请求,所有请求都通过 `fetches` 独立管理。
|
||||
|
||||
```tsx
|
||||
// 第一次默认执行
|
||||
useEffect(() => {
|
||||
if (!manual) {
|
||||
// 如果有缓存
|
||||
if (Object.keys(fetches).length > 0) {
|
||||
/* 重新执行所有的 */
|
||||
Object.values(fetches).forEach((f) => {
|
||||
f.refresh();
|
||||
});
|
||||
} else {
|
||||
// 第一次默认执行,可以通过 defaultParams 设置参数
|
||||
run(...(defaultParams as any));
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
```
|
||||
|
||||
默认执行第 11 行,并根据当前的 `fetchKey` 生成对应 `fetches`,如果初始化已经存在 `fetches`,则行为改为重新执行所有 **已存在的** 并行请求。
|
||||
|
||||
**手动触发请求**
|
||||
|
||||
上一节已经在初始请求时禁用了 `manual` 开启时的默认取数。下一步只要将封装的取数函数 `run` 定义出来并暴露给用户:
|
||||
|
||||
```tsx
|
||||
const run = useCallback(
|
||||
(...args: P) => {
|
||||
if (fetchKeyPersist) {
|
||||
const key = fetchKeyPersist(...args);
|
||||
newstFetchKey.current = key === undefined ? DEFAULT_KEY : key;
|
||||
}
|
||||
const currentFetchKey = newstFetchKey.current;
|
||||
// 这里必须用 fetchsRef,而不能用 fetches。
|
||||
// 否则在 reset 完,立即 run 的时候,这里拿到的 fetches 是旧的。
|
||||
let currentFetch = fetchesRef.current[currentFetchKey];
|
||||
if (!currentFetch) {
|
||||
const newFetch = new Fetch(
|
||||
servicePersist,
|
||||
config,
|
||||
subscribe.bind(null, currentFetchKey),
|
||||
{
|
||||
data: initialData,
|
||||
}
|
||||
);
|
||||
currentFetch = newFetch.state;
|
||||
setFeches((s) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
s[currentFetchKey] = currentFetch;
|
||||
return { ...s };
|
||||
});
|
||||
}
|
||||
return currentFetch.run(...args);
|
||||
},
|
||||
[fetchKey, subscribe]
|
||||
);
|
||||
```
|
||||
|
||||
主动取数函数与内部取数函数共享一个,所以 `run` 函数要考虑多种情况,其中之一就是并行取数的情况,因此需要拿到当前取数的 `fetchKey`,并创建一个 `Fetch` 的实例,最终调用 `Fetch` 实例的 `run` 函数取数。
|
||||
|
||||
**轮询请求**
|
||||
|
||||
轮询取数在 `Fetch` 实际取数函数 `_fetch` 中定义,当取数函数 `fetchService`(对多种形态的取数方法进行封装后)执行完后,无论正常还是报错,都要进行轮询逻辑,因此在 `.finally` 时机里判断:
|
||||
|
||||
```tsx
|
||||
fetchService.then().finally(() => {
|
||||
if (!this.unmountedFlag && currentCount === this.count) {
|
||||
if (this.config.pollingInterval) {
|
||||
// 如果屏幕隐藏,并且 !pollingWhenHidden, 则停止轮询,并记录 flag,等 visible 时,继续轮询
|
||||
if (!isDocumentVisible() && !this.config.pollingWhenHidden) {
|
||||
this.pollingWhenVisibleFlag = true;
|
||||
return;
|
||||
}
|
||||
this.pollingTimer = setTimeout(() => {
|
||||
this._run(...args);
|
||||
}, this.config.pollingInterval);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
轮询还要考虑到屏幕是否隐藏,如果可以触发轮询则触发定时器再次调用 `_run`,注意这个定时器需要正常销毁。
|
||||
|
||||
**并行请求**
|
||||
|
||||
每个 `fetchKey` 对应一个 `Fetch` 实例,这个逻辑在 **手动触发请求** 介绍的 `run` 函数中已经实现。
|
||||
|
||||
这块的封装思路可以品味一下,从外到内分别是 React Hooks 的 fetch -> Fetch 类的 run -> Fetch 类的 \_run,并行请求做在 React Hooks 这一层。
|
||||
|
||||
**请求防抖、请求节流**
|
||||
|
||||
这个实现就在 Fetch 类的 `run` 函数中:
|
||||
|
||||
```tsx
|
||||
function run(...args: P) {
|
||||
if (this.debounceRun) {
|
||||
this.debounceRun(...args);
|
||||
return Promise.resolve(null as any);
|
||||
}
|
||||
if (this.throttleRun) {
|
||||
this.throttleRun(...args);
|
||||
return Promise.resolve(null as any);
|
||||
}
|
||||
return this._run(...args);
|
||||
}
|
||||
```
|
||||
|
||||
由于防抖和节流是 React 无关的,也不是最终取数无关的,因此实现在 `run` 这个夹层函数进行分发。
|
||||
|
||||
这里实现的比较简化,防抖后 `run` 拿到的 Promise 不再是有效的取数结果了,其实这块还是可以进一步对 Promise 进行封装,无论在防抖还是正常取数的场景都返回 Promise,只需 resolve 的时机由 `Fetch` 这个类灵活把控即可。
|
||||
|
||||
**请求预加载**
|
||||
|
||||
预加载就是缓存机制,首先利用 `useEffect` 同步缓存:
|
||||
|
||||
```tsx
|
||||
// cache
|
||||
useEffect(() => {
|
||||
if (cacheKey) {
|
||||
setCache(cacheKey, {
|
||||
fetches,
|
||||
newstFetchKey: newstFetchKey.current,
|
||||
});
|
||||
}
|
||||
}, [cacheKey, fetches]);
|
||||
```
|
||||
|
||||
在初始化 `Fetch` 实例时优先采用缓存:
|
||||
|
||||
```tsx
|
||||
const [fetches, setFeches] = useState<Fetches<U, P>>(() => {
|
||||
// 如果有 缓存,则从缓存中读数据
|
||||
if (cacheKey) {
|
||||
const cache = getCache(cacheKey);
|
||||
if (cache) {
|
||||
newstFetchKey.current = cache.newstFetchKey;
|
||||
/* 使用 initState, 重新 new Fetch */
|
||||
const newFetches: any = {};
|
||||
Object.keys(cache.fetches).forEach((key) => {
|
||||
const cacheFetch = cache.fetches[key];
|
||||
const newFetch = new Fetch();
|
||||
// ...
|
||||
newFetches[key] = newFetch.state;
|
||||
});
|
||||
return newFetches;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
```
|
||||
|
||||
**屏幕聚焦重新请求**
|
||||
|
||||
在 `Fetch` 构造函数实现监听并调用 `refresh` 即可,源码里采取全局统一监听的方式:
|
||||
|
||||
```tsx
|
||||
function subscribe(listener: () => void) {
|
||||
listeners.push(listener);
|
||||
return function unsubscribe() {
|
||||
const index = listeners.indexOf(listener);
|
||||
listeners.splice(index, 1);
|
||||
};
|
||||
}
|
||||
|
||||
let eventsBinded = false;
|
||||
if (typeof window !== "undefined" && window.addEventListener && !eventsBinded) {
|
||||
const revalidate = () => {
|
||||
if (!isDocumentVisible()) return;
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
// dispatch 每个 listener
|
||||
const listener = listeners[i];
|
||||
listener();
|
||||
}
|
||||
};
|
||||
window.addEventListener("visibilitychange", revalidate, false);
|
||||
// only bind the events once
|
||||
eventsBinded = true;
|
||||
}
|
||||
```
|
||||
|
||||
在 `Fetch` 构造函数里注册:
|
||||
|
||||
```tsx
|
||||
this.limitRefresh = limit(this.refresh.bind(this), this.config.focusTimespan);
|
||||
|
||||
if (this.config.pollingInterval) {
|
||||
this.unsubscribe.push(subscribeVisible(this.rePolling.bind(this)));
|
||||
}
|
||||
```
|
||||
|
||||
并通过 `limit` 封装控制调用频率,并 push 到 `unsubscribe` 数组,一边监听可以随组件一起销毁。
|
||||
|
||||
**请求结果突变**
|
||||
|
||||
这个函数只要更新 `data` 数据结果即可:
|
||||
|
||||
```tsx
|
||||
function mutate(data: any) {
|
||||
if (typeof data === "function") {
|
||||
this.setState({
|
||||
data: data(this.state.data) || {},
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
值得注意的是,`cancel`、`refresh`、`mutate` 都必须在初次请求完成后才有意义,所以初次返回的函数是一个抛错:
|
||||
|
||||
```tsx
|
||||
const noReady = useCallback(
|
||||
(name: string) => () => {
|
||||
throw new Error(`Cannot call ${name} when service not executed once.`);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
loading: !manual || defaultLoading,
|
||||
data: initialData,
|
||||
error: undefined,
|
||||
params: [],
|
||||
cancel: noReady("cancel"),
|
||||
refresh: noReady("refresh"),
|
||||
mutate: noReady("mutate"),
|
||||
...(fetches[newstFetchKey.current] || {}),
|
||||
} as BaseResult<U, P>;
|
||||
```
|
||||
|
||||
等取数完成后会被 `...(fetches[newstFetchKey.current] || {})` 这一段覆盖为正常函数。
|
||||
|
||||
**加载延迟**
|
||||
|
||||
如果设置了加载延迟,请求发动时就不应该立即设置为 loading,这个逻辑写在 `_run` 函数中:
|
||||
|
||||
```tsx
|
||||
function _run(...args: P) {
|
||||
// 取消 loadingDelayTimer
|
||||
if (this.loadingDelayTimer) {
|
||||
clearTimeout(this.loadingDelayTimer);
|
||||
}
|
||||
this.setState({
|
||||
loading: !this.config.loadingDelay,
|
||||
params: args,
|
||||
});
|
||||
|
||||
if (this.config.loadingDelay) {
|
||||
this.loadingDelayTimer = setTimeout(() => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
}, this.config.loadingDelay);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
启动一个 `setTimeout` 将 loading 设为 `true` 即可,这个 timeout 在下次执行 `_run` 时被 `clearTimeout` 清空。
|
||||
|
||||
**自定义请求依赖**
|
||||
|
||||
最明智的做法是利用 `useEffect` 实现,实际代码做了组件 unmount 保护:
|
||||
|
||||
```tsx
|
||||
// refreshDeps 变化,重新执行所有请求
|
||||
useUpdateEffect(() => {
|
||||
if (!manual) {
|
||||
/* 全部重新执行 */
|
||||
Object.values(fetchesRef.current).forEach((f) => {
|
||||
f.refresh();
|
||||
});
|
||||
}
|
||||
}, [...refreshDeps]);
|
||||
```
|
||||
|
||||
非手动条件下,依赖变化所有已存在的 `fetche` 执行 `refresh` 即可。
|
||||
|
||||
分页和加载更多就不解析了,原理是在 `useAsync` 这个基础请求 Hook 基础上再包一层 Hook,拓展取数参数与返回结果。
|
||||
|
||||
## 4 总结
|
||||
|
||||
目前还有 错误重试、请求超时管理、Suspense 没有支持,看完这篇精读后,相信你已经可以提 PR 了。
|
||||
|
||||
> 讨论地址是:[精读《@umijs/use-request》源码 · Issue #249 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/249)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,284 @@
|
||||
## 1 引言
|
||||
|
||||
[Recoil](https://recoiljs.org/) 是 Facebook 公司出的数据流管理方案,有一定思考的价值。
|
||||
|
||||
Recoil 是基于 Immutable 的数据流管理方案,这也是它值得被拿出来看的最重要原因,如果要用 Mutable 方式管理 React 数据流,直接看 [mobx-react](https://github.com/mobxjs/mobx-react) 就足够了。
|
||||
|
||||
然而 React Immutable 特性带来的可预测性非常利于调试和维护:
|
||||
|
||||
1. 断点调试时变量的值与当前执行位置无关,已创建过的值不会突然 Mutable 突变,非常可预测。
|
||||
2. 在 React 框架下组件更新机制单一,只有引用变化才触发重渲染,而没有 Mutable 模式下 ForceUpdate 的心智负担。
|
||||
|
||||
当然 Immutable 模式下存在一定编码心智负担,所以各有优劣。
|
||||
|
||||
> 但 Recoil 和 Redux 一样,并不代表 React 官方数据流管理方案,因此不用带着官方光环去看它。
|
||||
|
||||
## 2 简介
|
||||
|
||||
Recoil 解决 React 全局数据流管理的问题,采用分散管理原子状态的设计模式,支持派生数据与异步查询,在基本功能上可以覆盖 Redux。
|
||||
|
||||
### 状态作用域
|
||||
|
||||
和 Redux 一样,全局数据流管理需要存在作用域 `RecoilRoot`:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
import { RecoilRoot } from "recoil";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<CharacterCounter />
|
||||
</RecoilRoot>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`RecoilRoot` 在被嵌套时,最内层的 `RecoilRoot` 会覆盖外层的配置及状态值。
|
||||
|
||||
### 定义数据
|
||||
|
||||
与 Redux 集中定义 `initState` 不同,Recoil 采用 `atom` 以分散方式定义数据:
|
||||
|
||||
```jsx
|
||||
const textState = atom({
|
||||
key: "textState",
|
||||
default: "",
|
||||
});
|
||||
```
|
||||
|
||||
其中 `key` 必须在 `RecoilRoot` 作用域内唯一,也可以认为是 state 树打平时 key 必须唯一的要求。
|
||||
|
||||
`default` 定义默认值,既然数据定义分散了,默认值定义也是分散的。
|
||||
|
||||
### 读取数据
|
||||
|
||||
与 Redux 的 Connect 或 useSelector 类似,Recoil 采用 Hooks 方式读取数据:
|
||||
|
||||
```jsx
|
||||
import { useRecoilValue } from "recoil";
|
||||
|
||||
function App() {
|
||||
const text = useRecoilValue(textState);
|
||||
}
|
||||
```
|
||||
|
||||
`useRecoilValue` 与 `useSetRecoilState` 都可以获取数据,区别是 `useRecoilState` 还可以获取写数据的函数:
|
||||
|
||||
```jsx
|
||||
import { useRecoilState } from "recoil";
|
||||
|
||||
function App() {
|
||||
const [text, setText] = useRecoilValue(useRecoilState);
|
||||
}
|
||||
```
|
||||
|
||||
### 修改数据
|
||||
|
||||
与 Redux 集中定义纯函数 `reducer` 修改数据不同,Recoil 采用 Hooks 方式写数据。
|
||||
|
||||
除了上面提到的 `useRecoilState` 之外,还有一个 `useSetRecoilState` 可以仅获取写函数:
|
||||
|
||||
```jsx
|
||||
import { useSetRecoilState } from "recoil";
|
||||
|
||||
function App() {
|
||||
const setText = useSetRecoilValue(useRecoilState);
|
||||
}
|
||||
```
|
||||
|
||||
`useSetRecoilState` 与 `useRecoilState`、`useRecoilValue` 的不同之处在于,数据流的变化不会导致组件 Rerender,因为 `useSetRecoilState` 仅写不读。
|
||||
|
||||
这也导致 Recoil API 偏多被诟病,这也是 Immutable 模式下存的编码心智负担,虽然很好理解,但也只有 `useSelector` 或 Recoil 这样拆分 API 的方式可以解决。
|
||||
|
||||
> 另外还提供了 `useResetRecoilState` 重置到默认值并读取。
|
||||
|
||||
### 仅读不订阅
|
||||
|
||||
与 ReactRedux 的 `useStore` 类似,Recoil 提供了 `useRecoilCallback` 用于只读不订阅场景:
|
||||
|
||||
```jsx
|
||||
import { atom, useRecoilCallback } from "recoil";
|
||||
|
||||
const itemsInCart = atom({
|
||||
key: "itemsInCart",
|
||||
default: 0,
|
||||
});
|
||||
|
||||
function CartInfoDebug() {
|
||||
const logCartItems = useRecoilCallback(async ({ getPromise }) => {
|
||||
const numItemsInCart = await getPromise(itemsInCart);
|
||||
|
||||
console.log("Items in cart: ", numItemsInCart);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`useRecoilCallback` 通过回调方式定义要读取的数据,这个数据变化也不会导致当前组件重渲染。
|
||||
|
||||
### 派生值
|
||||
|
||||
与 Mobx `computed` 类似,recoil 提供了 `selector` 支持派生值,这是比较有特色的功能:
|
||||
|
||||
```jsx
|
||||
import { atom, selector, useRecoilState } from "recoil";
|
||||
|
||||
const tempFahrenheit = atom({
|
||||
key: "tempFahrenheit",
|
||||
default: 32,
|
||||
});
|
||||
|
||||
const tempCelcius = selector({
|
||||
key: "tempCelcius",
|
||||
get: ({ get }) => ((get(tempFahrenheit) - 32) * 5) / 9,
|
||||
set: ({ set }, newValue) => set(tempFahrenheit, (newValue * 9) / 5 + 32),
|
||||
});
|
||||
|
||||
function TempCelcius() {
|
||||
const [tempF, setTempF] = useRecoilState(tempFahrenheit);
|
||||
const [tempC, setTempC] = useRecoilState(tempCelcius);
|
||||
}
|
||||
```
|
||||
|
||||
`selector` 提供了 `get`、`set` 分别定义如何赋值与取值,所以其与 `atom` 定义一样可以被 `useRecoilState` 等三套 API 操作,这里甚至不用看源码就能猜到,`atom` 应该是基于 `selector` 的一个特定封装。
|
||||
|
||||
### 异步读取
|
||||
|
||||
基于 `selector` 可以实现异步数据读取,只要将 `get` 函数写成异步即可:
|
||||
|
||||
```jsx
|
||||
const currentUserNameQuery = selector({
|
||||
key: "CurrentUserName",
|
||||
get: async ({ get }) => {
|
||||
const response = await myDBQuery({
|
||||
userID: get(currentUserIDState),
|
||||
});
|
||||
if (response.error) {
|
||||
throw response.error;
|
||||
}
|
||||
return response.name;
|
||||
},
|
||||
});
|
||||
|
||||
function CurrentUserInfo() {
|
||||
const userName = useRecoilValue(currentUserNameQuery);
|
||||
return <div>{userName}</div>;
|
||||
}
|
||||
|
||||
function MyApp() {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<ErrorBoundary>
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<CurrentUserInfo />
|
||||
</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
</RecoilRoot>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
1. 异步状态可以被 `Suspense` 捕获。
|
||||
2. 异步过程报错可以被 `ErrorBoundary` 捕获。
|
||||
|
||||
如果不想用 `Suspense` 阻塞异步,可以换 `useRecoilValueLoadable` 这个 API 在当前组件内管理异步状态:
|
||||
|
||||
```jsx
|
||||
function UserInfo({ userID }) {
|
||||
const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID));
|
||||
switch (userNameLoadable.state) {
|
||||
case "hasValue":
|
||||
return <div>{userNameLoadable.contents}</div>;
|
||||
case "loading":
|
||||
return <div>Loading...</div>;
|
||||
case "hasError":
|
||||
throw userNameLoadable.contents;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 依赖外部变量
|
||||
|
||||
与 `reselect` 一样,Recoil 也面临状态管理不纯粹的问题,即数据读取依赖外部变量,这样会面临较为复杂的缓存计算问题,甚至还出现了 `re-reselect` 库。
|
||||
|
||||
因为 Recoil 本身是原子化状态管理的,所以这个问题相对好解决:
|
||||
|
||||
```jsx
|
||||
const myMultipliedState = selectorFamily({
|
||||
key: "MyMultipliedNumber",
|
||||
get: (multiplier) => ({ get }) => {
|
||||
return get(myNumberState) * multiplier;
|
||||
},
|
||||
});
|
||||
|
||||
function MyComponent() {
|
||||
const number = useRecoilValue(myMultipliedState(100));
|
||||
}
|
||||
```
|
||||
|
||||
当外部传参 `multiplier` 与依赖值 `myNumberState` 不变时,就不会重新计算。
|
||||
|
||||
Recoil 在 `get` 与 `set` 函数定义 `Atom` 时,内部会自动生成依赖,这个部分做的比较好。
|
||||
|
||||
> 依赖外部变量使用了 Family 后缀,比如 selector -> selectorFamily;atom -> atomFamily。
|
||||
|
||||
## 3 精读
|
||||
|
||||
Recoil 以原子化方式对状态进行分离管理,确实比较契合 Immutable 的编程模式,尤其在缓存处理时非常亮眼,但编程领域中,优势换一个角度看往往就变成了劣势,我们还是要客观评价一下 Recoil。
|
||||
|
||||
### Immutable 心智负担
|
||||
|
||||
API 较多,在简介中也提到了,这可能是 Immutable 自带的硬伤,而不仅仅是 Recoil 的问题。
|
||||
|
||||
Immutable 模式中,对数据流只有读与写两种诉求,**而申明式编程讲究的是数据变化后 UI 自动 Rerender,那么对数据的读自然而然就被赋予了订阅其变化后触发 Rerender 的期待**,但是写与读不同,为什么 `setState` 强调用回调方式写数据?因为回调方式的写不依赖读,有写诉求的组件没必要与读挂上钩,也就是写组件的地方不一定要订阅对应数据。
|
||||
|
||||
Recoil 提供了 `useRecoilState` 作为读写双重 API,仅在既读又写的场景使用,而 `useRecoilValue` 仅仅是为了简化 API,替换为 `useRecoilState` 不会有性能损失,而 `useSetRecoilValue` 则必须认真对待,在仅写不读的场景必须严格使用这个 API。
|
||||
|
||||
那 `useState` 为什么默认是读写的?因为 `useState` 是单组件状态管理的场景,一个定义在组件内的状态不可能只写不读,但 Recoil 是全局状态解决方案,读写分离的场景下,对于只写的组件很有必要脱离对数据的订阅实现性能最大化。
|
||||
|
||||
### 条件访问数据
|
||||
|
||||
这也是 Hooks 的通病,由于 Hooks 不能写在条件语句中,因此要利用 Hooks 获取一个带有条件判断的数据时,必须回到 `selector` 模式:
|
||||
|
||||
```jsx
|
||||
const articleOrReply = selectorFamily({
|
||||
key: "articleOrReply",
|
||||
get: ({ isArticle, id }) => ({ get }) => {
|
||||
if (isArticle) {
|
||||
return get(article(id));
|
||||
}
|
||||
|
||||
return get(reply(id));
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
这样的代码其实挺冗余的,其实在 Mutable 模式下可以 `isArticle ? store.articles[id] : store.replies[id]` 就能搞定的模式,必须单独抽一个 `selector` 出来写上头十行代码,显得非常繁琐。
|
||||
|
||||
### Recoil 的本质
|
||||
|
||||
从 Hooks API 到派生值,这两个核心特点恰巧是对 Context 与 useMemo 的封装。
|
||||
|
||||
首先基于 Hooks 的 `useContext` 已经足够轻量易用,可以认为 `atom` 与 `useRecoilState`、`useRecoilValue`、`useSetRecoilValue` 分别对应封装后的 `createContext` 与 `useContext`。
|
||||
|
||||
再看 `useMemo`,大部分情况我们可以利用 `useMemo` 造出派生值,这对应了 Recoil 的 `selector` 和 `selectorFamily`。
|
||||
|
||||
所以 Recoil 本质更像一个模式化封装库,针对数据驱动易于数据原子化管理的场景,并做到高性能。
|
||||
|
||||
## 3 总结
|
||||
|
||||
无论你用不用 Recoil,我们都可以从 Recoil 这儿学到 React 状态管理的基本功:
|
||||
|
||||
1. 对象的读与写分离,做到最优按需渲染。
|
||||
2. 派生的值必须严格缓存,并在命中缓存时引用保证严格相等。
|
||||
3. 原子存储的数据相互无关联,所有关联的数据都使用派生值方式推导。
|
||||
|
||||
> 讨论地址是:[精读《recoil》· Issue #251 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/251)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
Reference in New Issue
Block a user