入门教程
状态提升:当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,需要把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中所有组件的状态数据就能够更方便地同步共享了。
为了提高可读性,我们把返回的 React 元素拆分成了多行,同时在最外层加了小括号,这样 JavaScript 解析的时候就不会在 return 的后面自动插入一个分号从而破坏代码结构了。
在 React 中,有一个命名规范,通常会将代表事件的监听 prop 命名为 on[Event],将处理事件的监听方法命名为 handle[Event] 这样的格式。
concat() 方法可能与你比较熟悉的 push() 方法不太一样,它并不会改变原数组,所以我们推荐使用 concat()
KEY
每当一个列表重新渲染时,React 会根据每一项列表元素的 key 来检索上一次渲染时与每个 key 所匹配的列表项。如果 React 发现当前的列表有一个之前不存在的 key,那么就会创建出一个新的组件。如果 React 发现和之前对比少了一个 key,那么就会销毁之前对应的组件。如果一个组件的 key 发生了变化,这个组件会被销毁,然后使用新的 state 重新创建一份。
key 是 React 中一个特殊的保留属性(还有一个是 ref,拥有更高级的特性)。当 React 元素被创建出来的时候,React 会提取出 key 属性,然后把 key 直接存储在返回的元素上。虽然 key 看起来好像是 props 中的一个,但是你不能通过 this.props.key 来获取 key。React 会通过 key 来自动判断哪些组件需要更新。组件是不能访问到它的 key 的。
我们强烈推荐,每次只要你构建动态列表的时候,都要指定一个合适的 key。如果你没有找到一个合适的 key,那么你就需要考虑重新整理你的数据结构了,这样才能有合适的 key。
函数组件
- 如果你想写的组件只包含一个 render 方法,并且不包含 state,那么使用函数组件就会更简单。我们不需要定义一个继承于 React.Component 的类,我们可以定义一个函数,这个函数接收 props 作为参数,然后返回需要渲染的元素。函数组件写起来并不像 class 组件那么繁琐,很多组件都可以使用函数组件来写。
定义组件最简单的方式就是编写 JavaScript 函数:
1
2
3
4
5
6
7
8
9
10
| function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
ES6:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
注意: 组件名称必须以大写字母开头。
|
Props的只读性
1
2
3
| function sum(a, b) {
return a + b;
}
|
- 这样的函数被称为“纯函数”,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。
相反,下面这个函数则不是纯函数,因为它更改了自己的入参:
1
2
3
| function withdraw(account, amount) {
account.total -= amount;
}
|
- React 非常灵活,但它也有一个严格的规则:
所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
正确地使用 State
- 不要直接修改 State,要使用setState()
- State 的更新可能是异步的
出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。
因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。
1
2
3
4
| // Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
|
要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:
1
2
3
4
5
6
7
| // Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
|
当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state。
- 这通常会被叫做“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。
事件处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// 此语法确保 `handleClick` 内的 `this` 已被绑定。
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
}
}
|
- 此语法问题在于每次渲染 LoggingButton 时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。我们通常建议在构造器中绑定或使用 class fields 语法来避免这类性能问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class LoggingButton extends React.Component {
// 此语法确保 `handleClick` 内的 `this` 已被绑定。
// 注意: 这是 *实验性* 语法。
handleClick = () => {
// class fields
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
|
1
2
| <button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
|
- 在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。
列表 & Key
- 一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串
- 一个好的经验法则是:在 map() 方法中的元素需要设置 key 属性。
React.lazy
- React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。
1
2
3
4
| 使用之前:
import OtherComponent from './OtherComponent';
使用之后:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
|
- React.lazy 目前只支持默认导出(default exports)。如果你想被引入的模块使用命名导出(named exports),你可以创建一个中间模块,来重新导出为默认模块。这能保证 tree shaking 不会出错,并且不必引入不需要的组件。
1
2
3
4
5
6
7
8
| // ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));
|
Context
- Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。
- Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
1
| const MyContext = React.createContext(defaultValue);
|
- 创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。
只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。
1
2
| <MyContext.Provider value={/* 某个值 */}>
MyClass.contextType = MyContext;
|
- 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
1
2
3
4
5
6
7
8
9
10
| class MyClass extends React.Component {
static contextType = MyContext;
render() {
let value = this.context;
/* 基于这个值进行渲染工作 */
}
}
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
|
1
2
3
4
| const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
|
错误边界
1
2
3
4
5
6
7
8
9
| static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
|
Refs 转发
- Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
| //3.React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数。
const FancyButton = React.forwardRef((props, ref) => (
//4.我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
//5.当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 1.我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
const ref = React.createRef();
// 2.我们通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>。
<FancyButton ref={ref}>Click me!</FancyButton>;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| function logProps(WrappedComponent) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
return LogProps;
}
// 使用
class FancyButton extends React.Component {
focus() {
// ...
}
// ...
}
// 我们导出 LogProps,而不是 FancyButton。
// 虽然它也会渲染一个 FancyButton。
export default logProps(FancyButton);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// 注意 React.forwardRef 回调的第二个参数 “ref”。
// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
// 然后它就可以被挂载到被 LogProps 包裹的子组件上。
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
|
Fragments
- React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
1
2
3
4
5
6
7
8
9
| render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
|
- 你可以使用一种新的,且更简短的语法来声明 Fragments。它看起来像空标签:
1
2
3
4
5
6
7
8
9
10
| class Columns extends React.Component {
render() {
return (
<>
<td>Hello</td>
<td>World</td>
</>
);
}
}
|
- key 是唯一可以传递给 Fragment 的属性。
高阶组件HOC
将不相关的 props 传递给被包裹的组件
- HOC 为组件添加特性。自身不应该大幅改变约定。HOC 返回的组件与原组件应保持类似的接口。
HOC 应该透传与自身无关的 props。大多数 HOC 都应该包含一个类似于下面的 render 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| render() {
// 过滤掉非此 HOC 额外的 props,且不要进行透传
const { extraProp, ...passThroughProps } = this.props;
// 将 props 注入到被包装的组件中。
// 通常为 state 的值或者实例方法。
const injectedProp = someStateOrInstanceMethod;
// 将 props 传递给被包装组件
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
|
最大化可组合性
包装显示名称以便轻松调试
- 请选择一个显示名称,以表明它是 HOC 的产物。
- 最常见的方式是用 HOC 包住被包装组件的显示名称。比如高阶组件名为 withSubscription,并且被包装组件的显示名称为 CommentList,显示名称应该为 WithSubscription(CommentList):
1
2
3
4
5
6
7
8
9
| function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* ... */}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
|
HOC注意事项
1
2
3
4
5
6
7
| render() {
// 每次调用 render 函数都会创建一个新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
return <EnhancedComponent />;
}
|
这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。
你可以在组件的生命周期方法或其构造函数中进行调用。
- 务必复制静态方法
当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法。
1
2
3
4
5
6
7
| // 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);
// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
|
为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:
1
2
3
4
5
6
| function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// 必须准确知道应该拷贝哪些方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
|
另一个可行的方案是再额外导出这个静态方法。
1
2
3
4
5
6
7
8
9
| // 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...单独导出该方法...
export { someFunction };
// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from './MyComponent.js';
|
JSX
实际上,JSX 仅仅只是 React.createElement(component, props, …children) 函数的语法糖。
1
2
3
4
5
6
7
8
9
| <MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
会编译为:
React.createElement(
MyButton,
{color: 'blue', shadowSize: 2},
'Click Me'
)
|
React 必须在作用域内
由于 JSX 会编译为 React.createElement 调用形式,所以 React 库也必须包含在 JSX 代码作用域内。
在如下代码中,虽然 React 和 CustomButton 并没有被直接使用,但还是需要导入:
1
2
3
4
5
6
7
| import React from 'react';
import CustomButton from './CustomButton';
function WarningButton() {
// return React.createElement(CustomButton, {color: 'red'}, null);
return <CustomButton color="red" />;
}
|
在 JSX 类型中使用点语法
1
2
3
4
5
6
7
8
9
10
11
| import React from 'react';
const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
}
}
function BlueDatePicker() {
return <MyComponents.DatePicker color="blue" />;
}
|
用户定义的组件必须以大写字母开头
以小写字母开头的元素代表一个 HTML 内置组件,比如 <div> 或者 <span>会生成相应的字符串 ‘div’ 或者 ‘span’ 传递给 React.createElement(作为参数)。大写字母开头的元素则对应着在 JavaScript 引入或自定义的组件,如 会编译为 React.createElement(Foo)。
我们建议使用大写字母开头命名自定义组件。如果你确实需要一个以小写字母开头的组件,则在 JSX 中使用它之前,必须将它赋值给一个大写字母开头的变量。
在运行时选择类型
你不能将通用表达式作为 React 元素类型。如果你想通过通用表达式来(动态)决定元素类型,你需要首先将它赋值给大写字母开头的变量。这通常用于根据 prop 来渲染不同组件的情况下:
1
2
3
4
5
6
7
8
9
10
11
12
| import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// 错误!JSX 类型不能是一个表达式。
return <components[props.storyType] story={props.story} />;
}
|
要解决这个问题, 需要首先将类型赋值给一个大写字母开头的变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
| import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// 正确!JSX 类型可以是大写字母开头的变量。
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
|
JSX 中的 Props
JavaScript 表达式作为 Props
你可以把包裹在 {} 中的 JavaScript 表达式作为一个 prop 传递给 JSX 元素。
1
| <MyComponent foo={1 + 2 + 3 + 4} />
|
if 语句以及 for 循环不是 JavaScript 表达式,所以不能在 JSX 中直接使用。
字符串字面量
你可以将字符串字面量赋值给 prop.
1
2
3
| <MyComponent message="hello world" />
// 等价
<MyComponent message={'hello world'} />
|
当你将字符串字面量赋值给 prop 时,它的值是未转义的。
1
2
3
| <MyComponent message="<3" />
// 等价
<MyComponent message={'<3'} />
|
Props 默认值为 “True”
如果你没给 prop 赋值,它的默认值是 true。
1
2
3
| <MyTextBox autocomplete />
// 等价
<MyTextBox autocomplete={true} />
|
通常,我们不建议不传递 value 给 prop,因为这可能与 ES6 对象简写混淆,{foo} 是 {foo: foo} 的简写,而不是 {foo: true}。
属性展开
如果你已经有了一个 props 对象,你可以使用展开运算符 … 来在 JSX 中传递整个 props 对象。
1
2
3
4
5
6
7
8
| function App1() {
return <Greeting firstName="Ben" lastName="Hector" />;
}
// 等价
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
|
你还可以选择只保留当前组件需要接收的 props,并使用展开运算符将其他 props 传递下去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| const Button = props => {
const { kind, ...other } = props;
const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
return <button className={className} {...other} />;
};
const App = () => {
return (
<div>
<Button kind="primary" onClick={() => console.log("clicked!")}>
Hello World!
</Button>
</div>
);
};
|
在上述例子中,kind 的 prop 会被安全的保留,它将不会被传递给 DOM 中的 <button> 元素。 所有其他的 props 会通过 …other 对象传递,使得这个组件的应用可以非常灵活。你可以看到它传递了一个 onClick 和 children 属性。
属性展开在某些情况下很有用,但是也很容易将不必要的 props 传递给不相关的组件,或者将无效的 HTML 属性传递给 DOM。我们建议谨慎的使用该语法。
JSX 中的子元素
包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children 传递给外层组件。
字符串字面量
你可以将字符串放在开始和结束标签之间,此时 props.children 就只是该字符串。
1
| <MyComponent>Hello world!</MyComponent>
|
这是一个合法的 JSX,MyComponent 中的 props.children 是一个简单的未转义字符串 “Hello world!"。
JSX 会移除行首尾的空格以及空行。与标签相邻的空行均会被删除,文本字符串之间的新行会被压缩为一个空格。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <div>Hello World</div>
<div>
Hello World
</div>
<div>
Hello
World
</div>
<div>
Hello World
</div>
|
JSX 子元素
子元素允许由多个 JSX 元素组成。
1
2
3
4
| <MyContainer>
<MyFirstComponent />
<MySecondComponent />
</MyContainer>
|
React 组件也能够返回存储在数组中的一组元素:
1
2
3
4
5
6
7
8
9
| render() {
// 不需要用额外的元素包裹列表元素!
return [
// 不要忘记设置 key :)
<li key="A">First item</li>,
<li key="B">Second item</li>,
<li key="C">Third item</li>,
];
}
|
JavaScript 表达式作为子元素
1
2
3
| <MyComponent>foo</MyComponent>
// 等价
<MyComponent>{'foo'}</MyComponent>
|
函数作为子元素
通常,JSX 中的 JavaScript 表达式将会被计算为字符串、React 元素或者是列表。不过,props.children 和其他 prop 一样,它可以传递任意类型的数据,而不仅仅是 React 已知的可渲染类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 调用子元素回调 numTimes 次,来重复生成组件
function Repeat(props) {
let items = [];
for (let i = 0; i < props.numTimes; i++) {
items.push(props.children(i));
}
return <div>{items}</div>;
}
function ListOfTenThings() {
return (
<Repeat numTimes={10}>
// 回调函数作为children传递
{(index) => <div key={index}>This is item {index} in the list</div>}
</Repeat>
);
}
|
布尔类型、Null 以及 Undefined 将会忽略
false, null, undefined, and true 是合法的子元素。但它们并不会被渲染。
1
2
3
4
5
6
7
8
9
10
11
| <div />
<div></div>
<div>{false}</div>
<div>{null}</div>
<div>{undefined}</div>
<div>{true}</div>
|
值得注意的是有一些 “falsy” 值,如数字 0,仍然会被 React 渲染。例如,以下代码并不会像你预期那样工作,因为当 props.messages 是空数组时,0 仍然会被渲染:
1
2
3
4
5
6
| <div>
{props.messages.length &&
<MessageList messages={props.messages} />
}
</div>
// 当messages是空数组,会在页面渲染出一个0
|
要解决这个问题,确保 && 之前的表达式总是布尔值:
1
2
3
4
5
| <div>
{props.messages.length > 0 &&
<MessageList messages={props.messages} />
}
</div>
|
反之,如果你想渲染 false、true、null、undefined 等值,你需要先将它们转换为字符串:
1
2
3
| <div>
My JavaScript variable is {String(myVariable)}.
</div>
|
shouldComponentUpdate
shouldComponentUpdate 的作用

