React高級特性Context萬字詳細解讀

Context提供瞭一種不需要手動地通過props來層層傳遞的方式來傳遞數據。

正文

在典型的React應用中,數據是通過props,自上而下地傳遞給子組件的。但是對於被大量組件使用的固定類型的數據(比如說,本地的語言環境,UI主題等)來說,這麼做就顯得十分的累贅和笨拙。Context提供瞭一種在組件之間(上下層級關系的組件)共享這種類型數據的方式。這種方式不需要你手動地,顯式地通過props將數據層層傳遞下去。

什麼時候用Context?

這一小節,講的是context適用的業務場景。

Context是為那些可以認定為【整顆組件樹范圍內可以共用的數據】而設計的。比如說,當前已認證的用戶數據,UI主題數據,當前用戶的偏好語言設置數據等。舉個例子,下面的代碼中,為瞭裝飾Button component我們手動地將一個叫“theme”的prop層層傳遞下去。 傳遞路徑是:App -> Toolbar -> ThemedButton -> Button

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}
function Toolbar(props) {
  // The Toolbar component must take an extra "theme" prop
  // and pass it to the ThemedButton. This can become painful
  // if every single button in the app needs to know the theme
  // because it would have to be passed through all components.
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}
class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}

使用context,我們可以跳過層層傳遞所經過的中間組件。現在我們的傳遞路徑是這樣的:App -> Button。

// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');
class App extends React.Component {
  render() {
    // Use a Provider to pass the current theme to the tree below.
    // Any component can read it, no matter how deep it is.
    // In this example, we're passing "dark" as the current value.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}
// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
class ThemedButton extends React.Component {
  // Assign a contextType to read the current theme context.
  // React will find the closest theme Provider above and use its value.
  // In this example, the current theme is "dark".
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

在你用Context之前

這一小節,講的是我們要慎用context。在用context之前,我們得考慮一下當前的業務場景有沒有第二種技術方案可用。隻有在確實想不出來瞭,才去使用context。

Context主要用於這種業務場景:大量處在組件樹不同層級的組件需要共享某些數據。實際開發中,我們對context要常懷敬畏之心,謹慎使用。因為它猶如潘多拉的盒子,一旦打開瞭,就造成很多難以控制的現象(在這裡特指,context一旦濫用瞭,就會造成很多組件難以復用)。參考React實戰視頻講解:進入學習

如果你隻是單純想免去數據層層傳遞時對中間層組件的影響,那麼組件組合是一個相比context更加簡單的技術方案。

舉個例子來說,假如我們有一個叫Page的組件,它需要將useravatarSize這兩個prop傳遞到下面好幾層的Link組件和Avatar組件:

<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... which renders ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... which renders ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

我們大費周章地將useravatarSize這兩個prop傳遞下去,最終隻有Avatar組件才真正地用到它。這種做法顯得有點低效和多餘的。假如,到後面Avatar組件需要從頂層組件再獲取一些格外的數據的話,你還得手動地,逐層地將這些數據用prop的形式來傳遞下去。實話說,這真的很煩人。

不考慮使用context的前提下,另外一種可以解決這種問題的技術方案是:將Avatar組件作為prop傳遞下去。這樣一來,其他中間層的組件就不要知道user這個prop的存在瞭。

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}
// Now, we have:
<Page user={user} />
// ... which renders ...
<PageLayout userLink={...} />
// ... which renders ...
<NavigationBar userLink={...} />
// ... which renders ...
{props.userLink}

通過這個改動,隻有最頂層的組件Page需要知道Link組件和Avatar組件需要用到“user”和“avatarSize”這兩個數據集。

在很多場景下,這種通過減少需要傳遞prop的個數的“控制反轉”模式讓你的代碼更幹凈,並賦予瞭最頂層組件更多的控制權限。然而,它並不適用於每一個業務場景。因為這種方案會增加高層級組件的復雜性,並以此為代價來使得低層傢的組件來變得更加靈活。而這種靈活性往往是過度的。

在“組件組合”這種技術方案中,也沒有說限定你一個組件隻能有一個子組件,你可以讓父組件擁有多個的子組件。或者甚至給每個單獨的子組件設置一個單獨的“插槽(slots)”,正如這裡所介紹的那樣。

function Page(props) {
  const user = props.user;
  const content = <Feed user={user} />;
  const topBar = (
    <NavigationBar>
      <Link href={user.permalink}>
        <Avatar user={user} size={props.avatarSize} />
      </Link>
    </NavigationBar>
  );
  return (
    <PageLayout
      topBar={topBar}
      content={content}
    />
  );
}

這種模式對於大部分需要將子組件從它的父組件中分離開來的場景是足夠有用的瞭。如果子組件在渲染之前需要與父組件通訊的話,你可以進一步考慮使用render props技術。

然而,有時候你需要在不同的組件,不同的層級中去訪問同一份數據,這種情況下,還是用context比較好。Context負責集中分發你的數據,在數據改變的同時,能將新數據同步給它下面層級的組件。第一小節給出的范例中,使用context比使用本小節所說的“組件組合”方案更加的簡單。適用context的場景還包括“本地偏好設置數據”共享,“UI主題數據”共享和“緩存數據”共享等。

相關API

React.createContext

const MyContext = React.createContext(defaultValue);

該API是用於創建一個context object(在這裡是指Mycontext)。當React渲染一個訂閱瞭這個context object的組件的時候,將會從離這個組件最近的那個Provider組件讀取當前的context值。

創建context object時傳入的默認值隻有組件在上層級組件樹中沒有找到對應的的Provider組件的時候時才會使用。這對於脫離Provider組件去單獨測試組件功能是很有幫助的。註意:如果你給Provider組件value屬性提供一個undefined值,這並不會引用React使用defaultValue作為當前的value值。也就是說,undefined仍然是一個有效的context value。

Context.Provider

<MyContext.Provider value={<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->/* some value */}>

每一個context object都有其對應的Provider組件。這個Provider組件使得Consumer組件能夠訂閱並追蹤context數據。

它接受一個叫value的屬性。這個value屬性的值將會傳遞給Provider組件所有的子孫層級的Consumer組件。這些Consumer組件會在Provider組件的value值發生變化的時候得到重新渲染。從Provider組件到其子孫Consumer組件的這種數據傳播不會受到shouldComponentUpdate(這個shouldComponentUpdate應該是指Cousumer組件的shouldComponentUpdate)這個生命周期方法的影響。所以,隻要父Provider組件發生瞭更新,那麼作為子孫組件的Consumer組件也會隨著更新。

判定Provider組件的value值是否已經發生瞭變化是通過使用類似於Object.is算法來對比新舊值實現的。

註意:當你給在Provider組件的value屬性傳遞一個object的時候,用於判定value是否已經發生改變的法則會導致一些問題,見註意點。

Class.contextType

譯者註:官方文檔給出的關於這個API的例子我並沒有跑通。不知道是我理解錯誤還是官方的文檔有誤,讀者誰知道this.context在new context API中是如何使用的,麻煩在評論區指教一下。

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* perform a side-effect at mount using the value of MyContext */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* render something based on the value of MyContext */
  }
}
MyClass.contextType = MyContext;

組件(類)的contextType靜態屬性可以賦值為一個context object。這使得這個組件類可以通過this.context來消費離它最近的context value。this.context在組件的各種生命周期方法都是可訪問的。

註意:

使用這個API,你隻可以訂閱一個context object。如果你需要讀取多個context object,那麼你可以查看Consuming Multiple Contexts。

如果你想使用ES7的實驗性特征public class fields syntax,你可以使用static關鍵字來初始化你的contextType屬性:

class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* render something based on the value */
  }
}

Context.Consumer

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>

Consumer組件是負責訂閱context,並跟蹤它的變化的組件。有瞭它,你就可以在一個function component裡面對context發起訂閱。

如上代碼所示,Consumer組件的子組件要求是一個function(註意,這裡不是function component)。這個function會接收一個context value,返回一個React node。這個context value等同於離這個Consumer組件最近的Provider組件的value屬性值。假如Consumer組件在上面層級沒有這個context所對應的Provider組件,則function接收到的context value就是創建context object時所用的defaultValue。

註意:這裡所說的“function as a child”就是我們所說的render props模式。

示例

1. 動態context

我在這個例子裡面涉及到this.context的組件的某個生命周期方法裡面打印console.log(this.context),控制臺打印出來是空對象。從界面來看,DOM元素button也沒有background。

