[譯]React全新的Context API

DoubleDimos發表於2018-02-22

連結

原文地址

中文翻譯

blabla

翻譯水平有限,部分內容比較晦澀,因此可能錯誤較多,請理解或指教。

簡介

介紹用於解決現有侷限性的全新Context API。

基本例項

type Theme = 'light' | 'dark';
// Pass a default theme to ensure type correctness
//傳遞預設的主題確保型別的正確
const ThemeContext: Context<Theme> = React.createContext('light');

class ThemeToggler extends React.Component {
  state = {theme: 'light'};
  render() {
    return (
       /*傳遞現有的Context的值給Provider的props上的`value`屬性
      資料變化使用Object.is來進行嚴格比較*/
      <ThemeContext.Provider value={this.state.theme}>
        <button
          onClick={() =>
            this.setState(state => ({
              theme: state.theme === 'light' ? 'dark' : 'light',
            }))
          }>
          Toggle theme
        </button>
        {this.props.children}
      </ThemeContext.Provider>
    );
  }
}

class Title extends React.Component {
  render() {
    return (
      //消費層使用一個渲染的prop API,以避免與props名稱空間衝突
      <ThemeContext.Consumer>
        {theme => (
          <h1 style={{color: theme === 'light' ? '#000' : '#fff'}}>
            {this.props.children}
          </h1>
        )}
      </ThemeContext.Consumer>
    );
  }
}
複製程式碼

動機

通常情況下,React裡面的資料是按照top-down(parent to child)的順序,通過props來傳遞的。但有些時候,跳過多個抽象層級來傳遞一些值往往是很有用處的。就如例子中所傳遞的UI主題引數一樣。很多元件都依賴於此,可你卻不想在每一層元件上都使用prop來向下傳遞。

React裡面的Context API正是為了解決這一問題所誕生的。在本文中,我們將祖先元件成為Provider(提供者),把孩子元件稱為Consumer(消費者)。

現有版本的Context API的缺點

shouldComponentUpdate會阻止Context的更改

現在的Context API的主要問題就是如何與shouldComponentUpdate互動。如果一箇中間元件使用了shouldComponentUpdate來操作,那麼它的下方元件在沒有等待更新的時候,React將認為整個子樹都沒有發生改變。如果子樹包含了Context的消費者,那麼消費者將不會收到任何最新的Context。換句話來說,Context的更改將不會在shouldComponentUpdate返回為false上的元件上傳播。

在React應用中,shouldComponentUpdate是經常使用的優化操作方式。在共享元件與開源庫中它的使用往往很頻繁。在實踐中,這意味著Context在廣播變化的時候是不可靠的。

將使用者空間複雜化

現在,開發者們通過使用訂閱來繞過了shouldComponentUpdate的問題

  • 提供者從當事件發生器,它追蹤最新的Context,並且當它發生更改時通知訂閱了的使用者。
  • 消費者使用Context API來訪問事件發生器(這種方法很好,因為事件發生器本身並不會發生更改)。
  • 消費者向提供者註冊一個事件監聽器。
  • 當提供者出發了一個變化事件,消費者會收到通知並呼叫setState來更新並觸發重渲染。訂閱被開源軟體廣發使用,例如ReduxReact Broadcast。它很有用,但它也有一些明顯的缺點:
  • 不符合人體工程學。由於Context使用的普遍性,正確的實施起來應該不會特別困難。
  • 啟動成本。為每個消費者訂閱的花費很高,特別是因為它們在初始掛載的時候並未使用。
  • 鼓勵突變(注:這裡原文是Encourages mutation,不知道怎麼翻譯,直接直譯)和一些不常用的但會在非同步模式下造成一定的BUG的模式。
  • 相同的程式碼在每個庫中都會被複制一次,這樣增加了包的大小。 最根本的問題在於,核心功能的所有權和責任已經從框架轉移到了使用者身上。

這個提案的主要目的

  • 零成本(或接近於零)的初始安裝、提交和解除安裝,根據需要取捨的更新成本。
  • 簡單使用的API
  • 靜態的型別
  • 鼓勵非同步友好的實踐,例如不可變性。
  • 組織費非理想的做法,例如事件發生器與變異。
  • 消除使用者級程式碼中重複的複雜性。

詳細的設計

介紹全新的元件型別:ProviderConsumer

type Provider<T> = React.Component<{
  value: T,
  children?: React.Node,
}>;

type Consumer<T> = React.Component<{
  children: (value: T) => React.Node,
}>;
複製程式碼

ProviderComsumer是成對出現的,對於每一個Provider,都會有一個對應的Consumer。一個Provider-Consumer組合是用React.createContext()來產生的:

type Context<T> = {
  Provider: Provider<T>,
  Consumer: Consumer<T>,
};

interface React {
  createContext<T>(defaultValue: T): Context<T>;
}
複製程式碼

createContext需要一個預設值來確保型別的正確。

請注意,即使任何提供者與任何消費者的值的型別相同,它們也不能聯合使用。它們必須是同一個createContext產生的結果。

Providerprops上接收一個value,無論巢狀的深度如何,它都能被提供者的任何匹配的消費者所訪問。

render() {
  return (
    <Provider value={this.state.contextValue}>
      {this.props.children}
    </Provider>
  );
}
複製程式碼

為了更新Context的值,父級重新渲染並傳遞一個不同的值。Context的變化將被檢測到,檢測所使用的是Object.is來比較的。這意味著鼓勵使用不可變或持久的資料結構。在典型的場景中,通過呼叫提供者父級的setState來更新Context。

