女王控的博客

React之createContext入门学习

初识 context

在典型的 React 应用中,数据是通过 props 属性显式的由父及子进行传递的,但这种方式,对于复杂情况(例如,跨多级传递,多个组件共享)来说,是极其繁琐的。

组件的封装与组合,将组件自身传递下去

在项目中,我们在父层获取数据,不同层级的子组件访问时,我们可以使用将子组件的公共组件封装,将公共组件传递下去

jsx 复制代码
function Page(props) {
  // 你可以传递多个子组件,甚至会为这些子组件(children)封装多个单独的接口(slots)
  const localeCom = <span>{props.locale}</span>;
  return <Content localeCom={localeCom} />;
} // 这种情况下,只有顶层 Page 才知道 localeCom 的具体实现,实现了组件的控制反转

function Content(props) {
  return (
    <div>
      <FirstComponent localeCom={localeCom} />
    </div>
  );
}

class FirstComponent extends React.Component {
  render() {
    return <div>FirstComponent: {this.props.localeCom}</div>;
  }
}

这种对组件的控制反转减少了在应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件提到高层实现,这很多时候有违常理。

context

context 提供了一种在组件之间共享此类值的方式,使得我们无需每层显式添加 props 或传递组件,就能够实现在组件树中传递数据

jsx 复制代码
// Context 可以让我们无须显式地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 locale 创建一个 context(默认值为 anan)。
// context 会在每一个创建或使用 context 的组件上引入,所以,最好在单独一个文件中定义
// 这里只做演示
const LocaleContext = React.createContext('anan');

class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 name 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “ananGe” 作为当前的值传递下去。
    return (
      // Provider 接收一个 value 属性,传递给消费组件
      <LocaleContext.Provider value='ananGe'>
        <Content />
      </LocaleContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 locale 了。
// LocaleContext 分别在 FirstComponent 组件与 SecondComponent 的子组件 SubComponent 中使用
function Content(props) {
  return (
    <div>
      <FirstComponent />
      <SecondComponent />
    </div>
  );
}

// 第一个子组件
class FirstComponent extends React.Component {
  // 指定 contextType 读取当前的 locale context。
  // React 会往上找到最近的 locale Provider,然后使用它的值。
  // 在这个例子中,当前的 locale 值为 ananGe
  static contextType = LocaleContext;
  render() {
    return (
      <div>
        FirstComponent: <span>{this.context}</span>
      </div>
    );
  }
}

// 第二个子组件(中间件)
function SecondComponent(props) {
  return (
    <div>
      <SubComponent />
    </div>
  );
}
// SecondComponent 的子组件
class SubComponent extends React.Component {
  static contextType = LocaleContext;
  render() {
    return (
      <div>
        SubComponent: <span>{this.context}</span>
      </div>
    ); // this.context 为传递过来的 value 值
  }
}

注意:在大多数情况下,context 一般用来做中间件的方式使用,例如 redux

React.createContext

js 复制代码
const LocaleContext = React.createContext(defaultValue);
// 创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

Context.Provider

jsx 复制代码
<LocaleContext.Provider value={/* 某个值 */}>
  • Provider 接收一个 value 属性,传递给消费组件。
  • 一个 Provider 可以和 多个消费组件 有对应关系。多个 Provider 可以 嵌套使用 ,里层的会覆盖外层的数据。
  • 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会 重新渲染
  • Provider 及其内部 consumer 组件都 不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
  • 通过新旧值检测来确定变化,使用了与 Object.isObject.is MDN) 相同的算法。

Class.contextType

js 复制代码
// 挂载在 SubComponent 上的 contextType 属性会被重赋值为 LocaleContext
SubComponent.contextType = LocaleContext;
// 使用 this.context 来消费最近 Context 上的那个值
let value = this.context;

// 你可以使用这种方式来获取 context value,也可以使用 Context.Consumer 函数式订阅获取

Context.Consumer

