[譯]React高級話題之Context

前言

本文爲意譯,翻譯過程當中摻雜本人的理解,若有誤導,請放棄繼續閱讀。javascript

原文地址:Contexthtml

Context提供了一種不須要手動地經過props來層層傳遞的方式來傳遞數據。java

正文

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

何時用Context?

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

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。babel

Context主要用於這種業務場景:大量處在組件樹不一樣層級的組件須要共享某些數據。實際開發中,咱們對context要常懷敬畏之心,謹慎使用。由於它猶如潘多拉的盒子,一旦打開了,就形成不少難以控制的現象(在這裏特指,context一旦濫用了,就會形成不少組件難以複用)。app

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

舉個例子來講,假如咱們有一個叫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={/* 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在組件的各類生命週期方法都是可訪問的。

注意:

  1. 使用這個API,你只能夠訂閱一個context object。若是你須要讀取多個context object,那麼你能夠查看Consuming Multiple Contexts
  2. 若是你想使用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將會在將來的某個大版本中去除掉。想了解更多關於老context API,查看這裏

相關文章
相關標籤/搜索