重拾React: Context

前言

  首先歡迎你們關注個人Github博客,也算是對個人一點鼓勵,畢竟寫東西無法得到變現,能堅持下去也是靠的是本身的熱情和你們的鼓勵,但願你們多多關注呀!很久已經沒寫React,發現連Context都發生了變化,突然有一種村裏剛通上的網的感受,可能文章所說起的知識點已經算是過期了,僅僅算做是本身的學習體驗吧,javascript

Context

  對於React開發者而言,Context應該是一個不陌生的概念,可是在16.3以前,React官方一直不推薦使用,並聲稱該特性屬於實驗性質的API,可能會從以後的版本中移除。可是在實踐中很是多的第三方庫都基於該特性,例如:react-redux、mobx-react。java

  如上面的組件樹中,A組件與B組件之間隔着很是多的組件,假如A組件但願傳遞給B組件一個屬性,那麼不得不使用props將屬性從A組件歷經一系列中間組件最終跋山涉水傳遞給B組件。這樣代碼不只很是的麻煩,更重要的是中間的組件可能壓根就用不上這個屬性,卻要承擔一個傳遞的職責,這是咱們不但願看見的。Context出現的目的就是爲了解決這種場景,使得咱們能夠直接將屬性從A組件傳遞給B組件。react

Legacy Context

  這裏所說的老版本Context指的是React16.3以前的版本所提供的Context屬性,在我看來,這種Context是以一種協商聲明的方式使用的。做爲屬性提供者(Provider)須要顯式聲明哪些屬性能夠被跨層級訪問而且須要聲明這些屬性的類型。而做爲屬性的使用者(Consumer)也須要顯式聲明要這些屬性的類型。官方文檔中給出了下面的例子:git

import React, {Component} from 'react';
import PropTypes from 'prop-types';

class Button extends React.Component {

    static contextTypes = {
        color: PropTypes.string
    };

    render() {
        return (
            <button style={{background: this.context.color}}>
                {this.props.children}
            </button>
        );
    }
}

class Message extends React.Component {
    render() {
        return (
            <div>
                {this.props.text} <Button>Delete</Button>
            </div>
        );
    }
}

class MessageList extends React.Component {
    static childContextTypes = {
        color: PropTypes.string
    };

    getChildContext() {
        return {color: "red"};
    }

    render() {
        const children = this.props.messages.map((message) =>
            <Message text={message.text} />
        );
        return <div>{children}</div>;
    }
}

  咱們能夠看到MessageList經過函數getChildContext顯式聲明提供color屬性,而且經過靜態屬性childContextTypes聲明瞭該屬性的類型。而Button經過靜態屬性contextTypes聲明瞭要使用屬性的類型,兩者經過協商的方式約定了跨層級傳遞屬性的信息。Context確實很是方便的解決了跨層級傳遞屬性的狀況,可是爲何官方卻不推薦使用呢?github

  首先Context的使用是與React可複用組件的邏輯背道而馳的,在React的思惟中,全部組件應該具備複用的特性,可是正是由於Context的引入,組件複用的使用變得嚴格起來。就以上面的代碼爲例,若是想要複用Button組件,必須在上層組件中含有一個能夠提供String類型的colorContext,因此複用要求變得嚴格起來。而且更重要的是,當你嘗試修改Context的值時,可能會觸發不肯定的狀態。咱們舉一個例子,咱們將上面的MessageList稍做改造,使得Context內容能夠動態改變:redux

class MessageList extends React.Component {

    state = {
        color: "red"
    };

    static childContextTypes = {
        color: PropTypes.string
    };

    getChildContext() {
        return {color: this.state.color};
    }

    render() {
        const children = this.props.messages.map((message) =>
            <Message text={message.text} />
        );
        return (
            <div>
                <div>{children}</div>
                <button onClick={this._changeColor}>Change Color</button>
            </div>
        );
    }

    _changeColor = () => {
        const colors = ["red", "green", "blue"];
        const index = (colors.indexOf(this.state.color) + 1) % 3;
        this.setState({
            color: colors[index]
        });
    }
}

  上面的例子中咱們MessageList組件Context提供的color屬性改爲了state的屬性,當每次使用setState刷新color的時候,子組件也會被刷新,所以對應按鈕的顏色也會發生改變,一切看起來是很是的完美。可是一旦組件間的組件存在生命週期函數ShouldComponentUpdate那麼一切就變得詭異起來。咱們知道PureComponent實質就是利用ShouldComponentUpdate避免沒必要要的刷新的,所以咱們能夠對以前的例子作一個小小的改造:安全

