- 原文地址:
- 原文作者:
- 译文出自:
- 本文永久链接:
- 译者:
- 校对者:
什么是反模式?反模式是软件开发中被认为是糟糕的编程实践的特定模式。同样的模式,可能在过去一度被认为是正确的,但是现在开发者们已经发现,从长远来看,它们会造成更多的痛苦和难以追踪的 Bug。
作为一个 UI 库,React 已经成熟,并且随着时间的推移,许多最佳实践也逐渐形成。我们将从数千名开发者集体的智慧中学习,他们曾用笨方法(the hard way)学习这些最佳实践。
此言不虚!
让我们开始吧!
1. 组件中的 bind() 与箭头函数
在使用自定义函数作为组件属性之前你必须将你的自定义函数写在 constructor
中。如果你是用 extends
关键字声明组件的话,自定义函数(如下面的 updateValue
函数)会失去 this
绑定。因此,如果你想使用 this.state
,this.props
或者 this.setState
,你还得重新绑定。
Demo
class app extends Component { constructor(props) { super(props); this.state = { name: '' }; this.updateValue = this.updateValue.bind(this); }updateValue(evt) { this.setState({ name: evt.target.value }); }render() { return () }}复制代码
问题
有两种方法可以将自定义函数绑定到组件的 this
。一种方法是如上面所做的那样,在 constructor
中绑定。另一种方法是在传值的时候作为属性的值进行绑定:
复制代码
这种方法有一个问题。由于 .bind()
每次运行时都会创建一个函数,这种方法会导致每次 render
**函数执行时都会创建一个新函数。**这会对性能造成一些影响。然而,在小型应用中这可能并不会造成显著影响。随着应用体积变大,差别就会开始显现。 有一个案例研究。
箭头函数所涉及的性能问题与 bind
相同。
this.setState({ name: evt.target.value }) } value={this.state.name} />复制代码
这种写法明显更清晰。可以看到 prop onChange
函数中发生了什么。但是,这也导致了每次 input
组件渲染时都会创建一个新的匿名函数。因此,箭头函数有同样的性能弊端。
解决方案
避免上述性能弊端的最佳方法是在函数本身的构造器中进行绑定。这样,在组件创建时仅创建了一个额外函数,即使再次执行 render
也会使用该函数。
有一种情况经常发生就是你忘记在构造函数中去 bind
你的函数,然后就会收到报错(Cannot find X on undefined.)。Babel 有个插件可以让我们使用箭头语法写出自动绑定的函数。插件是 。现在你可以这样编写组件:
class App extends Component { constructor(props) { super(props); this.state = { name: '' };// 看!无需在此处进行函数绑定!}updateValue = (evt) => { this.setState({ name: evt.target.value }); }render() { return () }}复制代码
延伸阅读
2. 在 key prop 中使用索引
遍历元素集合时,key 是必不可少的 prop。key 应该是稳定,唯一,可预测的,这样 React 才能追踪元素。key 是用来帮助 React 轻松调和虚拟 DOM 与真实 DOM 间的差异的。然而,使用某些值集例如数组索引,可能会导致你的应用崩溃或是渲染出错误数据。
Demo
{elements.map((element, index) =>)}复制代码
问题
当子元素有了 key,React 就会使用 key 来匹配原始树结构和后续树结构中的子元素。**key 被用于作身份标识。**如果两个元素有同样的 key,React 就会认为它们是相同的。当 key 冲突了,即超过两个元素具有同样的 key,React 就会抛出警告。
警告出现重复的 key。
是 CodePen 上使用索引作为 key 可能导致的问题的一个示例。
解决方案
被使用的 key 应该是:
- 唯一的: 元素的 key 在它的兄弟元素中应该是唯一的。没有必要拥有全局唯一的 key。
- 稳定的: 元素的 key 不应随着时间,页面刷新或是元素重新排序而变。
- 可预测的: 你可以在需要时拿到同样的 key,意思是 key 不应是随机生成的。
数组索引是唯一且可预测的。然而,并不稳定。同样,随机数或时间戳不应被用作为 key。
由于随机数既不唯一也不稳定,使用随机数就相当于根本没有使用 key。即使内容没有改变,组件也会每次都重新渲染。
时间戳既不稳定也不可预测。**时间戳也会一直递增。**因此每次刷新页面,你都会得到新的时间戳。
通常,你应该依赖于数据库生成的 ID 如关系数据库的主键,Mongo 中的对象 ID。如果数据库 ID 不可用,你可以生成内容的哈希值来作为 key。关于哈希值的更多内容可以在阅读。
延伸阅读
- .
3. setState() 是异步的
React 组件主要由三部分组成:state
,props
和标记(或其它组件)。props 是不可变的,state 是可变的。state 的改变会导致组件重新渲染。如果 state 是由组件在内部管理的,则使用 this.setState
来更新 state。关于这个函数有几件重要的事需要注意。我们来看看:
Demo
class MyComponent extends Component { constructor(props) { super(props); this.state = { counter: 350 }; } updateCounter() { // 这行代码不会生效 this.state.counter = this.state.counter + this.props.increment; // --------------------------------- // 不会如预期生效 this.setState({ counter: this.state.counter + this.props.increment; // 可能不会渲染 }); this.setState({ counter: this.state.counter + this.props.increment; // this.state.counter 的值是什么? }); // --------------------------------- // 如期生效 this.setState((prevState, props) => ({ counter: prevState.counter + props.increment })); this.setState((prevState, props) => ({ counter: prevState.counter + props.increment })); }}复制代码
问题
请注意第 11 行代码。如果你直接修改了 state,组件并不会重新渲染,修改也不会有任何体现。这是因为 state 是进行的。你应该永远都使用 setState
来改变 state 的值。
现在,如果你在 setState
中通过当前的 state
值来更新至下一个 state (正如第 15 行代码所做的),React 可能不会重新渲染。这是因为 state
和 props
是异步更新的。也就是说,DOM 并不会随着 setState
被调用就立即更新。React 会将多次更新合并到同一批次进行更新,然后渲染 DOM。查询 state
对象时,你可能会收到已经过期的值。也提到了这一点:
由于
this.props
和this.state
是异步更新的,你不应该依赖它们的值来计算下一个 state。
另一个问题出现于一个函数中有多次 setState
调用时,如第 16 和 20 行代码所示。counter 的初始值是 350。假设 this.props.increment
的值是 10。你可能以为在第 16 行代码第一次调用 setState
后,counter 的值会变成 350+10 = **360。**并且,当第 20 行代码再次调用 setState
时,counter 的值会变成 360+10 = 370。然而,这并不会发生。第二次调用时所看到的 counter
的值仍为 350。**这是因为 setState 是异步的。**counter 的值直到下一个更新周期前都不会发生改变。setState 的执行在中等待,直到 updateCounter
执行完毕前,setState
都不会执行, 因此 state
的值也不会更新。
解决方案
你应该看看第 27 和 31 行代码使用 setState
的方式。以这种方式,你可以给 setState
传入一个接收 currentState 和 currentProps 作为参数的函数。这个函数的返回值会与当前 state 合并以形成新的 state。
延伸阅读
- 对于为什么
setState
是异步的所做的超级棒的
4. 初始值中的 props
React 文档提到这也是反模式:
在 getInitialState 中使用 props 来生成 state 经常会导致重复的“事实来源”,即真实数据的所在位置。这是因为 getInitialState 仅仅在组件第一次创建时被调用。
Demo
import React, { Component } from 'react'class MyComponent extends Component { constructor(props){ super(props); this.state = { someValue: props.someValue, }; }}复制代码
问题
constructor
(getInitialState) 仅仅在组件创建阶段被调用。也就是说,constructor
只被调用一次。因此,当你下一次改变 props
时,state 并不会更新,它仍然保持为之前的值。
经验尚浅的开发者经常设想 props
的值与 state 是同步的,随着 props
改变,state
也会随之变化。然而,真实情况并不是这样。
解决方案
如果你需要特定的行为即你希望 state 仅由 props 的值生成一次的话,可以使用这种模式。state 将由组件在内部管理。
在另一个场景下,你可以通过生命周期方法 componentWillReceiveProps
保持 state 与 props 的同步,如下所示。
import React, { Component } from 'react'class MyComponent extends Component { constructor(props){ super(props); this.state = { someValue: props.someValue, }; } componentWillReceiveProps(nextProps){ if (nextProps.inputValue !== this.props.inputValue) { this.setState({ inputVal: nextProps.inputValue }) } }}复制代码
要注意,关于使用 componentWillReceiveProps
有一些注意事项。你可以在中阅读。
最佳方法是使用状态管理库如 Redux 去 state 和组件。
延伸阅读
5. 组件命名
在 React 中,如果你想使用 JSX 渲染你的组件,组件名必须以大写字母开头。
Demo
// 不会生效 :( 复制代码 // 可以生效!
问题
如果你创建了一个 app
组件,以 <app label="Save" />
的形式去渲染它,React 将会报错。
使用非大写自定义组件时的警告。
报错表明 <app>
是无法识别的。只有 HTML 元素和 SVG 标签可以以小写字母开头。因此 <div />
是可以识别的,<app>
却不能。
解决方案
你需要确保在 JSX 中使用的自定义组件是以大写字母开头的。
但是也要明白,声明组件无需遵从这一规则。因此,你可以这样写:
// 在这里以小写字母开头是可以的class primaryButton extends Component { render() { return ; }}export default primaryButton;// 在另一个文件中引入这个按钮组件。要确保以大写字母开头的名字引入。import PrimaryButton from 'primaryButton';复制代码
延伸阅读
以上这些都是 React 中不直观,难以理解也容易出现问题的地方。如果你知道任何其它的反模式,请回复本文。?
我还写了一篇
如果你仍在学习如何构建 React 项目,这个 可以帮助你理解 React 构建系统的多个方面。
我写作 JavaScript,Web 开发与计算机科学领域的文章。关注我可以每周阅读新文章。如果你喜欢,可以分享本文。
关注我 @ @ @ .
✉️ 订阅 CodeBurst的每周邮件 , ?可以在 上关注 CodeBurst, 浏览 ?️ , 和 ?️ 。
是一个翻译优质互联网技术文章的社区,文章来源为 上的英文分享文章。内容覆盖 、、、、、、、等领域,想要查看更多优质译文请持续关注 、、。