jsx 复制代码
// 在函数式组件中完成订阅 context
<LocaleContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</LocaleContext.Consumer>

深入 context

  • 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会 重新渲染
  • 当需要在 Consumer 中触发 Provider 执行更新 context value 操作 时,可以通过 context 传递一个 函数 ,使得 consumer 组件触发更新 context
  • 多个 context 可以 嵌套使用
  • 注意: 不要在 Provider value 直接赋值<LocaleProvider.Provider value={{name: 'AnGe'}}>),因为这样会导致,每次 Provider 的父组件进行重渲染时,都会导致 Consumer 组件中重新渲染,因为 value 属性总是被赋值为新的对象(Object.is 新旧值检测)

locale-context.js

js 复制代码
export const locales = {
  An: {
    name: 'an',
    color: 'red'
  },
  AnGe: {
    name: 'anGe',
    color: 'green'
  }
};

export const LocaleContext = React.createContext(
  locales.An // 默认值
);

// 确保传递给 createContext 的默认值数据结构是调用的组件(consumers)所能匹配的!
export const AddressContext = React.createContext({
  address: 'Shanghai',
  updateAddress: () => {} // Consumer 更新 Provider value 函数
});

app.jsx

jsx 复制代码
import { locales, LocaleContext, AddressContext } from './locale-context';
import SubComponent from './SubComponent';

class App extends React.Component {
  state = {
    locale: locales.An,
    address: 'Beijing'
  };
  // 更新 locale 函数
  changePerson = () => {
    this.setState((state) => ({
      locale: state.locale === locales.An ? locales.AnGe : locales.An
    }));
  };
  // 更新 address 函数
  updateAddress = () => {
    this.setState((state) => ({
      address: state.address === 'Beijing' ? 'Shanghai' : 'Beijing'
    }));
  };

  render() {
    const { locale, address } = this.state;

    // addressValue 包含了 updateAddress 更新函数
    const addressValue = {
      address: 'Beijing',
      updateAddress: this.updateAddress
    };
    return (
      <div>
        // 在 LocaleProvider 内部的 SubComponent 组件使用 state 中的 locale 值
        // 当 LocaleProvider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染
        <LocaleProvider.Provider value={locale}>
          // addressValue 都被传递进 AddressContext.Provider
          <AddressContext.Provider value={addressValue}>
            <Toolbar changePerson={this.changePerson} />
          </AddressContext.Provider>
        </LocaleProvider.Provider>
        // 而外部的组件,没有被 LocaleProvider.Provider 包裹,则使用默认的 locale 值
        <div>
          <SubComponent />
        </div>
      </div>
    );
  }
}

// 一个使用 SubComponent 的中间组件
function Toolbar(props) {
  return <SubComponent onClick={props.changePerson}>Change Person</SubComponent>;
}

ReactDOM.render(<App />, document.root);

SubComponent.jsx

jsx 复制代码
import { LocaleContext, AddressContext } from './locale-context';

class SubComponent extends React.Component {
  render() {
    const props = this.props;
    return (
      // 一个组件可能会消费多个 context
      <LocaleContext.Consumer>
        {(locale) => (
          <div {...props} style={{ color: locale.color }}>
            {locale.name}
            <AddressContext.Consumer>
              {' '}
              // AddressContext.Consumer 可以从 context 中获取到 address 值 与 updateAddress 函数
              {(
                address,
                updateAddress // 点击 button,执行 AddressContext.Provider 的 updateAddress 函数,更新 address
              ) => <button onClick={updateAddress}>{address}</button>}
            </AddressContext.Consumer>
          </div>
        )}
      </LocaleContext.Consumer>
    );
  }
}

export default SubComponent;

源码解读

js 复制代码
export function createContext<T>(
  defaultValue: T, // context 默认值
  calculateChangedBits: ?(a: T, b: T) => number // 计算新老 context 变化函数
): ReactContext<T> {
  if (calculateChangedBits === undefined) {
    calculateChangedBits = null;
  } else {
    if (__DEV__) {
      warningWithoutStack(
        calculateChangedBits === null || typeof calculateChangedBits === 'function',
        'createContext: Expected the optional second argument to be a ' + 'function. Instead received: %s',
        calculateChangedBits
      );
    }
  }

  // 声明了一个 context 对象
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    _calculateChangedBits: calculateChangedBits,
    // As a workaround to support multiple concurrent renderers, we categorize
    // some renderers as primary and others as secondary. We only expect
    // there to be two concurrent renderers at most: React Native (primary) and
    // Fabric (secondary); React DOM (primary) and React ART (secondary).
    // Secondary renderers store their context values on separate fields.
    _currentValue: defaultValue, // 用来记录 context 最新值,当 Provider value 更新时,同步到 _currentValue 上
    _currentValue2: defaultValue,
    // Used to track how many concurrent renderers this context currently
    // supports within in a single renderer. Such as parallel server rendering.
    _threadCount: 0,
    // These are circular
    Provider: (null: any), // context Provider
    Consumer: (null: any) // context Consumer
  };

  context.Provider = {
    // context.Provider 的 _context 为 context
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context
  };

  let hasWarnedAboutUsingNestedContextConsumers = false;
  let hasWarnedAboutUsingConsumerProvider = false;

  if (__DEV__) {
    // A separate object, but proxies back to the original context object for
    // backwards compatibility. It has a different $$typeof, so we can properly
    // warn for the incorrect usage of Context as a Consumer.
    const Consumer = {
      //Consumer 的 _context 也为 context
      $$typeof: REACT_CONTEXT_TYPE,
      _context: context,
      _calculateChangedBits: context._calculateChangedBits
    };
    // $FlowFixMe: Flow complains about not setting a value, which is intentional here
    Object.defineProperties(Consumer, {
      Provider: {
        get() {
          if (!hasWarnedAboutUsingConsumerProvider) {
            hasWarnedAboutUsingConsumerProvider = true;
            warning(
              false,
              'Rendering <Context.Consumer.Provider> is not supported and will be removed in ' +
                'a future major release. Did you mean to render <Context.Provider> instead?'
            );
          }
          return context.Provider;
        },
        set(_Provider) {
          context.Provider = _Provider;
        }
      },
      _currentValue: {
        get() {
          return context._currentValue;
        },
        set(_currentValue) {
          context._currentValue = _currentValue;
        }
      },
      _currentValue2: {
        get() {
          return context._currentValue2;
        },
        set(_currentValue2) {
          context._currentValue2 = _currentValue2;
        }
      },
      _threadCount: {
        get() {
          return context._threadCount;
        },
        set(_threadCount) {
          context._threadCount = _threadCount;
        }
      },
      Consumer: {
        get() {
          if (!hasWarnedAboutUsingNestedContextConsumers) {
            hasWarnedAboutUsingNestedContextConsumers = true;
            warning(
              false,
              'Rendering <Context.Consumer.Consumer> is not supported and will be removed in ' +
                'a future major release. Did you mean to render <Context.Consumer> instead?'
            );
          }
          return context.Consumer;
        }
      }
    });
    // $FlowFixMe: Flow complains about missing properties because it doesn't understand defineProperty
    context.Consumer = Consumer;
  } else {
    context.Consumer = context;
  }
  // Provider 与 Consumer 均指向 context,也就是说,Provider 与 Consumer 使用同一个变量 _currentValue,当 Consumer 需要渲染时,直接从自身取得 context 最新值 _currentValue 去渲染
  if (__DEV__) {
    context._currentRenderer = null;
    context._currentRenderer2 = null;
  }

  return context;
}

评论

阅读上一篇

CommonJS规范总结与扩展
2019-09-24 14:32:18

阅读下一篇

JS基础之继承
2019-09-21 02:26:30
0%