Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 FQA](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))
|
||||
Reference in New Issue
Block a user