目录:
1. 通过条件判断优化渲染
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;
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;
下面在以上代码的基础上实现点击 “删除” 按钮之后该商品被删除的功能。
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;
handleDelete = (id) => {
const _list = this.state.listData.filter( item => item.id !== id )
this.setState({
listData : _list
})
}
页面表现:
点击 “删除” 按钮之后,商品消失。
使用 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;
页面表现:
点击 “删除” 按钮,可以看到目前的 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;
页面表现:
因为点击 “ + ” 按钮触发的事件修改 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;
页面表现:
点击 “ 删除 ” 按钮将商品删光,从控制台可以看出删除商品没有执行 render。
使用 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;
当使用 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;
页面表现:
点击 “减一个” 按钮之后,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;
子组件 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;
页面表现:
点击 “重置” 按钮之后,页面并没有发生变化,查看控制台 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;
父组件 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;
页面表现:
点击 “重置” 之后,商品数量变为 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;
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;
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;
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;
加、减、删除按钮都能正常使用。下面测试导航栏的 “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 改成无状态组件。
在无状态组件中没有 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;
还有另一种更简单的写法,将 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;
页面表现同上