react 常见组件间通信方式总结

2,169 阅读3分钟

前言:组件通信在项目中经常用到,合理的使用组件间通信方式,对于开发人员来说非常具有考验。如何让不同的组件在通信中保持一致性、排除副作用,几乎是所有状态管理框架的开发者都在思考的问题。今天就来总结一下常用的组件通信方式吧!

由于 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 不是同时存在于页面上,那么这个方案又不适用了。

参考:

  1. 如何面向组件跨层级通信
  2. react中文网