
壹 ❀ 引
在前面的文章中,我們了解到react中的數據由props與State構成,數據就像瀑布中的水自上而下流動,屬於單向數據流。而這兩者的區別也很簡單,對於一個組件而言,如果說props是外部傳遞進來的屬性,那么State便是組件內部自身提供的屬性。當然這個組件又可以將自己的State與props作為props繼續傳遞給自己的子級,比如下圖:

而對於props與State的通信,我們也在前文中提供了一些例子,但這些例子相對都比較簡單,都是容易理解的父傳子。但在實際開發中,組件之間的關系往往比我們學習時遇到的例子要復雜的多,高層級組件嵌套,兄弟組件通信,子傳父等等都是在寫組件時很常見的問題。其實說到這里,我想各位已經想到了redux的狀態管理(vue中的vuex)。但本文並不會直接介紹redux,在介紹redux之前,我們還是需要了解react自身提供的狀態管理做法,因為只有這樣,我們才能明白為什么需要使用redux,以及react的狀態提升的局限性在哪。那么本文開始。
貳 ❀ react的狀態提升
react中的狀態提升其實很好理解,由於react屬於單向數據流,當有多個組件需要使用相同的數據時,比如兄弟組件相互感知數據變化,在react中我們一般推薦將這份數據提升到兩兄弟的共同父組件中進行管理,這便是所謂的狀態提升。
讓我們說的更直白點,因為瀑布的水(props)沒辦法橫向流動(兄弟組件之間),所以我們將水源提到兩兄弟的頂部,讓它同時灌溉這兩兄弟,從而滿足了瀑布水自上而下的特性。
這里我們直接引用react官方溫度計的例子來了解這種做法的含義。
假設現在我們有一個簡單的溫度計組件,輸入一個溫度,當大於等於100攝氏度時,文案提示水燒開了,反之提示水未燒開,直接上代碼:
function BoilingVerdict(props) {
return props.celsius >= 100 ? <p>水燒開了。</p> : <p>水未燒開。</p>;
}
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = { temperature: 0 };
}
handleChange(e) {
this.setState({ temperature: e.target.value });
}
render() {
return (
<div className="echo">
<input value={this.state.temperature} onChange={this.handleChange} />
<BoilingVerdict celsius={parseFloat(this.state.temperature)} />
</div>
);
}
}
ReactDOM.render(<Calculator />, document.getElementById('root'));

現在需求升級了,我們知道溫度有攝氏度,華氏度不同單位,現在需求是,為用戶提供攝氏度與華氏度兩個單位的溫度輸入框,不管用戶操作哪一個,另一個溫度能自動同步數據展示出對應溫度數值。
由於需要兩個溫度輸入框,這里我們直接將溫度輸入抽離成一個組件,那么完整的代碼為:
// 這是判斷水溫有沒有燒開的組件
function BoilingVerdict(props) {
return props.celsius >= 100 ? <p>水燒開了。</p> : <p>水未燒開。</p>;
}
const textType = {
c: '輸入攝氏度',
f: '輸入華氏度'
};
// 這是抽離的溫度組件
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = { temperature: 0 };
}
handleChange(e) {
this.setState({ temperature: e.target.value });
}
render() {
return (
<div>
<p>{textType[this.props.scale]}:</p>
<input value={this.state.temperature} onChange={this.handleChange} />
</div>
);
}
}
// 這是父組件,目前內部只有兩個溫度組件,並提供了不同的溫度單位類型
class Calculator extends React.Component {
render() {
return (
<div className='echo'>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
ReactDOM.render(<Calculator />, document.getElementById('root'));

可以看到,我們抽離了溫度輸入框組件后,通過不同type,得到了兩個互不影響的溫度輸入框組件實例。
但需求是它們兩者不管哪一個輸入溫度,都應該通過對應的單位換算,同步另一方的溫度,並判斷當前溫度的水有沒有燒開,所以我們還需要將這兩個組件通過某種方式給聯系起來。
OK,我們先准備好攝氏度轉換華氏度,與華氏度轉為攝氏度的計算方法:
// 這是華氏度轉攝氏度
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
// 這是攝氏度轉華氏度
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
考慮到輸入的值的有效性,官方還提供了一個對於輸入值效驗的函數:
/**
* 嘗試轉換溫度的方法
* @param {*} temperature 用戶輸入的溫度
* @param {*} convert 用於計算溫度的方法,為toCelsius與toFahrenheit其一
*/
function tryConvert(temperature, convert) {
// 將輸入的溫度轉為浮點數,這里的input表示輸入,常與output輸出一起使用
const input = parseFloat(temperature);
// 判斷是不是數字,如果不是數字直接返回0
if (Number.isNaN(input)) {
return 0;
}
// 調用對應的溫度計算方法,得到輸出的溫度
const output = convert(input);
// 保留3位精度的做法
const rounded = Math.round(output * 1000) / 1000;
return rounded;
}
那么到這里,我們已經准備好了溫度相互轉換的方法,只差狀態提升將兩個溫度輸入框組件關聯起來了。由於上面的例子中,我們得到了兩個溫度輸入框組件實例,兩者都有屬於自己的state,相互獨立互不影響,前文已經說了狀態提升的做法與含義,既然要提升,那自然是要將輸入框的state提升到它們共有的且最近的父組件Calculator
中。
通過這種做法,Calculator
組件內部將擁有唯一的數據源(state),而攝氏度與華氏度的溫度輸入組件將分別與Calculator
進行數據交互,我們要做的就是當一方溫度組件進行修改時,需要在父組件中更新state的同時,再利用當前state的數值,去調用對應的單位換算方法得到對應的溫度,再傳遞給對應溫度組件即可。
所以說到這里,我們需要對上面的例子進行整體的修改,我們直接上完整的代碼,再解釋做了什么:
// 這是華氏度轉攝氏度
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
// 這是攝氏度轉華氏度
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
/**
* 嘗試轉換溫度的方法
* @param {*} temperature 用戶輸入的溫度
* @param {*} convert 用於計算溫度的方法,為toCelsius與toFahrenheit其一
*/
function tryConvert(temperature, convert) {
// 將輸入的溫度轉為浮點數,這里的input表示輸入,常與output輸出一起使用
const input = parseFloat(temperature);
// 判斷是不是數字,如果輸入不是數字直接返回空,比如啥都沒輸入的情況
if (Number.isNaN(input)) {
return '';
}
// 調用對應的溫度計算方法,得到輸出的溫度
const output = convert(input);
// 保留3位精度的做法
const rounded = Math.round(output * 1000) / 1000;
return rounded;
}
const textType = {
c: '輸入攝氏度',
f: '輸入華氏度'
};
function BoilingVerdict(props) {
// 因為不管操作哪一方,都會同步更新攝氏度,所以我們就用攝氏度來判斷水燒開沒就行了
return props.celsius >= 100 ? <p>水燒開了。</p> : <p>水未燒開。</p>;
}
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
render() {
return (
<div>
<p>{textType[this.props.scale]}:</p>
<input value={this.props.temperature} onChange={this.handleChange} />
</div>
);
}
}
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 });
}
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 className='echo'>
<TemperatureInput scale="c" temperature={celsius} onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput scale="f" temperature={fahrenheit} onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict celsius={parseFloat(celsius)} />
</div>
);
}
}
ReactDOM.render(<Calculator />, document.getElementById('root'));