這是一個關於動態設置UI主題類型的context的更加復雜的例子:

theme-context.js

export const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
};
export const ThemeContext = React.createContext(
  themes.dark // default value
);

themed-button.js

import {ThemeContext} from './theme-context';
class ThemedButton extends React.Component {
  render() {
    let props = this.props;
    let theme = this.context;
    return (
      <button
        {...props}
        style={{backgroundColor: theme.background}}
      />
    );
  }
}
ThemedButton.contextType = ThemeContext;
export default ThemedButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';
// An intermediate component that uses the ThemedButton
function Toolbar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}>
      Change Theme    </ThemedButton>
  );
}
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: themes.light,
    };
    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };
  }
  render() {
    // The ThemedButton button inside the ThemeProvider
    // uses the theme from state while the one outside uses
    // the default dark theme
    // 以上註釋所說的結果,我並沒有看到。
    return (
      <Page>
        <ThemeContext.Provider value={this.state.theme}>
          <Toolbar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
        <Section>
          <ThemedButton />
        </Section>
      </Page>
    );
  }
}
ReactDOM.render(<App />, document.root);

2. 在內嵌的組件中更新context

組件樹的底層組件在很多時候是需要更新Provider組件的context value的。面對這種業務場景,你可以在創建context object的時候傳入一個function類型的key-value,然後伴隨著context把它傳遞到Consumer組件當中:

theme-context.js

// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

theme-toggler-button.js

import {ThemeContext} from './theme-context';
function ThemeTogglerButton() {
  // The Theme Toggler Button receives not only the theme
  // but also a toggleTheme function from the context
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>
          Toggle Theme        </button>
      )}    </ThemeContext.Consumer>
  );
}
export default ThemeTogglerButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';
class App extends React.Component {
  constructor(props) {
    super(props);
    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };
    // State also contains the updater function so it will
    // be passed down into the context provider
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }
  render() {
    // The entire state is passed to the provider
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}
function Content() {
  return (
    <div>
      <ThemeTogglerButton />
    </div>
  );
}
ReactDOM.render(<App />, document.root);

3. 同時消費多個context

為瞭使得context所導致的重新渲染的速度更快,React要求我們對context的消費要在單獨的Consumer組件中去進行。

// Theme context, default to light theme
const ThemeContext = React.createContext('light');
// Signed-in user context
const UserContext = React.createContext({
  name: 'Guest',
});
class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;
    // App component that provides initial context values
    // 兩個context的Provider組件嵌套
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}
function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}
// A component may consume multiple contexts
function Content() {
  return (
     // 兩個context的Consumer組件嵌套
    <ThemeContext.Consumer>
      {theme => (        <UserContext.Consumer>
          {user => (            <ProfilePage user={user} theme={theme} />
          )}        </UserContext.Consumer>
      )}    </ThemeContext.Consumer>
  );
}

但是假如兩個或以上的context經常被一同消費,這個時候你得考慮合並它們,使之成為一個context,並創建一個接受多個context作為參數的render props component。

註意點

因為context是使用引用相等(reference identity)來判斷是否需要re-redner的,所以當你給Provider組件的value屬性提供一個字面量javascript對象值時,這就會導致一些性能問題-consumer組件發生不必要的渲染。舉個例子,下面的示例代碼中,所有的consumer組件將會在Provider組件重新渲染的時候跟著一起re-render。這是因為每一次value的值都是一個新對象。

class App extends React.Component {
  render() {
    return (
     // {something: 'something'} === {something: 'something'}的值是false
      <Provider value={{something: 'something'}}>
        <Toolbar />
      </Provider>
    );
  }
}

為瞭避免這個問題,我們可以把這種引用類型的值提升到父組件的state中去:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }
  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}

遺留的API

React在先前的版本中引入瞭一個實驗性質的context API。相比當前介紹的這個context API,我們稱它為老的context API。這個老的API將會被支持到React 16.x版本結束前。但是你的app最好將它升級為上文中所介紹的新context API。這個遺留的API將會在未來的某個大版本中去除掉。

到此這篇關於React高級特性Context萬字詳細解讀的文章就介紹到這瞭,更多相關React Context內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: