"小和山的菜鳥們",爲前端開發者提供技術相關資訊以及系列基礎文章。爲更好的用戶體驗,請您移至咱們官網小和山的菜鳥們 ( 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
組件間通訊的相關知識,組件化的內容將以前的實戰案例進行改版,優化以前的實戰方案。敬請期待!