[譯]React全新的Context API

連接

原文地址react

中文翻譯git

blabla

翻譯水平有限,部份內容比較晦澀,所以可能錯誤較多,請理解或指教。github

簡介

介紹用於解決現有侷限性的全新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(消費者)。markdown

現有版本的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
  • 靜態的類型
  • 鼓勵異步友好的實踐,例如不可變性。
  • 組織費非理想的作法,例如事件發生器與變異。
  • 消除用戶級代碼中重複的複雜性。

詳細的設計

介紹全新的組件類型:ProviderConsumeride

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?
  • 須要運行基準測試來肯定這將是多快。
  • 啓發式緩存。
  • 此功能的高優先級版本用於動畫。 (能夠單獨提交。)
相關文章
相關標籤/搜索