sisterAn / blog

瓶博:每日更新,前端前进

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

React 源码漂流(六)之 createContext

sisterAn opened this issue · comments

context

一、初识 context

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

  • 第一种解决方式是: 组件的封装与组合,将组件自身传递下去

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

    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 或传递组件 ,就能够实现在 组件树中传递数据

    // 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

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

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

      // 挂载在 SubComponent 上的 contextType 属性会被重赋值为 LocaleContext
      SubComponent.contextType = LocaleContext;
      // 使用 this.context 来消费最近 Context 上的那个值
      let value = this.context;
      
      // 你可以使用这种方式来获取 context value,也可以使用 Context.Consumer 函数式订阅获取
    • Context.Consumer

      // 在函数式组件中完成订阅 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

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.js

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.js

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;

三、源码解读

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;
}