复合组件通信概念
项目开发基本都采用 “工程化” + “组件化”方式开发。
工程化: 基于gulp、webpack、vite、rollup等,实现代码转换、文件优化、代码分割、模块合并、自动刷新、代码校验、自动发布等。
组件化:
普通业务组件:SPA单页面应用、业务拆分等;
通用业务组件:具备复用性的业务模块;
通用功能组件:UI组件库中的组件;
……
而组件化开发中,必然会涉及 “父子组件、祖先和后代组件、平行组件、兄弟组件” 等,这就是所谓的 “复合组件”。我们今天要研究的话题就是复合组件之间的通信(或数据传输)问题。
示例结构:
父子组件通信
类组件
class Vote extends React.Component {
state = {
supNum: 10,
oppNum: 5
}
// 设置为箭头函数:后续无论方法在哪执行,方法中的this永远都是Vote父组件的实例
change = (type) => {
let { supNum, oppNum } = this.state
if (type === "sup") {
this.setState({ supNum: supNum + 1 })
return
}
this.setState({ oppNum: oppNum + 1 })
}
render() {
let { supNum, oppNum } = this.state
return <div className="vote-box">
<div className="header">
<h2 className="title">React 52lkk</h2>
<span className="num">{supNum + oppNum}</span>
</div>
<VoteMain supNum={supNum} oppNum={oppNum} />
<VoteFooter change={this.change} />
</div>;
}
}
VoteMain组件接收父组件基于属性传递的状态,然后渲染:
class VoteMain extends React.Component {
/* 属性规则校验 */
static defaultProps = {
supNum: 0,
oppNum: 0
}
static propTypes = {
supNum: PropTypes.number,
oppNum: PropTypes.number
}
render() {
let { supNum, oppNum } = this.props
let ratio = "--",
total = supNum + oppNum
if (total > 0) ratio = (supNum / total * 100).toFixed(2) + '%'
return <div className="main">
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
<p>支持比率:{ratio}</p>
</div>;
}
}
VoteFooter组件接收父组件基于属性传递的函数,点击支持反对按钮时,可以将函数方法执行:
- 可以在执行方法的时候传递实参给父组件,实现子传父
- 可以修改父组件中的相关状态,实现子改父
class VoteFooter extends React.PureComponent {
/* 属性规则校验 */
static defaultProps = {}
static propTypes = {
change: PropTypes.func.isRequired
}
render() {
let { change } = this.props
return <div className="footer">
<Button type="primary" onClick={change.bind(null, 'sup')}>支持</Button>
<Button type="primary" danger onClick={change.bind(null, 'opp')}>反对</Button>
</div>;
}
}
总结-父子组件通信的两大方式
@1 以父组件为主导,基于“属性”实现通信
原因:只有父组件可以调用子组件,此时才可以基于属性,把信息传递给子组件
- 父组件基于属性,可以把信息传递给子组件 「父->子」
- 父组件基于属性「插槽」,可以把HTML结构传递给子组件「父->子」
- 父组件把修改自己数据的方法基于属性传递给子组件,子组件把传递过来的方法执行 「父->子」「子->父」
@2 父组件基于ref获取子组件实例「或者子组件基于useImperativeHandle暴露的数据和方法」
但子组件没有办法主动拿到父组件的实例(或者属性和方法),这是跟Vue很大的一个区别:
console.log(this);//控制台打印VoteMain子组件实例
VoteMain子组件实例上没有获取父组件实例的相关方法或接口:
我们调用Antd中的组件,就是经典的父子组件通信。
组件渲染的顺序:依赖于深度优先原则
父组件第一次渲染:
父 willMount -> 父render 「子 willMount -> 子render -> 子didMount」-> 父didMount
———
父组件更新:
父 shouldUpdate -> 父willUpdate -> 父render 「子willReciveProps -> 子shouldUpdate -> 子willUpdate -> 子render -> 子didUpdate」-> 父 didUpdate
特殊: 我们完全可以在子组件内部做优化处理,验证传递的属性值有没有变化,如果没有变化,则禁止子组件更新。
———
父组件释放:
父 willUnmount -> 父释放中「子willUnMount->子释放」-> 父释放
引申的单向数据流的概念-两种理解:
优化处理: 依据上述组件渲染原则,当每次点击支持/反对按钮,子组件VoteFooter调用父组件基于属性传递过来的修改父组件中状态的方法,父组件状态改变,同时父组件更新,带动VoteMain和VoteFooter组件更新。对于类组件来讲,状态改变引起的组件更新只会修改状态值,并不会重新创建实例,也就是父组件中的中的change方法不会重新创建,依然是原来的堆内存引用。因为状态值改变,VoteMain需要用到修改后的状态值,因而VoteMain组件跟着更新没有问题;但是change方法没有改变,VoteFooter组件并没有必要在每次修改状态值触发父组件更新时跟着一起更新。
此时使得VoteFooter子组件继承自React.PureComponent即可(其内部自带对于新老属性的浅比较,如果新传递过来的属性值和原来传递的属性值相同,则组件不更新,否则则更新)
class VoteFooter extends React.PureComponent {
...
}
函数组件
函数组件与类组件父子组件通信的基本思想一致,仅写法上有区别。
const Vote = function Vote() {
let [supNum, setSupNum] = useState(14),
[oppNum, setOppNum] = useState(7)
const change = (type) => {
if (type === 'sup') {
setSupNum(supNum + 1)
return
}
setOppNum(oppNum + 1)
}
return <div className="vote-box">
<div className="header">
<h2 className="title">React 52lkkk</h2>
<span className="num">{supNum + oppNum}</span>
</div>
<VoteMain supNum={supNum} oppNum={oppNum} />
<VoteFooter change={change} />
</div>;
};
const VoteMain = function VoteMain(props) {
let { supNum, oppNum } = props
let ratio = '--',
total = supNum + oppNum
if (total > 0) ratio = (supNum / total * 100).toFixed(2) + '%'
return <div className="main">
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
<p>支持比率:{ratio}</p>
</div>;
};
/* 属性规则校验 */
VoteMain.defaultProps = {
supNum: 0,
oppNum: 0
}
VoteMain.propTypes = {
supNum: PropTypes.number,
oppNum: PropTypes.number
}
const VoteFooter = function VoteFooter(props) {
let { change } = props
return <div className="footer">
<Button type="primary" onClick={change.bind(null, 'sup')}>支持</Button>
<Button type="primary" danger onClick={change.bind(null, 'opp')}>反对</Button>
</div>;
};
/* 属性规则校验 */
VoteFooter.defaultProps = {}
VoteFooter.propTypes = {
change: PropTypes.func.isRequired
}
优化方案(本示例中意义不大):
VoteMain中:基于useMemo实现复杂逻辑的计算缓存
let ratio = useMemo(() => {
let ratio = '--',
total = supNum + oppNum
if (total > 0) ratio = (supNum / total * 100).toFixed(2) + '%'
return ratio
}, [supNum, oppNum])
Vote配合VoteFooter中:
const change = useCallback((type) => {
if (type === 'sup') {
setSupNum(supNum + 1)
return
}
setOppNum(oppNum + 1)
}, [])
export default memo(VoteFooter);
此时出现问题:
原因:
父组件Vote第一次渲染,创建状态及修改状态的方法change,并将修改状态的方法基于属性传递给子组件VoteFooter;后续点击支持/反对按钮,子组件VoteFooter执行方法修改父组件中的状态,使得父组件Vote重新渲染,由于函数闭包机制,父组件Vote第二次渲染产生的闭包中状态和修改状态的方法都是新的(地址引用),但由于上述优化中change函数经过了useCallback处理且未设置能够使其重新创建的依赖项,其将仍沿用第一次闭包中创建的change方法地址引用,也即其中引用到的状态及修改状态的方法依然是第一次闭包中创建的状态和方法;此时VoteFooter子组件被React.memo修饰(React.memo作用于函数组件上,能够对函数组件每一次传过来的属性同上一次传过来的属性做浅比较,如果一样则不会使函数组件再更新了),由于父组件Vote传递过来的属性change始终如一,则其也不会再重新渲染,因而只会渲染一次(第一次渲染随父组件Vote渲染);再点击支持/反对按钮后change引用的状态和方法仍是第一次闭包中的状态和方法,触发修改状态的函数setXxx的优化机制,因而状态也不会再改变。
解决方案:添加变化后使得函数重新创建的依赖项(使得本例优化无意义,但如果示例中还有其他状态及操作将有意义)
const change = useCallback((type) => {
if (type === 'sup') {
setSupNum(supNum + 1)
return
}
setOppNum(oppNum + 1)
}, [supNum, oppNum])
祖先和后代组件通信
不基于属性通信的原因:
祖先和后代通信基于的上下文方案逻辑:
类组件
(src文件夹下)创建一个上下文对象ThemeContext,用来管理上下文信息:
import React from 'react'
const ThemeContext = React.createContext()
export default ThemeContext
让祖先组件Vote具备状态和修改状态的方法;把这些信息存储到上下文中:
...
import ThemeContext from '@/ThemeContext'
class Vote extends React.Component {
state = {
supNum: 15,
oppNum: 10
}
change = (type) => {
let { supNum, oppNum } = this.state
if (type === 'sup') {
this.setState({ supNum: supNum + 1 })
return
}
this.setState({ oppNum: oppNum + 1 })
}
render() {
let { supNum, oppNum } = this.state
return <div className="vote-box">
<div className="header">
<h2 className="title">React 52lkk</h2>
<span className="num">{supNum + oppNum}</span>
</div>
<VoteMain />
<VoteFooter />
</div>;
}
}
基于上下文对象提供的Provider组件,实现:
- 向上下文中存储信息:value属性指定的值就是要存储的信息
- 当祖先组件更新,render重新执行,会把最新的状态值重新存储到上下文对象中
return <ThemeContext.Provider
value={{
supNum,
oppNum,
change: this.change
}}>
...
</ThemeContext.Provider >
在后代组件获取上下文对象中存储的信息:
方案一:
- 导入创建的上下文对象
- 给类组件设置静态私有属性
static contextType = 上下文对象
=>在this.context属性上,存储了上下文中的所有信息- 从this.context中获取需要的信息
...
import ThemeContext from '@/ThemeContext'
class VoteMain extends React.Component {
static contextType = ThemeContext //静态私有属性名称必须是contextType
render() {
let { supNum, oppNum } = this.context
return <div className="main">
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
</div>;
}
}
方案二:
context中存储了上下文中的所有信息
...
import ThemeContext from '@/ThemeContext'
class VoteFooter extends React.Component {
render() {
return <ThemeContext.Consumer>
{context => {
let { change } = context
return <div className="footer">
<Button type="primary" onClick={change.bind(null, 'sup')}>支持</Button>
<Button type="primary" danger onClick={change.bind(null, 'opp')}> 反对</Button>
</div>
}}
</ThemeContext.Consumer >
}
}
上下文的操作,核心是上下文对象。创建多个不同的上下文对象,可以基于不同上下文对象中的 Provider 存放不同的上下文信息,同时也基于不同的对象,获取指定上下文中的信息。
函数组件
创建上下文对象,同类组件;同时整体思路和类组件没有太大区别
让祖先组件Vote具备状态和修改状态的方法;把这些信息存储到上下文中(存储操作-Provider组件以及获取操作-Consumer组件同类组件一致)
...
import ThemeContext from '@/ThemeContext'
const Vote = function Vote() {
let [supNum, setSupNum] = useState(15),
[oppNum, setOppNum] = useState(10)
const change = (type) => {
if (type === 'sup') {
setSupNum(supNum + 1)
return
}
setOppNum(oppNum + 1)
}
return <ThemeContext.Provider
value={{
supNum,
oppNum,
change
}}>
<div className="vote-box">
<div className="header">
<h2 className="title">React 52lkk</h2>
<span className="num">{supNum + oppNum}</span>
</div>
<VoteMain />
<VoteFooter />
</div>;
</ThemeContext.Provider >
};
在后代组件获取上下文对象中存储的信息:
方案一:上述方案二
...
import ThemeContext from '@/ThemeContext'
const VoteMain = function VoteMain() {
return <ThemeContext.Consumer>
{context => {
let { supNum, oppNum } = context
return <div className="main">
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
</div>;
}}
</ThemeContext.Consumer>
};
方案二:useContext Hook函数
返回一个对象 { … } => 获取到上下文中存储的所有信息
import React, { useContext } from "react";
import ThemeContext from '@/ThemeContext'
const VoteMain = function VoteMain() {
let { supNum, oppNum } = useContext(ThemeContext)
return <div className="main">
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
</div>;
};
import React, { useContext } from "react";
import { Button } from 'antd';
import ThemeContext from '@/ThemeContext'
const VoteFooter = function VoteFooter() {
let { change } = useContext(ThemeContext)
return <div className="footer">
<Button type="primary" onClick={change.bind(null, 'sup')}>支持</Button>
<Button type="primary" danger onClick={change.bind(null, 'opp')}>反对</Button>
</div>;
};
同时,具有相同父亲的兄弟组件,以及具有相同祖先的平行组件之间进行通信,可以把要通信的信息放到祖先上,通过上下文的方式实现平行组件之间的信息共享/通信。
通过上下文方式实现祖先和后代的通信,需要把状态、修改状态的方法等都放到祖先组件上实现信息在后代组件之间的共享,会使得祖先组件比较臃肿,逻辑管理也会比较复杂。所以比较少用,后续通常使用redux实现复合组件通信方案。