首页 > 技术文章 > React 【State进阶】通过条件判断优化渲染、使用不可变数据、单一数据源、状态提升、使用无状态组件

xiaoxuStudy 2020-07-23 22:10 原文

目录:

1. 通过条件判断优化渲染

      shouldComponentUpdate

      PureComponent(推荐使用)

2. 使用不可变数据

3. 单一数据源

4. 状态提升

5. 使用无状态组件

 

一、通过条件判断优化渲染

 例 子 

目前的页面表现如下,点击 “删除” 按钮之后在控制台会输出商品 id。

(例子相关笔记:https://www.cnblogs.com/xiaoxuStudy/p/13327924.html#four )

import React, { Component } from 'react';
import ListItem from './components/listItem'

class App extends Component {
    constructor( props ){
        super(props)
        this.state = {
            listData : [
                {
                    id: 1,
                    name: '红苹果',
                    price: 2
                },
                {
                    id: 2,
                    name: '青苹果',
                    price: 3
                },
            ]
        }
    }
    renderList(){
        return this.state.listData.map( item => {
            return <ListItem key={item.id} data={ item }  onDelete={this.handleDelete} />
        })
    }
    handleDelete = (id) => {
        console.log( 'id:', id );
    }
    render() { 
        return(
            <div className="container">
                { this.state.listData.length === 0 && <div className="text-center">购物车是空的</div> }
                { this.renderList() }
            </div>
        )
    }
}
 
export default App;
父组件 App.js
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 : this.state.count + 1
        })
    }
    render() { 
        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

下面在以上代码的基础上实现点击 “删除” 按钮之后该商品被删除的功能。

App.js:

  当点击删除按钮时,会触发 listItem.jsx 的点击事件执行 listItem.jsx 中的从 App.js 传入的 onDelete 并且传入参数 id,在父组件中 onDelete 的值是函数 handleDelete。所以,点击按钮就会执行父组件中的 handleDelete 函数。如果想要实现点击 “ 删除 ” 按钮商品被删除,需要在父组件中去除当前选定的商品,以上已经实现了从子组件向父组件传递商品 id,以下只需要通过修改父组件的 state 来删除选定的商品。

  在 handleDelete 中,定义 listData 数组,使用 filter 方法去返回一个新的数组,过滤条件是 item 的 id 不等于传入的 id。使用 setState 方法将新创建的数组传给原来的数组 listData。

import React, { Component } from 'react';
import ListItem from './components/listItem'

class App extends Component {
    constructor( props ){
        super(props)
        this.state = {
            listData : [
                {
                    id: 1,
                    name: '红苹果',
                    price: 2
                },
                {
                    id: 2,
                    name: '青苹果',
                    price: 3
                },
            ]
        }
    }
    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
        })
    }
    render() { 
        return(
            <div className="container">
                { this.state.listData.length === 0 && <div className="text-center">购物车是空的</div> }
                { this.renderList() }
            </div>
        )
    }
}
 
export default App;
App.js
    handleDelete = (id) => {
        const _list = this.state.listData.filter( item => item.id !== id )
        this.setState({
            listData : _list
        })
    }
handleDelete另一种写法

页面表现:

  点击 “删除” 按钮之后,商品消失。

 

shouldComponentUpdate

  使用 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。

PureComponent

  使用 PureComponnet 能达到跟使用 shouldComponentUpdate 相同效果。

  例 子  

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 : 0
        }
    }
    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

 

二、使用不可变数据

  当使用 setState 的时候,需要使用不可变数据。(相关笔记:https://www.cnblogs.com/xiaoxuStudy/p/13336594.html#three

  例子 :没有使用不可变数据造成的问题 

  给页面添加一个 “减一个” 按钮,理想中的效果是点击按钮执行函数 handleMount 在页面上减少一个商品。

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
                },
                {
                    id: 2,
                    name: '青苹果',
                    price: 3
                },
            ]
        }
    }
    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
        })
    }
    handleAmount = () => {
        const _list = this.state.listData
        _list.pop()
        this.setState({
            listData : _list
        })
    }
    render() { 
        return(
            <div className="container">
                <button onClick={this.handleAmount} className="btn btn-primary">减一个</button>
                { this.state.listData.length === 0 && <div className="text-center">购物车是空的</div> }
                { this.renderList() }
            </div>
        )
    }
}
 
export default App;
App.js

页面表现:

  点击 “减一个” 按钮之后,state 值余下 1 个商品,但是页面并没有更新,仍然存在 2 个商品。这就是没有使用不可变数据造成的问题。

解决方法:

  使用 concat 方法生成新的数组,然后在新的数组上减一,并将新数组赋予 state 值 listData。

页面表现:

点击 “减一个” 之后,state 值余下 1 个商品,页面也更新了,存在 1 个商品。

 

为何使用不可变数据

  Undefined、Null、Boolean、Number 和 String 都是基本类型,它们是按值访问的,保存在栈中。Object、Array、Function是引用类型,是按引用访问的,保存在堆中。JS 不允许直接访问内存中的位置,在操作对象时,实际上是操作对象的引用而不是实际的对象。(相关笔记:https://www.cnblogs.com/xiaoxuStudy/p/12509729.html

  不同的引用指向堆内存的同一个对象,所以,当我们去做判断的时候,因为是在内存中的同一个对象,所以会判断为 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

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

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

页面表现同上

 

推荐阅读