React Context最佳实践加源码解析

前言

在一个典型的React应用中, 数据都是经过props属性自顶向下传递的, 也就是咱们一般所说的父传子。可是在某些场景下(换肤), 不少底层的子组件都是须要接收来自于顶层组件的换肤属性, 这会让咱们的代码中有不少显示传递props的地方。Context 提供了一种在组件之间共享此类值的方式,而没必要显式地经过组件树的逐层传递 props。前端

什么状况下使用Context

Context的主要应用场景在于: 不一样层级 的组件须要访问相同的一些数据。node

下面咱们来看一个例子:react

场景是这样的: 页面的根组件是一个Page, 咱们须要向目标组件层层向下传递 useravatarSize, 从而深度嵌套的 AvatarInfo 组件能够读取到这些属性。编程

<Page user={user} avatarSize={avatarSize} />
 // ... 渲染出 ...

<PageLayout user={user} avatarSize={avatarSize} />
 // ... 渲染出 ...

<NavigationBar user={user} avatarSize={avatarSize} />
 // ... 渲染出 ...

<Avatar user={user} size={avatarSize} />
<Info user={user} size={avatarSize} />
复制代码

若是最后只有Avatar和Info组件使用到了这两个属性, 那么这种层层传递的方式会显示十分冗余。若是后面还须要新增相似colorbackground 等属性, 咱们还得在中间层一个个地加上。markdown

在React官方文档中, 提供了一种无需Context的方案, 使用 component composition(组件组合)在Page组件中将 AvatarInfo 组件传递下去。前端工程师

function Page(props) {
   const user = props.user;
   const userComponent = (
     <div> <Avatar user={user} size={props.avatarSize} /> <Info user={user} size={props.avatarSize} /> </div>
   );
   return <PageLayout userComponent={userLink} />;
 }

 <Page user={user} avatarSize={avatarSize} />
 // ... 渲染出 ...
 <PageLayout userComponent={...} />
 // ... 渲染出 ...
 <NavigationBar userComponent={...} />
 // ... 渲染出 ...
 {props.userComponent}
复制代码

经过上面这种方式, 能够减小咱们React代码中无用props的传递。可是这么作有一个缺点: 顶层组件会变得十分复杂。ide

那么这时候, Context是否是最佳的实践方案呢?函数

答案是不必定。由于一旦你使用了Context, 组件的复用率将会变得很低。字体

在16.x以前的Context API有一个很很差点: 若是PageLayout 的props改变,可是在他的生命周期中 shouldComponentUpdate 返回的是false, 会致使 AvatarInfo的值没法被更新。可是在16.x以后新版的 Context API, 就不会出现这个问题, 具体原理咱们在后面会讲到, 此处先简单的提一下。优化

若是咱们的这个组件不须要被复用, 那么我以为使用Context应该是目前的最佳实践了。我以为官网对Context的翻译中,这句话讲的特别好: Context 能让你将这些数据向组件树下全部的组件进行“广播”,全部的组件都能访问到这些数据,也能访问到后续的数据更新 , 当我初学的时候, 我一看到这句话,就对Context有了一个清晰且深入的认识, 可是做为一个前端工程师, 咱们的认知毫不停步于此。

import React, { Component, createContext, PureComponent } from 'react';

const SizeContext = createContext();

class User extends Component {
 render() {
   return <SizeContext.Consumer> { value => <span style={{ fontSize: value }}>我是其枫</span> } </SizeContext.Consumer>
 }
}

class PageLayout extends PureComponent {
 render() {
   return <NavigationBar />
 }
}

class NavigationBar extends PureComponent {
 render() {
   return <User/>
 }
}

class Page extends Component {
 state = {
   fontSize: 20
 }
 render() {
   const { fontSize } = this.state;
   return (
     <SizeContext.Provider value={fontSize}> <button type="button" onClick={() => this.setState({ fontSize: fontSize + 1 })} > 增长字体大小 </button> <button type="button" onClick={() => this.setState({ fontSize: fontSize - 1 })} > 减小字体大小 </button> <PageLayout /> </SizeContext.Provider>
   );
 }

}

export default Page;
复制代码

经过使用Context, 可让咱们的代码变得更加的优雅。若是你们以为这种写法还不太优雅