class Message extends React.PureComponent {
    render() {
        return (
            <div>
                {this.props.text} <Button>Delete</Button>
            </div>
        );
    }
}

  你會發現即便你在MessageList中改變了Context的值,也沒法致使子組件中按鈕的顏色刷新。這是由於Message組件繼承自PureComponent,在沒有接受到新的props改變或者state變化時生命週期函數shouldComponentUpdate返回的是false,所以Message及其子組件並無刷新,致使Button組件沒有刷新到最新的顏色。ide

  若是你的Context值是不會改變的,或者只是在組件初始化的時候纔會使用一次,那麼一切問題都不會存在。可是若是須要改變Context的狀況下,如何安全使用呢? Michel Weststrate在[How to safely use React context
](https://medium.com/@mweststra...。做者認爲咱們不該該直接在getChildContext中直接返回state屬性,而是應該像依賴注入(DI)同樣使用conext。函數

class Theme {
    constructor(color) {
        this.color = color
        this.subscriptions = []
    }

    setColor(color) {
        this.color = color
        this.subscriptions.forEach(f => f())
    }

    subscribe(f) {
        this.subscriptions.push(f)
    }
}

class Button extends React.Component {
    static contextTypes = {
        theme: PropTypes.Object
    };

    componentDidMount() {
        this.context.theme.subscribe(() => this.forceUpdate());
    }

    render() {
        return (
            <button style={{background: this.context.theme.color}}>
                {this.props.children}
            </button>
        );
    }
}

class MessageList extends React.Component {

    constructor(props){
        super(props);
        this.theme = new Theme("red");
    }

    static childContextTypes = {
        theme: PropTypes.Object
    };

    getChildContext() {
        return {
            theme: this.theme
        };
    }

    render() {
        const children = this.props.messages.map((message) =>
            <Message text={message.text} />
        );
        return (
            <div>
                <div>{children}</div>
                <button onClick={this._changeColor}>Change Color</button>
            </div>
        );
    }

    _changeColor = () => {
        const colors = ["red", "green", "blue"];
        const index = (colors.indexOf(this.theme.color) + 1) % 3;
        this.theme.setColor(colors[index]);
    }
}

  在上面的例子中咱們創造了一個Theme類用來管理樣式,而後經過ContextTheme的實例向下傳遞,在Button中獲取到該實例而且訂閱樣式變化,在樣式變化時調用forceUpdate強制刷新達到刷新界面的目的。固然上面的例子只是一個雛形,具體使用時還須要考慮到其餘的方面內容,例如在組件銷燬時須要取消監聽等方面。學習

  回顧一下以前版本的Context,配置起來仍是比較麻煩的,尤爲還須要在對應的兩個組件中分別使用childContextTypescontextTypes的聲明Context屬性的類型。並且其實這兩個類型聲明並不能很好的約束context。舉一個例子,假設分別有三個組件: GrandFather、Father、Son,渲染順序分別是:

GrandFather -> Father -> Son

  那麼假設說組件GrandFather提供的context是類型爲number鍵爲value的值1,而Father提供也是類型爲number的鍵爲value的值2,組件Son聲明得到的是類型爲number的鍵爲value的context,咱們確定知道組件Son中this.context.value值爲2,由於context在遇到同名Key值時確定取的是最靠近的父組件。

  一樣地咱們假設件GrandFather提供的context是類型爲string鍵爲value的值"1",而Father提供是類型爲number的鍵爲value的值2,組件Son聲明得到的是類型爲string的鍵爲value的context,那麼組件Son會取到GrandFather的context值嗎?事實上並不會,仍然取到的值是2,只不過在開發過程環境下會輸出:

Invalid context value of type number supplied to Son, expected string

  所以咱們能得出靜態屬性childContextTypescontextTypes只能提供開發的輔助性做用,對實際的context取值並不能起到約束性的做用,即便這樣咱們也不得不重複體力勞動,一遍遍的聲明childContextTypescontextTypes屬性。

New Context

  新的Context發佈於React 16.3版本,相比於以前組件內部協商聲明的方式,新版本下的Context大不相同,採用了聲明式的寫法,經過render props的方式獲取Context,不會受到生命週期shouldComponentUpdate的影響。上面的例子用新的Context改寫爲:

import React, {Component} from 'react';

const ThemeContext = React.createContext({ theme: 'red'});

class Button extends React.Component {
    render(){
        return(
            <ThemeContext.Consumer>
                {({color}) => {
                    return (
                        <button style={{background: color}}>
                            {this.props.children}
                        </button>
                    );
                }}
            </ThemeContext.Consumer>
        );
    }
}

class Message extends React.PureComponent {
    render() {
        return (
            <div>
                {this.props.text} <Button>Delete</Button>
            </div>
        );
    }
}

class MessageList extends React.Component {

    state = {
        theme: { color: "red" }
    };

    render() {
        return (
            <ThemeContext.Provider value={this.state.theme}>
                <div>
                    {this.props.messages.map((message) => <Message text={message.text}/>)}
                    <button onClick={this._changeColor}>Change Color</button>
                </div>
            </ThemeContext.Provider>
        )
    }

    _changeColor = () => {
        const colors = ["red", "green", "blue"];
        const index = (colors.indexOf(this.state.theme.color) + 1) % 3;
        this.setState({
            theme: {
                color: colors[index]
            }
        });
    }
}

  咱們能夠看到新的Context使用React.createContext的方式建立了一個Context實例,而後經過Provider的方式提供Context值,而經過Consumer配合render props的方式獲取到Context值,即便中間組件中存在shouldComponentUpdate返回false,也不會致使Context沒法刷新的問題,解決了以前存在的問題。咱們看到在調用React.createContext建立Context實例的時候,咱們傳入了一個默認的Context值,該值僅會在Consumer在組件樹中沒法找到匹配的Provider纔會使用,所以即便你給Providervalue傳入undefined值時,Consumer也不會使用默認值。

  新版的Context API相比於以前的Context API更符合React的思想,而且能解決componentShouldUpdate的帶來的問題。與此同時你的項目須要增長專門的文件來建立Context。在 React v17 中,可能就會刪除對老版 Context API 的支持,因此仍是須要儘快升級。最後講了這麼多,可是在項目中仍是要儘可能避免Context的濫用,不然會形成組件間依賴過於複雜。

相關文章
相關標籤/搜索