节点 C2 的 shouldComponentUpdate 返回了 false,React 因而不会去渲染 C2,也因此 C4 和 C5 的 shouldComponentUpdate 不会被调用到。
对于 C1 和 C3,shouldComponentUpdate 返回了 true,所以 React 需要继续向下查询子节点。这里 C6 的 shouldComponentUpdate 返回了 true,同时由于渲染的元素与之前的不同使得 React 更新了该 DOM。
最后一个有趣的例子是 C8。React 需要渲染这个组件,但是由于其返回的 React 元素和之前渲染的相同,所以不需要更新 DOM。
显而易见,你看到 React 只改变了 C6 的 DOM。对于 C8,通过对比了渲染的 React 元素跳过了渲染。而对于 C2 的子节点和 C7,由于 shouldComponentUpdate 使得 render 并没有被调用。因此它们也不需要对比元素了。
Example
如果你的组件只有当 props.color 或者 state.count 的值改变才需要更新时,你可以使用 shouldComponentUpdate 来进行检查:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
|
你只要继承 React.PureComponent就行了。所以这段代码可以改成以下这种更简洁的形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
|
大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate。但它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| class ListOfWords extends React.PureComponent {
render() {
return <div>{this.props.words.join(',')}</div>;
}
}
class WordAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
words: ['marklar']
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 这部分代码很糟,而且还有 bug
const words = this.state.words;
words.push('marklar');
this.setState({words: words});
}
render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}
|
问题在于 PureComponent 仅仅会对新老 this.props.words 的值进行简单的对比。由于代码中 WordAdder 的 handleClick 方法改变了同一个 words 数组,使得新老 this.props.words 比较的其实还是同一个数组。即便实际上数组中的单词已经变了,但是比较结果是相同的。可以看到,即便多了新的单词需要被渲染, ListOfWords 却并没有被更新。
不可变数据的力量
避免该问题最简单的方式是避免更改你正用于 props 或 state 的值。
1
2
3
4
5
6
7
8
9
10
11
| handleClick() {
this.setState(state => ({
words: state.words.concat(['marklar'])
}));
}
// 或者使用ES6的扩展运算
handleClick() {
this.setState(state => ({
words: [...state.words, 'marklar'],
}));
};
|