<SizeContext.Consumer>
    {
      value => <span style={{ fontSize: value }}>我是其枫</span>
    }
  </SizeContext.Consumer> 
复制代码

那么咱们可使用 contextType 来进一步优化代码

class User extends Component {
  static contextType = SizeContext;
  render() {
    return <span style={{ fontSize: this.context }}>我是其枫</span>
  }
}
复制代码

注意: 它只支持单个Context, 若是多个的话仍是只能使用嵌套的手法。

若是咱们想要定义多个Context, 好比新增一个颜色的Context的。咱们只需在Page组件中新增一个 Provider , 而且在消费方也新增一个 Consumer 便可。

<SizeContext.Provider value={fontSize}>
    <button type="button" onClick={() => this.setState({ fontSize: fontSize + 1 })} > 增长字体大小 </button>
    <button type="button" onClick={() => this.setState({ fontSize: fontSize - 1 })} > 减小字体大小 </button>
    <ColorContext.Provider value="red"> <PageLayout /> </ColorContext.Provider>
  </SizeContext.Provider>
复制代码
<SizeContext.Consumer>
    {
      fontSize => <ColorContext.Consumer>{color => <span style={{ fontSize, color }}>我是其枫</span> }</ColorContext.Consumer>
    }
  </SizeContext.Consumer>
复制代码

多个Context的消费方的编程体验其实仍是不太友好的, Hook出现以后, 也推出了 useContext 这个API, 帮助咱们解决了这个问题。

const User = () => {
  const fontSize = useContext(SizeContext);
  const color = useContext(ColorContext);
  return (
    <span style={{ fontSize, color }}>我是其枫</span>
  );
};
复制代码

在此, 我以为有两个点是值得咱们思考的:

  • 在16.X以后新版的Context是如何解决 相似shouldComponentUpdate的问题

  • Function Component是如何作到能够订阅多个Context的

接下来, 咱们将会带着你们层层解开谜团, 探索新版Context的实现原理。

新版Context的实现原理

有源码阅读经验的朋友应该对 ReactFiberBeginWork.js 下面的 beginWork 方法都不陌生吧。若是你以前没有看过源码也不要紧, 在本文你只须要知道这个方法是用来执行对整棵树的每个节点进行更新的操做便可。那么咱们的源码解析就从 beginWork 开始。

Context的设计

var context = {
    ?typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    Provider: null,
    Consumer: null
  };
复制代码

咱们经过createContext会建立一个context对象, 该对象包含一个 Provider 以及 Consumer

Provider的_context指向是context自己。

context.Provider = {
  $$typeof: REACT_PROVIDER_TYPE,
  _context: context,
};
复制代码

下面咱们来看一下Consumer, 它的_context也是指向它自己

const Consumer = {
    $$typeof: REACT_CONTEXT_TYPE,
    _context: context,
    _calculateChangedBits: context._calculateChangedBits,
  };
复制代码

Provider的更新

