关于react.js:React高级特性之Context

38次阅读

共计 10615 个字符,预计需要花费 27 分钟才能阅读完成。

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 主题数据”共享和“缓存数据”共享等。

相干 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。

参考 React 实战视频解说:进入学习

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

正文完
 0