消費者使用一個render prop API

render() {
  return (
    <Consumer>
      {contextValue => <Child arbitraryProp={contextValue} />}
    </Consumer>
  )
}
複製程式碼

請注意上面這個例子,Context的值可以傳遞給子元件上的任意prop。render prop API的優點就是避免了破壞prop的名稱空間。

如果一個Consumer沒有提供一個匹配的Provider作為它的祖先,它會接收搭配傳遞給createContext的預設值,確保型別安全。

缺點

依賴於Context的值的嚴格比較

該提案使用嚴格(參考)比較來檢測對Context的更改。這部分鼓勵使用不可變性或持久的資料結構。但許多常見的資料來源都依賴與突變。例如Flux的某些實現,甚至像Relay Modern這樣的更新的庫。

但是,與非同步渲染結合時,突變存在固有的問題,主要與撕裂有關。 對於依賴變異的體系結構,開發人員要麼決定某種程度的撕裂是可以接受的,要麼演變為更好地支援非同步。 無論如何,這些問題並不僅限於Context API。(From Google Translation)

一些依賴於突變的庫的一個技巧就是克隆產生一個全新的外部容器(或者甚至只是在他們之間交替)。React將檢測到一個新物件的引用並且觸發一個更改。

每個Consumer只有一個Provider

建議的API只允許消費者從單一提供者型別讀取值,這與當前的API不同,後者允許消費者從任意數量的提供者型別讀取。

解決方法是使用合成消費者(待定):

<FooConsumer>
  {foo => (
    <BarConsumer>
      {bar => (
        // Render using both foo and bar
        <Child foo={foo} bar={bar} />
      )}
    </BarConsumer>
  )}
</FooConsumer>;
複製程式碼

大多數圍繞上下文的抽象已經使用了類似的模式。

備用方案

setContext

我們可以使用像setState一樣工作的setContext API,而不是依賴於引用相等來檢測對上下文的更改。 但是,如果不考慮實現的開銷,這個API只有在與突變結合使用時才有價值,我們專門致力於阻止這種突變。

將context傳遞給shouldComponentUpdate

一個說法是,我們可以通過將context作為引數傳遞給該方法來避免shouldComponentUpdate問題,將傳入的Context與前一個Context進行比較,如果它們不同,則返回true。 問題是,與prop或state不同,我們沒有型別資訊。 上下文物件的型別取決於元件在React樹中的位置。 您可以對這兩個物件執行淺層比較,但只有在我們假定這些值是不可變的時才有效。 如果我們假設這些值是不可變的,那麼React可能會自動進行比較

基於類的API

我們可以使用我們現在使用的基於類的API,而不是render prop:

class ThemeToggler extends React.Component {
  state = {theme: 'light'};
  getChildContext() {
    return this.state.theme;
  }
  render() {
    return (
      <>
        <button
          onClick={() =>
            this.setState(state => ({
              theme: state.theme === 'light' ? 'dark' : 'light',
            }))
          }>
          Toggle theme
        </button>
        {this.props.children}
      </>
    );
  }
}

class Title extends React.Component {
  static contextType = ThemeContext;
  componentDidUpdate(prevProps, prevState, prevContext) {
    if (this.context !== prevContext) {
      alert('Theme changed!');
    }
  }
  render() {
    return (
      <h1 style={{color: this.context.theme === 'light' ? '#000' : '#fff'}}>
        {this.props.children}
      </h1>
    );
  }
}
複製程式碼

這個API的優點是您可以更輕鬆地訪問生命週期方法中的上下文,可能避免在樹中需要額外的元件。

但是,儘管增加React樹的深度會帶來一些開銷,但使用特殊元件型別的優勢在於,為消費者掃描樹會更快,因為我們可以快速跳過其他型別。 使用基於類的API時,我們必須檢查每個類元件,它稍微慢一些。 這足以抵消額外元件的成本。

與類API不同,render prop API還具有與現有Context API充分不同的優勢,我們可以在過渡時期支援這兩個版本,而不會造成太多混淆。

(注意,原文部分接下來的一些章節並未翻譯,這些章節涉及的內容是React官方的一些計劃)

未解決的問題

當Consumer沒有匹配Provider時,是否應該發出警告

對於依賴預設Context的值,這是很有效的。在很多情況下,這種錯誤應該是開發者所造成的,因此我們可以列印警告出來。為了關閉這個,你可以傳遞true給allowDetached

render() {
  return (
    // If there's no provider, this renders with the default theme.
    // `allowDetached` suppresses the development warning
    <ThemeContext.Consumer allowDetached={true}>
      {theme => (
        <h1 style={{color: theme === 'light' ? '#000' : '#fff'}}>
          {this.props.children}
        </h1>
      )}
    </ThemeContext.Consumer>
  );
}
複製程式碼

將displayName引數新增到createContext以更好地進行除錯

對於警告和React DevTools,如果提供者和消費者具有displayName,則會有所幫助。 問題是這是否應該要求。 我們可以使其成為可選項,並使用Babel變換自動新增名稱。 這是我們用於createClass的策略。

其他

  • 消費者是否應該使用children作為prop或命名prop?
  • 我們應該多快棄用和刪除現有的上下文API?
  • 需要執行基準測試來確定這將是多快。
  • 啟發式快取。
  • 此功能的高優先順序版本用於動畫。 (可以單獨提交。)

相關文章