從零開始的react入門教程(七),react中的狀態提升,我們為什么需要使用redux


壹 ❀ 引

在前面的文章中,我們了解到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的方法handleCelsiusChangehandleFahrenheitChange,他們都會調用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了,那么到這里,本文結束。

參考

react官網 狀態提升

4 張動圖解釋為什么(什么時候)使用 Redux

微信讀書 深入淺出react和redux 2.4 2.5小節


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM