快来退出咱们吧!

"小和山的菜鸟们",为前端开发者提供技术相干资讯以及系列根底文章。为更好的用户体验,请您移至咱们官网小和山的菜鸟们 ( https://xhs-rookies.com/ ) 进行学习,及时获取最新文章。

"Code tailor" ,如果您对咱们文章感兴趣、或是想提一些倡议,微信关注 “小和山的菜鸟们” 公众号,与咱们取的分割,您也能够在微信上观看咱们的文章。每一个倡议或是同意都是对咱们极大的激励!

前言

这节咱们将介绍 React 中非父子组件的通信,上节咱们说到父子组件间的通信可通过 props 和回调函数实现,但随着应用程序越来越大,应用 props 和回调函数的形式就变得十分繁琐了,那么非父子组件间的组件通信,有没有一种简略的办法呢?

本文会向你介绍以下内容:

  • 跨级组件间的通信
  • Context
  • 兄弟组件通信

跨级组件间的通信

Context

Context 提供了一个无需为每层组件手动增加 props,就能在组件树间进行数据传递的办法

Context 的应用场景

  • 对于有一些场景:比方一些数据须要在多个组件中进行共享(地区偏好、UI 主题、用户登录状态、用户信息等)。
  • 如果咱们在顶层的 App 中定义这些信息,层层传递上来,对于一些中间层不须要数据的组件来说,这是一种冗余的操作。

如果层级更多的话,一层层传递是十分麻烦,并且代码是十分冗余的:

  • React 提供了一个 API:Context
  • Context 提供了一种在组件之间共享此类值的形式,而不用显式地通过组件树的逐层传递 props
  • Context 设计目标是为了共享那些对于一个组件树而言是“全局”的数据,例如以后认证的用户、主题或首选语言;

Context 相干的 API

React.createContext
const MyContext = React.createContext(defaultValue)

创立一个须要共享的 Context 对象:

  • 如果一个组件订阅了 Context,那么这个组件会从离本身最近的那个匹配的 Provider 中读取到以后的context 值;
  • 只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会失效。defaultValue 是组件在顶层查找过程中没有找到对应的Provider,那么就应用默认值
留神:undefined 传递给 Provider 的 value 时,生产组件的 defaultValue 不会失效。
Context.Provider
<MyContext.Provider value={/* 某个值 */}>

每个 Context 对象都会返回一个 Provider React 组件,它容许生产组件订阅 context 的变动:

  • Provider 接管一个 value 属性,传递给生产组件;
  • 一个 Provider 能够和多个生产组件有对应关系;
  • 多个 Provider 也能够嵌套应用,里层的会笼罩外层的数据;

当 Provider 的 value 值发生变化时,它外部的所有生产组件都会从新渲染;

Class.contextType
class MyClass extends React.Component {  componentDidMount() {    let value = this.context    /* 在组件挂载实现后,应用 MyContext 组件的值来执行一些有副作用的操作 */  }  componentDidUpdate() {    let value = this.context    /* ... */  }  componentWillUnmount() {    let value = this.context    /* ... */  }  render() {    let value = this.context    /* 基于 MyContext 组件的值进行渲染 */  }}MyClass.contextType = MyContext

挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创立的 Context 对象:

  • 这能让你应用 this.context 来生产最近 Context 上的那个值;
  • 你能够在任何生命周期中拜访到它,包含 render 函数中;
Context.Consumer
<MyContext.Consumer>  {value => /* 基于 context 值进行渲染*/}</MyContext.Consumer>

这里,React 组件也能够订阅到 context 变更。这能让你在 函数式组件 中实现订阅 context

  • 这里须要 函数作为子元素(function as child)这种做法;
  • 这个函数接管以后的 context 值,返回一个 React 节点;

Context 应用

举个例子,在上面的代码中,咱们通过一个 “theme” 属性手动调整一个按钮组件的款式:

class App extends React.Component {  render() {    return <Toolbar theme="dark" />  }}function Toolbar(props) {  // Toolbar 组件承受一个额定的“theme”属性,而后传递给 ThemedButton 组件。  // 如果利用中每一个独自的按钮都须要晓得 theme 的值,这会是件很麻烦的事,  // 因为必须将这个值层层传递所有组件。  return (    <div>      <ThemedButton theme={props.theme} />    </div>  )}class ThemedButton extends React.Component {  render() {    return <Button theme={this.props.theme} />  }}

应用 context, 咱们能够防止通过两头元素传递 props

// Context 能够让咱们毋庸明确地传遍每一个组件,就能将值深刻传递进组件树。// 为以后的 theme 创立一个 context(“light”为默认值)。const ThemeContext = React.createContext('light')class App extends React.Component {  render() {    // 应用一个 Provider 来将以后的 theme 传递给以下的组件树。    // 无论多深,任何组件都能读取这个值。    // 在这个例子中,咱们将 “dark” 作为以后的值传递上来。    return (      <ThemeContext.Provider value="dark">        <Toolbar />      </ThemeContext.Provider>    )  }}// 两头的组件再也不用指明往下传递 theme 了。function Toolbar() {  return (    <div>      <ThemedButton />    </div>  )}class ThemedButton extends React.Component {  // 指定 contextType 读取以后的 theme context。  // React 会往上找到最近的 theme Provider,而后应用它的值。  // 在这个例子中,以后的 theme 值为 “dark”。  static contextType = ThemeContext  render() {    return <Button theme={this.context} />  }}

兄弟组件通信

兄弟组件即他们领有独特的父组件!

而在讲兄弟组件之前咱们先要讲到一个概念:状态晋升

状态晋升 :在 React 中,将多个组件中须要共享的 state 向上挪动到它们的最近独特父组件中,便可实现共享 state。这就是所谓的 状态晋升

简略例子

接下来通过一个例子帮忙大家深刻理解:

咱们将从一个名为 BoilingVerdict 的组件开始,它承受 celsius 温度作为一个 prop,并据此打印出该温度是否足以将水煮沸的后果。

function BoilingVerdict(props) {  if (props.celsius >= 100) {    return <p>The water would boil.</p>  }  return <p>The water would not boil.</p>}

接下来, 咱们创立一个名为 Calculator 的组件。它渲染一个用于输出温度的 <input>,并将其值保留在 this.state.temperature 中。

另外, 它依据以后输出值渲染 BoilingVerdict 组件。

class Calculator extends React.Component {  constructor(props) {    super(props);    this.state = {temperature: ''};  }  handleChange(e) {    this.setState({temperature: e.target.value});  }  render() {    const temperature = this.state.temperature;    return (        <p>Enter temperature in Celsius:</p>        <input          value={temperature}          onChange={e => this.handleChange(e)} />        <BoilingVerdict          celsius={parseFloat(temperature)} />    );  }}

增加第二个输入框

当初的新需要是,在已有摄氏温度输入框的根底上,咱们提供华氏度的输入框,并放弃两个输入框的数据同步。

咱们先从 Calculator 组件中抽离出 TemperatureInput 组件,而后为其增加一个新的 scale prop,它能够是 "c" 或是 "f":(代表摄氏温度和华氏温度)

const scaleNames = {  c: 'Celsius',  f: 'Fahrenheit',}class TemperatureInput extends React.Component {  constructor(props) {    super(props)    this.handleChange = this.handleChange.bind(this)    this.state = { temperature: '' }  }  handleChange(e) {    this.setState({ temperature: e.target.value })  }  render() {    const temperature = this.state.temperature    const scale = this.props.scale    return (      <fieldset>        <legend>Enter temperature in {scaleNames[scale]}:</legend>        <input value={temperature} onChange={this.handleChange} />      </fieldset>    )  }}

咱们当初能够批改 Calculator 组件让它渲染两个独立的温度输入框组件:

class Calculator extends React.Component {  render() {    return (      <div>        <TemperatureInput scale="c" />        <TemperatureInput scale="f" />      </div>    )  }}

咱们当初有了两个输入框,但当你在其中一个输出温度时,另一个并不会更新。这与咱们的要求相矛盾:咱们心愿让它们放弃同步。

另外,咱们也不能通过 Calculator 组件展现 BoilingVerdict 组件的渲染后果。因为 Calculator 组件并不知道暗藏在 TemperatureInput 组件中的以后温度是多少。

状态晋升

到目前为止, 两个 TemperatureInput 组件均在各自外部的 state 中互相独立地保留着各自的数据。

class TemperatureInput extends React.Component {  constructor(props) {    super(props);    this.handleChange = this.handleChange.bind(this);    this.state = {temperature: ''};  }  handleChange(e) {    this.setState({temperature: e.target.value});  }  render() {    const temperature = this.state.temperature;    // ...

然而,咱们心愿两个输入框内的数值彼此可能同步。当咱们更新摄氏度输入框内的数值时,华氏度输入框内该当显示转换后的华氏温度,反之亦然。

React 中,将多个组件中须要共享的 state 向上挪动到它们的最近独特父组件中,便可实现共享 state。这就是所谓的“状态晋升”。接下来,咱们将 TemperatureInput 组件中的 state 挪动至 Calculator 组件中去。

如果 Calculator 组件领有了共享的 state,它将成为两个温度输入框中以后温度的“数据源”。它可能使得两个温度输入框的数值彼此保持一致。因为两个 TemperatureInput 组件的 props 均来自独特的父组件 Calculator,因而两个输入框中的内容将始终保持统一。

让咱们看看这是如何实现的。

外围点在于:父组件将状态扭转函数作为 props 传递给子组件。

咱们会把以后输出的 temperaturescale 保留在组件外部的 state 中。这个 state 就是从两个输入框组件中“晋升”而来的,并且它将用作两个输入框组件的独特“数据源”。这是咱们为了渲染两个输入框所须要的所有数据的最小示意。

因为两个输入框中的数值由同一个 state 计算而来,因而它们始终保持同步:

class Calculator extends React.Component {  constructor(props) {    super(props);    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);    this.state = {temperature: '', scale: 'c'};  }  handleCelsiusChange(temperature) {    this.setState({scale: 'c', temperature});  }  handleFahrenheitChange(temperature) {    this.setState({scale: 'f', temperature});  }  tryConvert(temperature, convert){      ... //用来转化温度  }  render() {    const scale = this.state.scale;    const temperature = this.state.temperature;    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;                    const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;    return (      <div>        <TemperatureInput          scale="c"          temperature={celsius}                  onTemperatureChange={this.handleCelsiusChange} />        <TemperatureInput          scale="f"          temperature={fahrenheit}                    onTemperatureChange={this.handleFahrenheitChange} />        <BoilingVerdict          celsius={parseFloat(celsius)} />      </div>    );  }}

再让咱们看下 TemperatureInput 组件如何变动。咱们移除组件本身的 state,通过应用 this.props.temperature 代替 this.state.temperature 来读取温度数据。当咱们想要响应数据扭转时,咱们须要调用 Calculator 组件提供的 this.props.onTemperatureChange(),而不再应用 this.setState()

class TemperatureInput extends React.Component {  constructor(props) {    super(props)    this.handleChange = this.handleChange.bind(this)  }  handleChange(e) {    this.props.onTemperatureChange(e.target.value)  }  render() {    const temperature = this.props.temperature    const scale = this.props.scale    return (      <fieldset>        <legend>Enter temperature in {scaleNames[scale]}:</legend>        <input value={temperature} onChange={this.handleChange} />      </fieldset>    )  }}

当初无论你编辑哪个输入框中的内容,Calculator 组件中的 this.state.temperaturethis.state.scale 均会被更新。其中一个输入框保留用户的输出并取值,另一个输入框始终基于这个值显示转换后的后果。

让咱们来从新梳理一下当你对输入框内容进行编辑时会产生些什么:

  • React 会调用 DOM 中 <input>onChange 办法。在本实例中,它是 TemperatureInput 组件的 handleChange 办法。
  • TemperatureInput 组件中的 handleChange 办法会调用 this.props.onTemperatureChange(),并传入新输出的值作为参数。其 props 诸如 onTemperatureChange 之类,均由父组件 Calculator 提供。
  • 起初渲染时,用于摄氏度输出的子组件 TemperatureInput 中的 onTemperatureChange 办法与 Calculator 组件中的 handleCelsiusChange 办法雷同,而,用于华氏度输出的子组件 TemperatureInput 中的 onTemperatureChange 办法与 Calculator 组件中的 handleFahrenheitChange 办法雷同。因而,无论哪个输入框被编辑都会调用 Calculator 组件中对应的办法。
  • 在这些办法外部,Calculator 组件通过应用新的输出值与以后输入框对应的温度计量单位来调用 this.setState() 进而申请 React 从新渲染本人自身。
  • React 调用 Calculator 组件的 render 办法失去组件的 UI 出现。温度转换在这时进行,两个输入框中的数值通过以后输出温度和其计量单位来从新计算取得。
  • React 应用 Calculator 组件提供的新 props 别离调用两个 TemperatureInput 子组件的 render 办法来获取子组件的 UI 出现。
  • React 调用 BoilingVerdict 组件的 render 办法,并将摄氏温度值以组件 props 形式传入。
  • React DOM 依据输出值匹配水是否沸腾,并将后果更新至 DOM。咱们刚刚编辑的输入框接管其以后值,另一个输入框内容更新为转换后的温度值。

得益于每次的更新都经验雷同的步骤,两个输入框的内容能力始终保持同步。

讲完了状态晋升,让咱们当初来看看它怎么使用到兄弟组件通信中来!

当初有这样一个场景

  • 绘制登录页面:输出用户名和明码
  • 点击登录

class Login extends React.Component {        constructor(props) {        super (props);        this.state = {            userName:"",            password:""        }    }      handlerLogin(e){      this.setState(e)    }    render(){        return(            <div>              <UserNameInput onChange = {value => this.handlerLogin({username:value})}>              <PasswordInput onChange = {value => this.handlerLogin({password:value})}>          </div>        )    }}class UserNameInput extends React.Component {     handlerUserName(e){       this.props.handlerLogin(e.target.value);     }      render(){      return (          <div>            <input onChange={e => this.handlerUserName(e)} placeholder="请输出用户名"/>        </div>      )    }}class PasswordInput extends React.Component {     handlerPassword(e){       this.props.handlerLogin(e.target.value);     }    render(){        return (          <div>            <input onChange={e => this.handlerUserName(e)} placeholder="请输出明码"/>          </div>        )      }}

其实这里的代码并没有写完,但咱们能够看到的是咱们曾经能够在 App 组件中拿到用户名和明码了,接下来咱们就能够在此去调用登录接口了。

下节预报

下节中咱们将讲述应用 React 组件间通信的相干常识,组件化的内容将之前的实战案例进行改版,优化之前的实战计划。敬请期待!