使用 shouldComponentUpdate 可以有效地去避免不必要的 render 方法的执行。
对于 React 的 render 方法,默认情况下,不管传入的 state 或 props 是否变化,都会触发重新渲染,这里重新渲染指的是虚拟 DOM 的渲染,不是真实 DOM 的重绘。即使真实 DOM 不变化,当 React 应用足够庞大的时候,重新去触发 render 也是一笔不小的开销,这个问题可以使用 shouldComponentUpdate 解决。
例 子 :了解 render 的执行
在子组件 listItem.jsx 的 render 方法中添加一条语句做标记,每次执行了 render 方法都会在控制台打印出 “item is rendering”。
页面初始化时控制台输出 2 次 "item is rendering",因为有 2 个商品
点击其中一个 “删除” 按钮之后控制台输出 1 次 ”item is rendering“,因为有 1 个商品。但是留下的商品的任何数据都没有发生变化与原来一致。
将子组件 listItem.jsx 中的方法 handleIncrease 中的 count 修改为 3,使得点击页面上 “ + ” 按钮的值 count 的值为 3,传入 render 的 count 值不变。
在页面上点击几下 "+" 按钮,count 值不变一直为 3 ,但是点击 “ + ” 按钮几次 render 就被执行几次。
例子:了解 shouldComponentUpdate 的 this.props、this.state、nextProps、nextState
shouldComponentUpdate 是重新渲染时 render 方法执行前被调用的,它接受 2 个参数,第一个是 nextProps,第二个是 nextState。nextProps 代表下一个 props , nextState 代表下一个 state。
在 listItem.jsx 里使用 shouldComponentUpdata。将目前的 props、下一个 props、目前的 state、下一个 state 打印出来看看。
import React, { Component } from 'react';
import style from './listItem.module.css';
import classnames from 'classnames/bind'
const cls = classnames.bind(style);
class ListItem extends Component {
constructor(props){
super(props)
this.state = {
count : 0
handleDecrease = () => {
this.setState({
count : this.state.count - 1
handleIncrease = () => {
this.setState({
count : 3
shouldComponentUpdate(nextProps, nextState){
console.log('props', this.props, nextProps);
console.log('state', this.state, nextState)
render() {
console.log('item is rendering');
return (
<div className="row mb-3">
<div className="col-6 themed-grid-col">
<span className={ cls('title', 'list-title') }>
{this.props.data.name}
</span>
</div>
<div className="col-1 themed-grid-col">¥{this.props.data.price}</div>
<div className={`col-2 themed-grid-col${this.state.count ? '' : '-s'}`}>
<button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button>
<span className={ cls('digital') }>{this.state.count}</span>
<button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button>
</div>
<div className="col-2 themed-grid-col">¥{this.props.data.price * this.state.count}</div>
<div className="col-1 themed-grid-col">
<button
onClick={()=>{this.props.onDelete(this.props.data.id)}}
className="btn btn-danger btn-sm"
type="button"
</button>
</div>
</div>
export default ListItem;
listItem.jsx
页面表现:
点击 “删除” 按钮,可以看到目前的 props、下一个 props、目前的 state、下一个 state。
可以分别展开查看目前的 props、下一个 props、目前的 state、下一个 state。
例子:使用 shouldComponentUpdate 阻止 render 重新渲染
下面实现:传入的 state 不变化时,不触发重新渲染。点击 “+” 按钮触发的事件是修改 count 值为 3 。
import React, { Component } from 'react';
import style from './listItem.module.css';
import classnames from 'classnames/bind'
const cls = classnames.bind(style);
class ListItem extends Component {
constructor(props){
super(props)
this.state = {
count : 0
handleDecrease = () => {
this.setState({
count : this.state.count - 1
handleIncrease = () => {
this.setState({
count : 3
shouldComponentUpdate(nextProps, nextState){
if( this.state.count === nextState.count ) return false
return true
render() {
console.log('item is rendering');
return (
<div className="row mb-3">
<div className="col-6 themed-grid-col">
<span className={ cls('title', 'list-title') }>
{this.props.data.name}
</span>
</div>
<div className="col-1 themed-grid-col">¥{this.props.data.price}</div>
<div className={`col-2 themed-grid-col${this.state.count ? '' : '-s'}`}>
<button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button>
<span className={ cls('digital') }>{this.state.count}</span>
<button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button>
</div>
<div className="col-2 themed-grid-col">¥{this.props.data.price * this.state.count}</div>
<div className="col-1 themed-grid-col">
<button
onClick={()=>{this.props.onDelete(this.props.data.id)}}
className="btn btn-danger btn-sm"
type="button"
</button>
</div>
</div>
export default ListItem;
listItem.jsx
页面表现:
因为点击 “ + ” 按钮触发的事件修改 count 为 3 ,state 值不发生变化,所以 render 没有再执行。
下面实现:当前 props 与 下一 props 相同时,不触发重新渲染。
注意:不能直接判断当前 props 跟下一个 props,因为他们是两个不同的引用,所以要通过判断 props 的某些属性来判断当前 props 与下一 props 是否相同。本例通过 id 进行判断。
import React, { Component } from 'react';
import style from './listItem.module.css';
import classnames from 'classnames/bind'
const cls = classnames.bind(style);
class ListItem extends Component {
constructor(props){
super(props)
this.state = {
count : 0
handleDecrease = () => {
this.setState({
count : this.state.count - 1
handleIncrease = () => {
this.setState({
count : 3
shouldComponentUpdate(nextProps, nextState){
if( this.props.id === nextProps.id ) return false
return true
render() {
console.log('item is rendering');
return (
<div className="row mb-3">
<div className="col-6 themed-grid-col">
<span className={ cls('title', 'list-title') }>
{this.props.data.name}
</span>
</div>
<div className="col-1 themed-grid-col">¥{this.props.data.price}</div>
<div className={`col-2 themed-grid-col${this.state.count ? '' : '-s'}`}>
<button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button>
<span className={ cls('digital') }>{this.state.count}</span>
<button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button>
</div>
<div className="col-2 themed-grid-col">¥{this.props.data.price * this.state.count}</div>
<div className="col-1 themed-grid-col">
<button
onClick={()=>{this.props.onDelete(this.props.data.id)}}
className="btn btn-danger btn-sm"
type="button"
</button>
</div>
</div>
export default ListItem;
listItem.jsx
页面表现:
点击 “ 删除 ” 按钮将商品删光,从控制台可以看出删除商品没有执行 render。
不同的引用指向堆内存的同一个对象,所以,当我们去做判断的时候,因为是在内存中的同一个对象,所以会判断为 true。当使用了 PureComponent ,UI 并不会进行更新。
const _list = this.state.listData
只有使用了不可变数据去生成一个新对象,这时候新对象与原来的 state 引用的是不同的对象,这时才可以进行正确的比较。不过,这个比较是浅复制的比较,会比较每一个 key 是否两者都有,对于数据有深层次嵌套的比较一般会使用 JSON.stringify 和 JSON.parse,或者会使用类似 Immutable 这样的 JS 库管理不可变的数据。
const _list = this.state.listData.concat([])
所以,在实际的 React 应用中,尽可能地使用 PureComponent 去优化 React 应用,同时,也要去使用不可变数据去修改 state 值或者 props 值保证数据引用不出错。使用不可变数据可以避免引用带来的副作用,使整个程序的数据变得易于管理。
三、单一数据源
所有相同的子组件应该有一个主状态,然后使用这个状态以 props 形式传递给子组件。
例 子 : 没有使用单一数据源会造成的问题
给购物车添加一个重置按钮,当点击 “重置” ,购物车的所有商品的数量都变为 0。
父组件 App.js:
给 listData 增加一个 value 值,模拟从后端传过来的购物车数量的初始值。
添加一个 “重置” 按钮,点击按钮调用 handleReset。
在 handleReset 里使用 map 方法创建新数组,新数组的 value 值为 0 ,使用 setState 方法将新数组赋给 listData。
import React, { PureComponent } from 'react';
import ListItem from './components/listItem'
class App extends PureComponent {
constructor( props ){
super(props)
this.state = {
listData : [
id: 1,
name: '红苹果',
price: 2,
value: 4
id: 2,
name: '青苹果',
price: 3,
value: 2
renderList(){
return this.state.listData.map( item => {
return <ListItem key={item.id} data={ item } onDelete={this.handleDelete} />
handleDelete = (id) => {
const listData = this.state.listData.filter( item => item.id !== id )
this.setState({
listData
handleReset = () => {
const _list = this.state.listData.map( item => {
const _item = {...item}
_item.value = 0
return _item
this.setState({
listData : _list
render() {
return(
<div className="container">
<button onClick={this.handleReset} className="btn btn-primary">重置</button>
{ this.state.listData.length === 0 && <div className="text-center">购物车是空的</div> }
{ this.renderList() }
</div>
export default App;
父组件 App.js
子组件 listItem.jsx:
将 state 值 count 初始化为 value 值
import React, { PureComponent } from 'react';
import style from './listItem.module.css';
import classnames from 'classnames/bind'
const cls = classnames.bind(style);
class ListItem extends PureComponent {
constructor(props){
super(props)
this.state = {
count : this.props.data.value
handleDecrease = () => {
this.setState({
count : this.state.count - 1
handleIncrease = () => {
this.setState({
count : 3
render() {
console.log('item is rendering');
return (
<div className="row mb-3">
<div className="col-6 themed-grid-col">
<span className={ cls('title', 'list-title') }>
{this.props.data.name}
</span>
</div>
<div className="col-1 themed-grid-col">¥{this.props.data.price}</div>
<div className={`col-2 themed-grid-col${this.state.count ? '' : '-s'}`}>
<button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button>
<span className={ cls('digital') }>{this.state.count}</span>
<button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button>
</div>
<div className="col-2 themed-grid-col">¥{this.props.data.price * this.state.count}</div>
<div className="col-1 themed-grid-col">
<button
onClick={()=>{this.props.onDelete(this.props.data.id)}}
className="btn btn-danger btn-sm"
type="button"
</button>
</div>
</div>
export default ListItem;
子组件 listItem.jsx
页面表现:
点击 “重置” 按钮之后,页面并没有发生变化,查看控制台 Component 处,可以看到在父组件的 state 里 value 值已经被设置为了 0。
子组件 listItem 传入的 props 的 value 也是 0,然而 state 还是原来的数据。如果点击 “ + ”“ - ” 按钮,state 中的 count 值会变,如果点击 “重置” 按钮,state 中的 count 值不会变。
以上,就是一个没有使用单一数据源造成问题的例子。
单一数据源原则
使用单一数据源,当主状态的任何一部分发生改变,它会自动更新以这部分为 props 的子组件,这种变化是从上而下传达到子组件的。
那要怎么做呢?首先,将子组件的 count 状态去掉,然后将所有的数据通过父组件传递给子组件,这时候子组件也被称为受控组件,在子组件中绑定 props 传入的函数,让父组件去操作数据。
例子:使用单一数据源
在上面例子的基础上进行改动。
子组件 listItem.jsx:
一般在设计比较好的组件中,比较少用到 state,一般只有一个 render 方法。
将 constructor 创建的 state 删除。
将 render 方法中用到的 state 都改为 props 传入的形式,将所有的数据通过父组件传递给子组件。
在子组件 react 元素上,绑定 props 传入的函数 onIncrese 跟 onDecrease 并带入参数。(可参考 onDelete 相关笔记: https://www.cnblogs.com/xiaoxuStudy/p/13327924.html#four )
import React, { PureComponent } from 'react';
import style from './listItem.module.css';
import classnames from 'classnames/bind'
const cls = classnames.bind(style);
class ListItem extends PureComponent {
render() {
console.log('item is rendering');
return (
<div className="row mb-3">
<div className="col-6 themed-grid-col">
<span className={ cls('title', 'list-title') }>
{this.props.data.name}
</span>
</div>
<div className="col-1 themed-grid-col">¥{this.props.data.price}</div>
<div className={`col-2 themed-grid-col${this.props.data.value ? '' : '-s'}`}>
<button
onClick={()=>{this.props.onDecrease(this.props.data.id)}}
type="button" className="btn btn-primary"
>-</button>
<span className={ cls('digital') }>{this.props.data.value}</span>
<button
onClick={()=>{this.props.onIncrease(this.props.data.id)}}
type="button" className="btn btn-primary"
>+</button>
</div>
<div className="col-2 themed-grid-col">¥{this.props.data.price * this.props.data.value}</div>
<div className="col-1 themed-grid-col">
<button
onClick={()=>{this.props.onDelete(this.props.data.id)}}
type="button" className="btn btn-danger btn-sm"
</button>
</div>
</div>
export default ListItem;
子组件 listItem.jsx
父组件 App.js:
在父组件定义好事件处理函数 handleIncrease 跟 handleDecrease,并通过 props 向子组件传递。
import React, { PureComponent } from 'react';
import ListItem from './components/listItem'
class App extends PureComponent {
constructor( props ){
super(props)
this.state = {
listData : [
id: 1,
name: '红苹果',
price: 2,
value: 4
id: 2,
name: '青苹果',
price: 3,
value: 2
renderList(){
return this.state.listData.map( item => {
return <ListItem
key={item.id}
data={ item }
onDelete={this.handleDelete}
onIncrease={this.handleIncrease}
onDecrease={this.handleDecrease}
handleDelete = (id) => {
const listData = this.state.listData.filter( item => item.id !== id )
this.setState({
listData
handleReset = () => {
const _list = this.state.listData.map( item => {
const _item = {...item}
_item.value = 0
return _item
this.setState({
listData : _list
handleIncrease = (id) => {
const _data = this.state.listData.map( item => {
if( item.id === id ){
const _item = {...item}
_item.value++
return _item
}else{
return item
this.setState({
listData : _data
handleDecrease = (id) => {
const _data = this.state.listData.map( item => {
if( item.id === id ){
const _item = {...item}
_item.value--
if( _item.value < 0 ) _item.value = 0
return _item
}else{
return item
this.setState({
listData : _data
render() {
return(
<div className="container">
<button onClick={this.handleReset} className="btn btn-primary">重置</button>
{ this.state.listData.length === 0 && <div className="text-center">购物车是空的</div> }
{ this.renderList() }
</div>
export default App;
父组件 App.js
页面表现:
点击 “重置” 之后,商品数量变为 0
随意点击加减按钮,点 “ + ” 会数量加 1,点 “ - ” 数量会减 1 ,但是数量不会变为负数
将 listItem.jsx 的 state 去除,子组件 listItem.jsx 的数据全部接受于父组件 App.js,这时 listItem.jsx 也被称为受控组件。所有,当开始设计应用结构时,应该尽量组织好组件之间的框架和数据传递的方式,尽可能采用单一数据源的方式,将子组件需要的数据都由父组件传入。
四、状态提升
多个组件需要对同一个数据的变化做出反应的时候,也就是操作同一个源数据的时候,建议将共享状态提升到最近的共同父组件去。
需求:购物车页面包含导航栏跟商品列表。导航栏显示总商品数、重置按钮,商品列表可以实现加减商品数量、删除商品。
效果预览:
购物车应用结构:
App:是公用的父组件。
NavBar:是 App 的子组件,负责头部导航栏的内容。
ListPage:是 App 的子组件,负责商品列表的渲染。
ListItem:是 ListPage 的子组件,负责每个具体商品的展示。
App 存储数据,通过向下传递数据的方式传入 props 将数据传给 NavBar、ListPage,ListPage 再将数据传给 ListItem,这样的形式就叫做状态提升。状态提升主要是用来处理父组件和子组件的数据传递,它可以让数据流动自顶向下,单向流动。所有组件的数据都是来自于它们的父辈组件,本例中是 App,父辈组件 App 统一存储和修改数据然后将其传入子组件中,子组件调用事件处理函数来使用父组件的方法,控制 state 数据的更新,从而完成整个应用的更新。
下面通过代码理解状态提升。
import React, { PureComponent } from 'react';
import Navbar from "./components/navbar"
import ListPage from './components/listPage'
class App extends PureComponent {
constructor( props ){
super(props)
this.state = {
listData : [
id: 1,
name: '红苹果',
price: 2,
value: 4
id: 2,
name: '青苹果',
price: 3,
value: 2
handleDelete = (id) => {
const listData = this.state.listData.filter( item => item.id !== id )
this.setState({
listData
handleReset = () => {
const _list = this.state.listData.map( item => {
const _item = {...item}
_item.value = 0
return _item
this.setState({
listData : _list
handleIncrease = (id) => {
const _data = this.state.listData.map( item => {
if( item.id === id ){
const _item = {...item}
_item.value++
return _item
}else{
return item
this.setState({
listData : _data
handleDecrease = (id) => {
const _data = this.state.listData.map( item => {
if( item.id === id ){
const _item = {...item}
_item.value--
if( _item.value < 0 ) _item.value = 0
return _item
}else{
return item
this.setState({
listData : _data
render() {
return(
<Navbar
onReset = {this.handleReset}
total = {this.state.listData.length}
<ListPage
data = {this.state.listData}
handleDecrease = {this.handleDecrease}
handleIncrease = {this.handleIncrease}
handleDelete = {this.handleDelete}
export default App;
App.js
return(
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<div className="container">
<div className="wrap">
<span className="title">NAVBAR</span>
<span className="badge badge-pill badge-primary ml-2 mr-2">
{this.props.total}
</span>
<button
onClick={this.props.onReset}
className="btn btn-outline-success my-2 my-sm-0 fr"
type="button"
Reset
</button>
</div>
</div>
</nav>
export default NavBar;
navbar.jsx
import React, { PureComponent } from 'react';
import ListItem from './listItem'
class ListPage extends PureComponent {
renderList(){
return this.props.data.map( item => {
return <ListItem
key={item.id}
data={ item }
onDelete={this.props.handleDelete}
onIncrease={this.props.handleIncrease}
onDecrease={this.props.handleDecrease}
render() {
return (
<div className="container">
{ this.props.data.length === 0 && <div className="text-center">购物车是空的</div> }
{ this.renderList() }
</div>
export default ListPage;
listPage.jsx
import React, { PureComponent } from 'react';
import style from './listItem.module.css';
import classnames from 'classnames/bind'
const cls = classnames.bind(style);
class ListItem extends PureComponent {
render() {
console.log('item is rendering');
return (
<div className="row mb-3">
<div className="col-6 themed-grid-col">
<span className={ cls('title', 'list-title') }>
{this.props.data.name}
</span>
</div>
<div className="col-1 themed-grid-col">¥{this.props.data.price}</div>
<div className={`col-2 themed-grid-col${this.props.data.value ? '' : '-s'}`}>
<button
onClick={()=>{this.props.onDecrease(this.props.data.id)}}
type="button" className="btn btn-primary"
>-</button>
<span className={ cls('digital') }>{this.props.data.value}</span>
<button
onClick={()=>{this.props.onIncrease(this.props.data.id)}}
type="button" className="btn btn-primary"
>+</button>
</div>
<div className="col-2 themed-grid-col">¥{this.props.data.price * this.props.data.value}</div>
<div className="col-1 themed-grid-col">
<button
onClick={()=>{this.props.onDelete(this.props.data.id)}}
type="button" className="btn btn-danger btn-sm"
</button>
</div>
</div>
export default ListItem;
listItem.jsx
页面表现:
加、减、删除按钮都能正常使用。下面测试导航栏的 “Reset” 按钮,点击 “Reset” 按钮之后商品数量都变为 0 了。
测试导航栏的显示的商品总数,点击 “删除” 按钮之后,商品总数变为 1。
总结:当子组件都要控制同样一个数据源的时候,需要将整个数据提升到它们共同的父组件中,然后再通过父组件赋值的方式传递给子组件,并由父组件统一地对数据进行管理与存储。
五、使用无状态组件
Stateful 和 Stateless 的区别
1. Stateful
有状态组件也被称为类组件、容器组件。用 class 创建的组件是可以使用 state 状态的,同时,它也是一个像容器一样可以包含其它的无状态组件的组件。
2. Stateless
无状态组件也被称为函数组件、展示组件。它是通过纯函数的方法来定义的,而它所有的数据都是来自于它的父组件,它仅起到一个展示的作用。
何时使用何种组件
尽可能通过状态提升原则,将需要的状态提取到父组件中,而其他的组件使用无状态组件编写。
尽可能使用无状态组件,尽少使用状态组件。因为无状态组件会使应用变得简单、可维护,会使整个数据流更加清晰,是一个单一的从上而下的数据流,可以非常容易地精确地定位到需要改变哪个状态去对应 UI。在必须使用状态的时候,编写有状态组件并在组件内部去组合其它无状态组件。
import React, {PureComponent} from 'react';
class NavBar extends PureComponent {
render(){
return(
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<div className="container">
<div className="wrap">
<span className="title">NAVBAR</span>
<span className="badge badge-pill badge-primary ml-2 mr-2">
{this.props.total}
</span>
<button
onClick={this.props.onReset}
className="btn btn-outline-success my-2 my-sm-0 fr"
type="button"
Reset
</button>
</div>
</div>
</nav>
export default NavBar;
有状态组件 navbar.jsx
在上面例子的基础上,将有状态组件 navbar.jsx 改成无状态组件。
在无状态组件中没有 render 方法,只需要在 return 中返回一段 React 元素。不能使用 this 关键字去引用 props。
import React from 'react';
const NavBar = ( props ) => {
return (
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<div className="container">
<div className="wrap">
<span className="title">NAVBAR</span>
<span className="badge badge-pill badge-primary ml-2 mr-2">
{props.total}
</span>
<button
onClick={props.onReset}
className="btn btn-outline-success my-2 my-sm-0 fr"
type="button"
Reset
</button>
</div>
</div>
</nav>
export default NavBar;
无状态组件 navbar.jsx
还有另一种更简单的写法,将 props 的内容解构出来,然后直接调用传入的参数。
import React from 'react';
const NavBar = ( {total, onReset} ) => {
return (
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<div className="container">
<div className="wrap">
<span className="title">NAVBAR</span>
<span className="badge badge-pill badge-primary ml-2 mr-2">
{total}
</span>
<button
onClick={onReset}
className="btn btn-outline-success my-2 my-sm-0 fr"
type="button"
Reset
</button>
</div>
</div>
</nav>
export default NavBar;
无状态组件 navbar.jsx
页面表现同上