当咱们第一次渲染的时候, 此时fiber树上当前的节点确定是不存在的, 所以就不走 if (current !== null) { // ... } 这里面的逻辑。咱们在建立 ColorContext.Provider 的时候, React会为咱们的fiber节点打上一个 tag, 好比在此会被打上一个叫作 ContextProvider 的WorkTag。其余节点也是相似, 目前一共有18种tag。

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
复制代码

  • 首次更新Provider

当React处理Provider节点的时候, 会调用 updateContextProvider 方法进行Provider的更新

因为是首次渲染, 因此当前fiber上的props是空的, 不存在 memoizedProps。所以咱们在这一步中仅仅执行了 pushProviderreconcileChildren。首先调用 pushProvider 将当前fiber以及它的props推入到栈(Stack)中。在React更新的过程当中有一个栈模块(fiberStack), 在遍历树的时候, 它会存储上下文。

在推入完成后, context的当前的值会置为传进来的值。

if (isPrimaryRenderer) {
  push(valueCursor, context._currentValue, providerFiber);
  context._currentValue = nextValue;
}
复制代码

此时, Consumer上的值其实已经更新了。

固然若是执行 pushProvider 的时候发现不是第一次更新它会将 _currentValue2 的修改成最新的值。

push(valueCursor, context._currentValue2, providerFiber);
  context._currentValue2 = nextValue;
复制代码

最后执行 reconcileChildren 将结果赋值给workInProgress.child。

  • 非首次更新Provider

当再次更新Provider的时候, 程序会进入 oldProps !== null

if (oldProps !== null) {
  const oldValue = oldProps.value;
  // 计算新老context上props的变化
  const changedBits = calculateChangedBits(context, newValue, oldValue);
  if (changedBits === 0) {
    // No change. Bailout early if children are the same.
    if (
      oldProps.children === newProps.children &&
      !hasLegacyContextChanged()
    ) {
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }
  } else {
    // The context value changed. Search for matching consumers and schedule
    // them to update.
    propagateContextChange(
      workInProgress,
      context,
      changedBits,
      renderExpirationTime,
    );
  }
}
复制代码

程序是否更新取决于 calculateChangedBits计算后的值,

export function calculateChangedBits<T>( context: ReactContext<T>, newValue: T, oldValue: T, ) {
 if (
   (oldValue === newValue &&  
     (oldValue !== 0 || 1 / oldValue === 1 / (newValue: any))) || //排除 + 0 和 - 0
   (oldValue !== oldValue && newValue !== newValue) // eslint-disable-line no-self-compare 排除NaN
 ) {
   // No change
   return 0;
 } else {
   const changedBits =
     typeof context._calculateChangedBits === 'function'
       ? context._calculateChangedBits(oldValue, newValue)
       : MAX_SIGNED_31_BIT_INT;

   return changedBits | 0;
 }
}
复制代码

咱们能够看到在这段代码中, 若是props没有变化,而且排除了 +0 和 -0 以及NaN 的状况, 此时结果返回0。除此以外的其余状况都是要进行更新的。 节点的更新调用的是 propagateContextChange 这个函数

在首先渲染完成后, 咱们已经将子树赋值给workInProgress.child。所以在第二次更新的时候, 咱们能够直接经过 let fiber = workInProgress.child; 拿到子树。

此时, 咱们经过遍历fiber上的全部节点找到全部拥有 firstContextDependency的节点。

firstContextDependency 的初始化赋值在 readContext 方法中, 后面讲到 Consumer的时候, 咱们会说起。

接着经过while循环判断节点上依赖的context是否依赖当前context, 若是是React将会建立一个更新 createUpdate(), 而且打上 ForceUpdate的标签表明强制更新, 最后把它塞到队列里面。为了确保本次更新渲染周期内它必定会被执行, 咱们将 fiber上的 expirationTime的值改成当前正在执行更新的 expirationTime

最后更新workInProgress上的子树。

Consumer的更新

Consumer的更新比较纯粹, 它一共涉及三个主要阶段: 1. prepareToReadContext(准备读取Context) 2. readContext(开始读取readContext) 3.子节点的渲染

  • prepareToReadContext

    在每次准备读取Context的时候, React都会把树上挂着的依赖给清空

    export function prepareToReadContext( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): void {
      currentlyRenderingFiber = workInProgress;
      lastContextDependency = null;
      lastContextWithAllBitsObserved = null;
    
      // Reset the work-in-progress list
      workInProgress.firstContextDependency = null;
    }
    复制代码
  • readContext

    在readContext中, 经过链表存储的形式, 将fiber上全部依赖的context都记录下来。在下一次更新后, Provider那边就能够经过fiber节点, 拿到它所依赖的context了。

Hook下useContext的解析

Hook下的全部钩子都是经过dispatch派发出来的

const HooksDispatcherOnRerender: Dispatcher = {
  readContext,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: rerenderReducer,
  useRef: updateRef,
  useState: rerenderState,
  useDebugValue: updateDebugValue,
  useDeferredValue: rerenderDeferredValue,
  useTransition: rerenderTransition,
  useMutableSource: updateMutableSource,
  useOpaqueIdentifier: rerenderOpaqueIdentifier,

  unstable_isNewReconciler: enableNewReconciler,
};
复制代码

在上述的代码中, 在使用Hook的同时, 其实是调用了 readContext 这个API。它直接调用了底层的API, 让咱们能够直接得到最新context的值。

在咱们以前执行 pushProvider 的时候会分别将值赋值给context的 _currentValue _currentValue2. 所以当咱们调用useContext的时候readContext 返回的已经当前context的最新值了。

function readContext() {
  // ...
return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}
复制代码
相关文章
相关标签/搜索