代碼量貌似有點多,不過沒關系,我們來解釋下做了什么。
首先,由於狀態提升,我們TemperatureInput
所需要的的state提升到了Calculator
中,由Calculator
統一管理,TemperatureInput
不再擁有state,而是接受從Calculator
傳遞過來的props。
輸入框組件只有一個,但事實上我們得到了兩個溫度輸入框實例,無論哪個輸入框改變值時,都需要去更新父組件中的state(把當前輸入的溫度同步過去),所以父組件一定得提供一個更改state的方法給子組件,不然根據props只讀性,我們就沒辦法同步父組件的狀態了。
且為了方便區分當前操作的是哪一個溫度輸入框,所以我們在父組件中分別定義了兩個更新state的方法handleCelsiusChange
與handleFahrenheitChange
,他們都會調用setState
更新溫度數值,同時記錄當前操作的是哪一種溫度。
最后,在父組件中,我們根據當前操作的溫度類型,用於分別同步計算兩個溫度,比如輸入的是攝氏度100,那么攝氏度自身不用參與計算,只用調用toFahrenheit
得到對應的華氏度即可,反之亦是如此。
比較巧妙的是,由於不管是修改攝氏度或者修改華氏度,我們都能得到一個對應的攝氏度,所以只需要將攝氏度傳遞給BoilingVerdict
組件,再利用溫度判斷,即可得到當前水溫是否燒開了。
叄 ❀ 為什么需要redux?redux與狀態提升的區別
OK,以上就是一個完整的狀態提升的例子,它所解決的其實就是兄弟組件共用了一個狀態的通信問題。但事實上,一個完整的react應用中需要通信的組件會復雜很多。雖然狀態提升也強調了,所謂狀態提升,只是將狀態提升到離自己最近的父組件上,但實際場景中往往會存在這樣的問題:

比如這個例子中,存在交互的兄弟組件是child3,它們最近的父組件還要往上找三層,那這樣就造成了一個問題,每次state同步進行傳遞時,都需要經過child1與child2。那對於這兩兄弟就很頭疼了,我們明明不需要這個屬性,還要作為媒介幫忙傳遞props,一層兩層還好,層級多了傳遞起來就難以維護了。
我們假設層級傳遞都不高,其實還會存在另外一個問題,如下圖:

在這個例子中,child2與child3有關聯,於是狀態提升到了parent中,而c4與c5也有關聯,它們的狀態提升到了child2中,我們可以假象將其關系方法,你會發現state與state之間的關系就像一張復雜的網,我們真的有把握維護好每一個state以及與之關聯的state的嗎?
與其將狀態提升到就近的父組件,能不能直接將所有狀態提升到一個最頂點並由它來統一管理呢?那么這個想法就高度契合redux的設計理念了。
我們直接通過兩張圖來對比兩者的區別:
狀態提升:

redux統一管理:

所以到這里,即便你之前從未了解過vuex與redux,我想redux是用來做什么的在你心中也應該有一個模糊的雛形了,沒錯,redux就是一個全局的狀態管理器,所有的state都被集中存放在Store中,當某個組件狀態被修改,便會通知到Store,然后再決定哪些受影響的組件應該重新渲染。
當然本文並不會立馬介紹redux,至於redux如何去使用,應該是下一篇文章應該介紹的事情了。
那么介紹到這里,你是不是又覺得redux強到不行?其實並不是這樣,前面我們也說了,redux是用來解決復雜的組件狀態通信,如果你的組件狀態更新本身就非常簡單,仍然使用redux反而多此一舉。
肆 ❀ 總
好了,到這里我們介紹了react的狀態提升,可以說在未接觸redux之前,這就是官方推薦的state管理做法。當然,通過文中的例子,在某些場景下我們也感受到了狀態提升的局限性,從而引出了redux的作用,所以下篇文章自然就是介紹redux了,那么到這里,本文結束。