"小和山的菜鸟们",为前端开发者提供技术相关资讯以及系列基础文章。为更好的用户体验,请您移至咱们官网小和山的菜鸟们 ( xhs-rookies.com/ ) 进行学习,及时获取最新文章。前端
"Code tailor" ,若是您对咱们文章感兴趣、或是想提一些建议,微信关注 “小和山的菜鸟们” 公众号,与咱们取的联系,您也能够在微信上观看咱们的文章。每个建议或是赞同都是对咱们极大的鼓励!web
这节咱们将介绍 React
中非父子组件的通讯,上节咱们说到父子组件间的通讯可经过 props
和回调函数完成,但随着应用程序愈来愈大,使用 props
和回调函数的方式就变得很是繁琐了,那么非父子组件间的组件通讯,有没有一种简单的方法呢?微信
本文会向你介绍如下内容:markdown
Context
Context
提供了一个无需为每层组件手动添加 props
,就能在组件树间进行数据传递的方法ide
UI
主题、用户登陆状态、用户信息等)。App
中定义这些信息,层层传递下去,对于一些中间层不须要数据的组件来讲,这是一种冗余的操做。若是层级更多的话,一层层传递是很是麻烦,而且代码是很是冗余的:函数
React
提供了一个 API:Context
;Context
提供了一种在组件之间共享此类值的方式,而没必要显式地经过组件树的逐层传递 props
;Context
设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言;const MyContext = React.createContext(defaultValue)
复制代码
建立一个须要共享的 Context
对象:oop
Context
,那么这个组件会从离自身最近的那个匹配的 Provider
中读取到当前的context
值;Provider
时,其 defaultValue
参数才会生效。defaultValue
是组件在顶层查找过程当中没有找到对应的Provider
,那么就使用默认值注意: 将
undefined
传递给 Provider 的 value 时,消费组件的defaultValue
不会生效。组件化
<MyContext.Provider value={/* 某个值 */}>
复制代码
每一个 Context
对象都会返回一个 Provider React
组件,它容许消费组件订阅 context
的变化:学习
Provider
接收一个 value
属性,传递给消费组件;Provider
能够和多个消费组件有对应关系;Provider
也能够嵌套使用,里层的会覆盖外层的数据;当 Provider 的 value
值发生变化时,它内部的全部消费组件都会从新渲染;优化
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
函数中;<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
复制代码
这里,React
组件也能够订阅到 context
变动。这能让你在 函数式组件
中完成订阅 context
。
function as child
)这种作法;context
值,返回一个 React
节点;举个例子,在下面的代码中,咱们经过一个 “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
传递给子组件。
咱们会把当前输入的 temperature
和 scale
保存在组件内部的 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.temperature
和 this.state.scale
均会被更新。其中一个输入框保留用户的输入并取值,另外一个输入框始终基于这个值显示转换后的结果。
让咱们来从新梳理一下当你对输入框内容进行编辑时会发生些什么:
<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 从新渲染本身自己。Calculator
组件的 render
方法获得组件的 UI 呈现。温度转换在这时进行,两个输入框中的数值经过当前输入温度和其计量单位来从新计算得到。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
组件间通讯的相关知识,组件化的内容将以前的实战案例进行改版,优化以前的实战方案。敬请期待!