来自 Dan 大神的博客摘录,一篇阅读摘要哈。
风格指南
别被虚幻的问题分散了注意力:指的就是明明正确的代码,由于代码风格的问题,被提示为错误,但这其实是 linter 配置产生的。
关于 linter 看法:通过良好的配置,linter 是一个很好的工具,它可以在 bug 出现前就能发现它们。但它对代码风格的关注过多,使其变得会分散注意力。
因此你需要做的一件事情就是:整理你的 Lint 配置,把你的团队叫到一起,一条条过一下你们项目中启用的 lint 规则,接着问问自己:“这条规则有帮我找到过 bug 吗?” 如果不是,关掉这条规则。不要假设一年前你或别人添加到你的 lint 配置中的任何东西,都是“最佳实践”。保持质疑,找到答案。
关于代码格式化:用 Prettier 然后忘掉 “风格”。
不阻断数据流
渲染中不要阻断数据流
常见的一个错误是,把 props 复制到 state,会导致后续 props 数据的更新会被忽略,这通常不是我们所希望的。
很少情况下,这样的行为是有意为之的,请确认将这样的属性取名为 initialXXX 或 defaultXXX 来表明组件会忽略这个属性的改变。
如果一个值,需要使用 props 经过复杂的计算来得到,这是一个会将 props 复制到 state 的场景。但通常我们要准备很多额外的事情才能保证没有 bug,以下是常见的方式
- 如果仅仅初始化 state 的时候进行计算,则 props 更新时会数据不新鲜
- 可以把计算放到 render 中,组件改为 PureComponent
- 以上方式你也知道,PureComponent 不能比较引用类型,如果 children 改变,会导致调用 render,会导致重新计算
- 直观上会想到在
componentDidUpdate
中调用计算,只有值不相等才重新计算,但这会导致每次更新都会有两次 render - 可以使用已不推荐的 componentWillReceiveProps 生命周期函数。然而,大家经常把 side effects 放这。这反过来又往往会给即将到来的并发渲染 特性像 Time Slicing 和 Suspense 带来问题。而更 “安全” 的 getDerivedStateFromProps 又有点难用。
- 回归问题的本质,我们是想记忆一个值,除非输入改变,否则重新计算输出
- 函数组件使用
useMemo
- 类组件使用
memoize-one
- 函数组件使用
不要在 Side Effects 里阻断数据流
这个咋一看有点难以理解,首先理解什么是副作用,可以理解成该函数不是纯函数。比如我们数据获取,每次都会对 state 进行赋值,这里就是副作用。
同样表现在,如果我们数据的获取,依赖 props 传递的参数,此时我们应该响应 props 的每一次变化,否则通常而言,就会存在 bug
- 我们可能不仅在
componentDidMount
中执行数据获取,同样在componentDidUpdate
中进行依赖判断,如果 props 响应的值改变了,则重新获取数据 - 上述存在的问题就是,如果某天数据获取函数,需要多依赖 props 传递的一个参数,
componentDidUpdate
中需要增加条件判断逻辑,但是这个逻辑通常我们很可能会忘记,应为没有任何提示 - 如果我们能够以某种方式自动捕捉到这些错误,那不是很好吗?useEffect 就可以做到这件事情,将逻辑放在 effect 中,这样可以更容易地看到它从 React 数据流中依赖了哪些值。这些值称为“依赖”,而且由于 useEffect 依赖关系是显示的,可以使用 lint 规则检验是否一致,这通常可以帮我们避免 bug
- 使用 class API,你必须自己考虑一致性,并验证对每个相关 prop 或 state 的更改是否该由 componentDidUpdate 处理。
不要在优化中阻断数据流
你可能会意外忽略对 props 的更改。当你手动优化组件时,可能会发生这类错误。
注意,使用浅比较的优化方法(如 PureComponent
和 React.memo
)与 默认比较
是安全的。
如果你尝试通过编写自己的比较方法来 “优化” 组件,你可能会错误地忘记比较函数属性,一开始很容易错过这个错误,因为对于类,你通常会传递一个方法,所以它会有相同的身份。
如何避免这个问题呢?建议避免手动实现 shouldComponentUpdate ,也要避免在 React.memo() 中使用自定义的比较方法。如果你坚持使用自定义的比较,请确保不跳过函数。
在类组件中很容易错过这个问题,因为方法标识通常是稳定的(但并非总是如此——而这就是 debug 困难的地方)。有了Hooks,情况不同了:
- function 在每个渲染中都不同,所以你能马上发现这个问题
- 通过 useCallback 和 useContext,你能避免往下传递函数。这让你优化渲染时不用太担心函数的问题。
时刻准备渲染
这个标题咋一看,会有点懵逼。大概意思就是说:不要试图在组件行为中,假设任何不必要的时间假设。你的组件应该随时可以重新渲染。
这里提到一个时间假设,组件渲染的三种可能,父组件更新子组件更新,子组件自身更新,这里应该是没有时间先后的,同时多次组件更新,子组件不应该被破坏掉。更具体一点:父组件的重渲染,不应该污染子组件的状态
组件应该具有弹性,能适应更少或更频繁地渲染,否则它们与特定父组件存在过多耦合。
违背这个原则的方式是什么样的?React 让这没那么容易发生——但你可以使用传统的 componentWillReceiveProps
生命周期方法来实现。
我们可以看到各种优化,例如 PureComponent、shouldComponentUpdate 和 React.memo,它们不应该用于控制行为。只有提高性能的场景下,去使用它们。如果删除优化就会破坏某个组件,那么它就太脆弱了。
没有单例组件
有时我们假设某个组件只会显示一次,如导航栏。在一段时间内这也许是对的,然而,这种假设导致的设计问题,常常会在后期显现。
要重现这类问题也很容易,试试渲染你的应用两次
ReactDOM.render(
<>
<MyApp />
<MyApp />
</>,
document.getElementById('root')
);
你的应用仍然正常运行吗?或者你是否看到奇怪的崩溃和错误?偶尔对复杂组件进行压力测试是个好主意,可以确保组件存在多个拷贝时不会相互冲突。
作者写过的几次有问题的代码情况
- componentWillUnmount 中执行全局状态 “清理”
- componentDidMount 执行全局状态的重置
这些模式是检测组件是否脆弱的好指标。显示或隐藏 一颗树,不应该破坏树之外的组件。
隔离本地状态
React 组件可能有本地状态。但是哪个状态真的是自己的呢?
如果你不确定某个状态是否属于本地,请问自己:“如果此组件呈现两次,交互是否应反映在另一个副本中?” 只要答案为“否”,那你就找到本地状态了。
别把该本地的状态全局化了,这涉及到我们的 “弹性” 主题:组件之间发生的意外同步更少。作为奖励,这也修复了一大类性能问题。
总结
让我们再一次回顾一下这些原则:
- 不阻断数据流:props 和 state 可能会更新,组件应该处理好这些更新,不论什么时候。
- 时刻准备渲染:一个组件不应该被或多或少的渲染而损坏。
- 没有单例组件:即使组件只渲染一次,但通过设计让它渲染两次也不会被破坏,是更好了。
- 隔离本地状态:想想哪个状态是特定 UI 展示下的本地状态——并且除非必要,不要将该状态提升到更高的地方。
生命周期的变化
16.4 版本的生命周期较 16 版本之前有比较大的变化。原来(React v16.0前)的生命周期在 React v16 推出的 Fiber 之后就不合适了,因为如果要开启 async rendering,在 render 函数之前的所有函数,都有可能被执行多次。
原来(React v16.0前)的生命周期有哪些是在render前执行的呢?
- componentWillMount
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
禁止不能用比劝导开发者不要这样用的效果更好,所以除了 shouldComponentUpdate
,其他在 render 函数之前的所有函数(componentWillMount
,componentWillReceiveProps
,componentWillUpdate
)都被 getDerivedStateFromProps 静态函数替代。
用一个静态函数 getDerivedStateFromProps
来取代被 deprecate
的几个生命周期函数,就是强制开发者在 render 之前只做无副作用的操作,而且能做的操作局限在根据 props
和 state
决定新的 state
static getDerivedStateFromProps(props, state) 在组件创建时和更新时的render方法之前调用,它应该返回一个对象来更新状态,或者返回 null 来不更新任何内容。
getSnapshotBeforeUpdate() 被调用于 render 之后,可以读取但无法使用 DOM 的时候。它使您的组件可以在可能更改之前从 DOM 捕获一些信息(例如滚动位置)。此生命周期返回的任何值都将作为参数传递给 componentDidUpdate()。
最常见的误解就是 getDerivedStateFromProps 和 componentWillReceiveProps 只会在 props “改变”时才会调用。实际上只要父级重新渲染时,这两个生命周期函数就会重新调用,不管 props 有没有“变化”。
派生 state
在 React 的官网 blog 中提到你可能不需要使用派生 state,和这篇文章的内容有一定的相关性,这里也学习一下。
getDerivedStateFromProps
的存在只有一个目的:让组件在 props 变化时更新 state。
有一个原则:我们应该保守使用派生 state,因为它可能会引入 bug。
用 props 传入数据的话,组件可以被认为是受控(因为组件被父级传入的 props 控制)。数据只保存在组件内部的 state 的话,是非受控组件(因为外部没办法直接控制 state)。
为避免出现问题,有两种常见的方式
- 完全可控的组件
- 有 key 的非可控组件
如果没法使用 key 的话,这里分为两种情况
- 如果某些情况下 key 不起作用(可能是组件初始化的开销太大),一个麻烦但是可行的方案是在 getDerivedStateFromProps 观察 ID 的变化
- 更少见的情况是,即使没有合适的 key,我们也想重新创建组件。一种解决方案是给一个随机值或者递增的值当作 key,另外一种是通过 ref 得到组件实例,通过调用实例方法,强制重置内部状态
设计组件时,重要的是确定组件是受控组件还是非受控组件。
- 不要直接复制 props 的值到 state 中,而是去实现一个受控的组件
- 对于不受控的组件,当你想在 prop 变化(通常是 ID )时重置 state 的话,可以选择一下几种方式:
- 建议: 重置内部所有的初始 state,使用 key 属性
- 选项一:仅更改某些字段,观察特殊属性的变化(比如 props.userID)。
- 选项二:使用 ref 调用实例方法。
memoization
仅在输入变化时,重新计算 render 需要使用的值————这个技术叫做 memoization。
在使用 memoization 时,请记住这些约束:
- 大部分情况下, 每个组件内部都要引入 memoized 方法,已免实例之间相互影响。
- 一般情况下,我们会限制 memoization 帮助函数的缓存空间,以免内存泄漏。(上面的例子中,使用 memoize-one 只缓存最后一次的参数和结果)。