关于react.js: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一旦滥用了,就会造成很多组件难以复用)。

如果你只是单纯想免去数据层层传递时对中间层组件的影响,那么组件组合是一个相比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主题数据”共享和“缓存数据”共享等。参考React实战视频解说:进入学习

相干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将会在将来的某个大版本中去除掉。

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据