Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 源码漂流(六)之 createContext #43

Open
sisterAn opened this issue Jul 21, 2019 · 0 comments
Open

React 源码漂流(六)之 createContext #43

sisterAn opened this issue Jul 21, 2019 · 0 comments

Comments

@sisterAn
Copy link
Owner

sisterAn commented Jul 21, 2019

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;
}
@sisterAn sisterAn changed the title React 源码解读系列(五)之 createContext React 源码漂流(五)之 createContext Jul 24, 2019
@sisterAn sisterAn changed the title React 源码漂流(五)之 createContext React 源码漂流(六)之 createContext Jul 24, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant