前言:组件通信在项目中经常用到,合理的使用组件间通信方式,对于开发人员来说非常具有考验。如何让不同的组件在通信中保持一致性、排除副作用,几乎是所有状态管理框架的开发者都在思考的问题。今天就来总结一下常用的组件通信方式吧!
由于 React 是一个组件化框架,那么基于组件树的位置分布,组件与组件之间的关系,大致可分为 4 种。
-
父与子:父组件包裹子组件,父组件向子组件传递数据。
-
子与父:子组件存在于父组件之中,子组件需要向父组件传递数据。
-
兄弟:两个组件并列存在于父组件中,需要金属数据进行相互传递。
-
无直接关系:两个组件并没有直接的关联关系,处在一棵树中相距甚远的位置,但需要共享、传递数据。
1.父子
父与子的通信是最常见的场景,React 开发的每个组件都在使用这样的设计模式。每个组件都会在父级被使用,再传入 Props,完成信息的传递。这样的交互方式尽管不起眼,容易让人忽略,但正是最经典的设计。
Props
这个是最常用、也最容易忽略的通信方式。如:
-
在初始化时展示默认文案;
-
初始化以后通过网络请求拉取文案数据;
-
通过 Props 传递 state 的文案数据,来更新按钮中的文案。
const Button = ({ text }) => {
<button type="button">{text}</button>
}
class HomePage extends React.Component {
state = {
text: "默认文案"
}
asyc componentDidMount()
const response = await fetch('/api/buttonText')
this.setState({
text: response.buttoText
})
}
render() {
const {
text
} = this.state
return (
<Button text={text} />
)
}
}
2.子父
子与父的通信主要依赖回调函数。
回调函数
又叫 callback。React 在设计中沿用了 JavaScript 的经典设计,允许函数作为参数赋值给子组件。最基础的用法就像下面的例子一样,通过包装传递 text 的值。
class Input extends React.Component {
handleChanged = (e) => {
this.onChangeText(e.target.text)
}
render() {
return <input onChange={handleTextChanged} />
}
}
class HomePage extends React.Component {
handleTextChanged = (text) => {
console.log(text)
}
render() {
return (
<Input onChangeText={this.handleTextChanged} />
)
}
}
回调函数不仅仅用于传递值,它还可以用在渲染中,父组件根据返回的结果,决定子组件该渲染什么。比如在 React Router 中,我们常常会这样使用它:
<Route path="/hello" render={() => <h1>Hello Everyone</h1>} />
采用这样的策略可以使子组件专注业务逻辑,而父组件专注渲染结果。
3.兄弟
兄弟组件之间的通信,往往依赖共同的父组件进行中转
class Input extends React.Component {
handleChanged = (e) => {
this.onChangeText(e.target.text)
}
render() {
return <input onChange={handleTextChanged} />
}
}
const StaticText = ({ children }) => {
return (
<P>{children}</p>
)
}
class HomePage extends React.Component {
state = {
text: '默认文案'
}
handleTextChanged = (text) => {
this.setState({
text,
})
}
render() {
return (
<>
<Input onChangeText={this.handleTextChanged} />
<StaticText>this.state.text</StaticText>
</>
)
}
}
这种模式主要负责在容器组件中协调各组件。
4.无直接关系
Context
一个最常见的用途就是做 i18n,也就是常说的国际化语言包
全局变量与事件
全局变量,顾名思义就是放在 Window 上的变量。但值得注意的是修改 Window 上的变量并不会引起 React 组件重新渲染。
所以在使用场景上,全局变量更推荐用于暂存临时数据。比如在 CallPage 页面点击了按钮之后,需要收集一个 callId,然后在 ReportPage 上报这个 callId。如下代码所示:
class CallPage extends React.Component {
render() {
return <Button onClick={() => {
window.callId = this.props.callId
}} />
}
class ReportPage extends React.Component {
render() {
return <Button onClick={() => {
fetch('/api/report', { id: window.callId })
}} />
}
}
除了全局变量以外,还有一种方案是全局事件。如下代码所示:
class CallPage extends React.Component {
dispatchEvent = () => {
document.dispatchEvent(new CustomEvent('callEvent', {
detail: {
callId: this.props.callId
}
}))
}
render() {
return <Button onClick={this.dispatchEvent} />
}
class ReportPage extends React.Component {
state = {
callId: null,
}
changeCallId = (e) => {
this.setState({
callId: e.detail.callId
})
}
componentDidMount() {
document.addEventListener('callEvent', this.changeCallId)
}
componentWillUnmount() {
document.removeEventListener('callEvent', this.changeCallId)
}
render() {
return <Button onClick={() => {
fetch('/api/report', { id: this.state.callId })
}} />
}
}
事件的绑定往往会在组件加载时放入,如果 CallPage 与 ReportPage 不是同时存在于页面上,那么这个方案又不适用了。
参考: