淺談react context

前言

很久不見!(兩個多月沒更新內容,慚愧了三分鐘)。接下來的文章主要是開始對react的內容作一些整理(瘋狂立Flag)。本文的對象是Context.react

正文

1.爲何須要使用Context

在React中,數據傳遞通常使用props傳遞數據,維持單向數據流,這樣可讓組件之間的關係變得簡單且可預測,可是單項數據流在某些場景中並不適用,看一個官方給出的例子:
有三個組件APPToolbarThemedButton,關係如圖:(爲了方便你們理解(偷懶),這個例子我會全文通用。
圖片描述編程

APP存放着主題相關的參數theme,須要傳遞組件ThemedButton, 若是考慮使用props,那麼代碼就長這樣:api

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />; // 1. 將theme傳遞給
  }
}

function Toolbar(props) {
  // Toolbar 組件接受一個額外的「theme」屬性,而後傳遞給 ThemedButton 組件。
  return (
    <div>
      <ThemedButton theme={props.theme} /> // 2. 繼續往下傳遞給Button
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />; // 最終獲取到參數
  }
}

能夠看到,實際上須要參數的是組件ThemedButton,可是卻必須經過Toolbar做爲中介傳遞。不妨再引伸思考一下:數據結構

  1. 若是ThemedButton並不是末級子節點,那麼參數必須繼續向下傳遞
  2. 若是App中,還有除了<ThemedButton>之外的組件,也須要theme參數,那麼也必須按照這種形式逐層傳遞

那麼數據結構圖大概如圖所示:
圖片描述react-router

結構圖placeholder:層層傳遞ide

顯然,這樣作太!繁!瑣!了!函數式編程

接下來,就要介紹今天的主角--Context函數

2. Context的用法介紹

Context 提供了一種在組件之間共享此類值的方式,而沒必要顯式地經過組件樹的逐層傳遞 props。

上面是官方對於context的介紹,簡單來講,就是能夠把context當作是特定一個組件樹內共享的store,用來作數據傳遞。
爲何這裏要加粗強調組件樹呢?由於它是基於樹形結構共享的數據:在某個節點開啓提供context後,全部後代節點compoent均可以獲取到共享的數據。this

語言描述略顯抽象,直接上代碼:spa

1. 基本使用

如下介紹的是在react 16.x之前的傳統寫法

class App extends React.Component {
// 核心代碼1: 首先在提供context的組件(即provider)裏 使用`getChildContext`定義要共享給後代組件的數據,同時使用`childContextTypes`作類型聲明
  static childContextTypes = {
        theme: PropTypes.string
  };

  getChildContext () {
    return {
      theme: 'dark'
    }
  }

  render() {
    return <Toolbar />; //  無需再將theme經過props傳遞
  }
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />  // Toolbar 組件再也不接受一個額外的「theme」屬性
    </div>
  );
}

// 核心代碼2: 而後在須要使用context數據(即consumer)的節點,用`contextTypes`聲明須要讀取的context屬性,不然讀不到text
class ThemedButton extends React.Component {
    static contextTypes = {
        theme: PropTypes.string
    }

    render() {
        return <h2>{this.context.theme}</h2>; // 直接從context獲取到參數 爲了直觀 這裏改用<h2>直接顯示出來
    }
}

這個結構圖就不畫了,顯然,就是把theme從層層傳遞的props中解放出來了。

在代碼中咱們提到了providerconsumer,這裏簡單解釋下:
context使用的生產者provider- 消費者consumer模式,

  • 把提供context的叫作provider,好比例子中的APP,
  • 把使用context的稱爲consumer,對應例子中的ThemedButton

2. 更新context

若是咱們在APP組件提供了一個切換主題的按鈕,那就須要context可以更新而且通知到相應的consumer
因爲context自己提供了相關功能:

  1. getChildContext方法在每次stateprops改變時會被調用;
  2. 一旦provider改變了context,全部的後代組件中的consumer都會從新渲染。

因此一般的方式是:將context的數據保存在Providestate屬性中,每次經過setState更新對應的屬性時。

class App extends React.Component {
  static childContextTypes = {
        theme: PropTypes.string
  };

  constructor(props) {
    super(props);
    this.state = {theme:'dark'};
  }

  getChildContext () {
    return {
      theme: this.state.theme // 核心代碼,將`context`的值保存在`state`
    }
  }

  render() {
    return <Toolbar />; 
  }
}

可是官方文檔同時提到了這種方法是有隱患的,下一節進行詳細解析。

3. 當context遇到shouldComponentUpdate

再次強調,如下介紹的是在react 16.x之前的版本,關於context新的api會在後面介紹

官方文檔提到:

The problem is, if a context value provided by component changes, descendants that use that value won’t update if an intermediate parent returns false from shouldComponentUpdate.

(皇家翻譯上場) 拿前面的例子來講,咱們在第二節經過使用context,將theme的傳遞方式由本來的
APP->Toolbar->ThemedButton 經過props層層傳遞變成:
圖片描述

可是組件自己的層級關係依然是APP->Toolbar->ThemedButton。若是咱們在中間層Toolbar()
的生命週期shouldComponent返回false會怎麼樣呢?接下來咱們針對Toolbar作一些改動

// 舊寫法
function Toolbar(props) {
  return (
    <div>
      <ThemedButton /> 
    </div>
  );
}

// 新寫法 使用PureComponent render內容同樣, 
// PS:PureComponent內置的shouldComponentUpdate對state和props作了淺比較,這裏爲了省事直接使用
//若是不熟悉PureComponent能夠直接用React.Component,而後補上shouldComponentUpdate裏的 淺比較判斷
class Toolbar extends React.PureComponent {
    render(){
        return (
            <div>
                <ThemedButton /> 
            </div>
        );
    }
}

這裏爲了省事,咱們直接使用了PureComponent,接下來會發現:

每次APP更新theme的值時,ThemedButton沒法再取到變動後的theme

新的結構圖是這樣的(注意紅線表示來自toolbar的抵抗):
圖片描述

如今問題來了:
因爲Toolbar組件是PureComponent,沒法重寫shouldComponentUpdate,這就意味着位於Toolbar以後的後代節點都沒法獲取到context的更新!

clipboard.png

  1. 第一種思路:首先,咱們先看看問題的根源之一,是context更新以後,後代節點沒法及時獲取到更新,那麼若是context不發生更,那就不存在這個問題了.【我我的以爲這個思路有點相似於,解決不了問題,能夠考慮解決提出問題的人】,也就意味着:

    • 設定爲不可變對象immutable
    • 後代組件應該僅在constructor函數中獲取一次context
  2. 第二種思路,咱們不在context中保存具體的狀態值,而是隻利用它作個依賴注入。繞開SCU(shouldComponentUpdate),從根本上解決問題。 例如,能夠經過發佈訂閱模型建立一個自我管理的ThemeManage類來解決問題。具體實現以下:
// 核心代碼
class ThemeManager {
  constructor(theme) {
    this.theme = theme
    this.subscriptions = []
  }
  
  // 變動顏色時 提示相關的訂閱者
  setColor(theme) {
    this.theme = theme
    this.subscriptions.forEach(f => f())
  }


  // 訂閱者接收到響應 觸發對應的callbck保證本身的及時更新
  subscribe(f) {
    this.subscriptions.push(f)
  }
}

class App extends React.Component {
  static childContextTypes = {
      themeManager: PropTypes.object // 本次經過context傳遞一個theme對象
  };

  constructor(props) {
    super(props);
    this.themeManager = new ThemeManager('dark') // 核心代碼
  }

  getChildContext () {
    return {theme: this.themeManager} // 核心代碼
  }

  render() {
    return <Toolbar />;
  }
}

// Toolbar依然是個PureComponent
class Toolbar extends React.PureComponent {
    render(){
        return (
            <div>
                <ThemedButton /> 
            </div>
        );
    }
}

class ThemedButton extends React.Component {
    constructor(){
        super();
        this.state = {
            theme: theme:this.context.themeManager.theme
        }
    }
  componentDidMount() {
    this.context.themeManager.subscribe(() => this.setState({
        theme: this.context.themeManager.theme  // 核心代碼 保證theme的更新
    }))
  }

  render() {
    return <Button theme={this.state.theme} />; // 核心代碼
  }
}

OK,回頭看看咱們都幹了些什麼:

  1. 咱們如今再也不利用context傳遞 theme值,而是傳遞一個themeManager注入對象,這個對象的特色是內置了狀態更新和消息通知的功能
  2. 消費組件ThemedButton訂閱theme的變化,而且利用setState做爲回調函數,保證theme值的及時更新。

從而完美繞開了context的傳遞問題。其實,它一樣符合咱們第一個解決方案:經過context傳遞的對象,只被接受一次,而且後續都沒有更新(都是同一個themeManager對象,更新是經過themeManager內部的自我管理實現的。)

4. 16.x後的新API

講完基本用法,接着聊聊context在16.x版本以後的API。
先說一個好消息!使用新API後

每當 Provider(提供者) 的 value 屬性發生變化時,全部做爲 Provider(提供者) 後代的 consumer(使用者) 組件 都將從新渲染。 從Provider 到其後代使用者的傳播不受 shouldComponentUpdate 方法的約束,所以即便祖先組件退出更新,也會更新 consumer(使用者)

換句話說 若是使用context的新API,第三節能夠跳過不看。(因此我把那一段寫前面去了)

clipboard.png

在傳統版本,使用getChildContextchildContextTypes來使用context,而在16.x版本以後,前面的例子能夠改寫成這樣:

  1. 首先使用createContext建立一個context,該方法返回一個對象,包含Provider(生產者)和Consumer(消費者)兩個組件:

    const themeContext = React.createContext('light'); // 這裏light是默認值 後續使用時能夠改變
  2. 使用Provider組件,指定context須要做用的組件樹範圍

    class App extends React.Component {
      render() {
        // 使用一個 Provider 來將當前的 theme 傳遞給如下的組件樹。
        // 不管多深,任何組件都能讀取這個值。
        // 在這個例子中,咱們將 「dark」 做爲當前的值傳遞下去。
        return (
          <ThemeContext.Provider value="dark">
            <Toolbar />
          </ThemeContext.Provider>
        );
      }
    }
    
    // 中間的組件不再必指明往下傳遞 theme 了。
    function Toolbar(props) {
      return (
          <ThemedButton />
      );
    }
  3. 後代組件根據須要,指定contextType須要做用的組件樹範圍

    class ThemedButton extends React.Component {
      // 指定 contextType 讀取當前的 theme context。
      // React 會往上找到最近的 theme Provider,而後使用它的值。
      // 在這個例子中,當前的 theme 值爲 「dark」。
      static contextType = ThemeContext;
      render() {
        return <Button theme={this.context} />;
      }
    }
    
    // 除了寫static contextType = ThemeContext 也能夠這樣寫:
    ThemedButton.contextType = ThemeContext;

    固然,也能夠經過Consumer組件指定消費者

    class ThemedButton extends React.Component {
        static contextType = ThemeContext;
      
        render() {
            // Consumer的children必須是一個函數,傳遞的等於組件樹中層這個 context 最接近的 Provider 的對應屬性
            <ThemeContext.Consumer>
            {
                theme =><Button theme={theme} />; // 核心代碼
            }
          
          </ThemeContext.Consumer>
        }
    }

    這兩種方式的主要區別是若是須要傳遞多個可能同名的context時(例如這個例子中Toolbar組件也經過context傳遞一個theme屬性,而ThemedButton須要的是從APP來的theme),只能用Consumer來寫

5. 注意事項和其餘

對於context的使用,須要注意的主要是如下2點:

  1. 減小沒必要要使用context,由於react重視函數式編程,講究複用,而使用了context的組件,複用性大大下降
  2. 傳統版本的react,尤爲要注意context在本身的可控範圍內,其實最大的問題也就是前面說的SUC的問題
  3. 前面說到context的值變動時,Consumer會受到相應的通知,所以要注意某些隱含非預期的變化,例如:
// bad 示例, 由於每次render時{something: 'something'}都指向一個新對象(引用類型的值是老問題,不贅述了)
class App extends React.Component {
  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}

// good 示例 使用固定的變量存儲值 固然能夠選擇除了state之外的其餘變量
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }
  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}

順便提一下react-router其實也用了Context的原理。<Router /><Link />以及<Route />這些組件之間共享一個router, 才能完成完成複雜的路由操做。有興趣的能夠自行查閱源碼。(瘋狂偷懶)

總結

本文主要介紹了contextreact的經常使用場景,以及在新舊API模式下的使用方法,着重介紹了shouldComponent的處理方案。

-----慣例偷懶分割線-----
若是以爲寫得很差/有錯誤/表述不明確,都歡迎指出
若是有幫助,歡迎點贊和收藏,轉載請徵得贊成後著明出處。若是有問題也歡迎私信交流,主頁有郵箱地址
若是以爲做者很辛苦,也歡迎打賞~

相關文章
相關標籤/搜索