Interpretation of React's source code and principles (16): Context and useContext

Written at the beginning of the column (stack armor)

  1. The author is not an expert in front-end technology, but just a novice in front-end technology who likes to learn new things. He wants to learn source code just to cope with the rapidly declining front-end market and the need to find a job. This column is the author's own thinking in the process of learning And experience, there are also many parts that refer to other tutorials. If there are errors or problems, please point them out to the author. The author guarantees that the content is 100% correct. Please do not use this column as a reference answer.

  2. The reading of this column requires you to have a certain foundation of React, JavaScript and front-end engineering. The author will not explain many basic knowledge points, such as: what is babel, what is the syntax of jsx, please refer to it when necessary material.

  3. Many parts of this column refer to a large number of other tutorials. If there are similarities, the author plagiarized them. Therefore, this tutorial is completely open source. You can integrate, summarize and add your own understanding to various tutorials as the author.

Contents of this section

This chapter is the last chapter of the React source code series. It mainly talks about the common hooks we use for cross-level communication——useContext. We will start with the creation and consumption of context, and then talk about how to use the hook of useContext to Simplify the operation, and finally analyze the operating principle and mounting process of the context.

Definition of Context

Context is a data transfer method officially provided by React that allows the parent component to provide data for the entire component tree below it. The reason for it is:

  • Props are a great way to explicitly pass data up the UI tree to components that use it. But when you need to pass parameters deep in the component tree and need to reuse the same parameters between components, passing props becomes cumbersome. The nearest root parent component may be far away from the component that needs the data, and promoting state too high can lead to a "passing props down layer by layer" situation.

  • Between multiple levels of Context is a data transfer method that does not require props to "directly" data to the required components

Use of Context

In React, the steps to use Context are as follows:

  1. Create a context.
  2. Provide this context in the component specifying the data .
  3. Consume this context in subcomponents

Here is a concrete example:

First we use createContextthis API to create a Context, which passes in an initial value and returns a context

const Context = React.createContext('default-value')

We can provide this context by wrapping the component in Provider, and the value in it is the initial value of the Provider for the subcomponent. The following is equivalent to all subcomponents wrapped in Context.Provider can obtain the same data through Context, the data of which The initial value is new-value.

Note: The value provided in the Provider is the default value of the context, and the value initialized by createContext is not the default value. Only when the Provider does not provide a default value, the default value at the time of definition will be used.

const Context = React.createContext('default-value')
function Parent() {
    
    
 return ( 
  // 在内部的后代组件都能够通过相同的 Ract.createContext() 的实例访问到 context 数据
  <Context.Provider value="new-value">
     <Children>
  <Context.Provider>
 )
}

And our subcomponents can consume our context, here we need to import it from the position where we defined the Context before, only the same Context can be used to get the same data

import Context from "xxxxxx"
<Context.Consumer>
      {
    
     v => {
    
    
        // 内部通过函数访问祖先组件提供的 Context 的值
        return <div> {
    
    v} </div>
      }}
</Context.Consumer>

We can change the content of Context by modifying the value provided by Provider, the following is an example:

  • We pass in a language and a method of changing the language as a context to subcomponents
  • The subcomponent receives these contents through the Consumer, and the subcomponent clicks the button to call the method provided by the Provider to change the language
  • One thing to note is that when the value of Context.Provider changes, all components that use Context will be forced to update
class App extends Component {
    
    
  setLanguage = language => {
    
    
    this.setState({
    
     language });
  };

  state = {
    
    
    language: "en",
    setLanguage: this.setLanguage
  };
    
  render() {
    
    
    return (
      <LanguageContext.Provider value={
    
    this.state}>
        <h2>Current Language: {
    
    this.state.language}</h2>
        <p>Click button to change to jp</p>
        <div>
          <LanguageSwitcher />
        </div>
      </LanguageContext.Provider>
    );
  }
}

class LanguageSwitcher extends Component {
    
    
  render() {
    
    
    return (
      <LanguageContext.Consumer>
        {
    
    ({
     
      language, setLanguage }) => (
          <button onClick={
    
    () => setLanguage("jp")}>
            Switch Language (Current: {
    
    language})
          </button>
        )}
      </LanguageContext.Consumer>
    );
  }
}

The definition and use of useContext

useContextis a React Hook that allows you to read and subscribe to the Context in the component

const value = useContext(SomeContext)

It actually simplifies the process of consuming Context. We no longer need to obtain the required data through Consumer, but we can get the internal value of Context through useContext:

import Context from "xxxxxx"
function Child() {
    
    
  const {
    
     ctx } = useContext(Context)
  return <div> {
    
    ctx} </div>
}

Context usage scenarios

  • Context is often used when a certain state needs to be provided to multiple subcomponents and subcomponents of subcomponents, such as login status management , night mode
  • Context can also be conveniently used to pass parameters between grandparents and grandchildren . If a grandchild of a component needs to use a state of its grandparent, under normal circumstances, it needs to pass through multiple layers of props, but using Context can avoid such complexity process

Context source code

Create Context

Now it's our highlight. Regarding the principle of context, let's start with the definition of the context class, which is in our packages/shared/ReactTypes.js file:

export type ReactContext<T> = {
    
    
  $$typeof: Symbol | number,
  Consumer: ReactContext<T>,           // 消费 context 的组件
  Provider: ReactProviderType<T>,      // 提供 context 的组件
  // 保存 2 个 value 用于支持多个渲染器并发渲染
  _currentValue: T,
  _currentValue2: T,
  _threadCount: number, // 用来追踪 context 的并发渲染器数量
  // DEV only
  _currentRenderer?: Object | null,
  _currentRenderer2?: Object | null,
    
  displayName?: string,  // 别名
  _defaultValue: T,      
  _globalName: string,
  ...
};

createContext is to create such a new data structure, including data, Consumer and Provider for users to use. Its code is in the file packages/react/src/ReactContext.js:

export function createContext<T>(defaultValue: T): ReactContext<T> {
    
    
    
  const context: ReactContext<T> = {
    
    
    $$typeof: REACT_CONTEXT_TYPE,   // 用 $$typeof 来标识这是一个 context
    _currentValue: defaultValue,    // 给予初始值
    _currentValue2: defaultValue,   // 给予初始值
    _threadCount: 0,
    Provider: (null: any),
    Consumer: (null: any),
    _defaultValue: (null: any),
    _globalName: (null: any),
  };
	
  // 添加 Provider ,并且 Provider 中的_context指向的是 context 对象
  context.Provider = {
    
    
    $$typeof: REACT_PROVIDER_TYPE,   // 用 $$typeof 来标识这是一个 Provider 的 symbol
    _context: context,
  };

  let hasWarnedAboutUsingNestedContextConsumers = false;
  let hasWarnedAboutUsingConsumerProvider = false;
  let hasWarnedAboutDisplayNameOnConsumer = false;
	
  // 添加 Consumer
  context.Consumer = context;
  return context;
}

Provide Context

After creating our Context, the next step is to provide Context. We know that Context uses Provider to provide Context content, and <Context.Provider>this written in our jsx code as a DOM element node. What is its value? What is provided to the child elements, let's look at:

As we mentioned above, we use $$typeof to identify a Provider. Readers who have read the previous tutorials should be familiar with this. As we mentioned in the first tutorial, we use this field in our ReactElement to identify that this is a react. element. Similarly, here we also use this field to identify the Provider element, so that we can perform unified processing when generating Fiber.

The code is in our /packages/react-reconciler/src/ReactFiber.old.js function, we call createFiberFromTypeAndProps to create Fiber, in which we judge by the type passed in, and give our Fiber according to the value of $$typeof The tag adds different values. In the above, when creating the context, the Provider gave the REACT_PROVIDER_TYPE type, and the Consumer points to the context itself, so it is the REACT_CONTEXT_TYPE type field. Therefore, when we parse these two types in jsx, it is It will be determined as the corresponding field:

export function createFiberFromTypeAndProps(
  type: any, // React$ElementType,element的类型
  key: null | string,
  pendingProps: any,
  owner: null | Fiber,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
    
    
  let fiberTag = IndeterminateComponent;
  let resolvedType = type;
  if (typeof type === 'function') {
    
    
	// ....
  } else if (typeof type === 'string') {
    
    
	// ....
  } else {
    
    
    getTag: switch (type) {
    
    
	// ....
      default: {
    
    
        if (typeof type === 'object' && type !== null) {
    
    
          switch (type.$$typeof) {
    
    
            case REACT_PROVIDER_TYPE:
              fiberTag = ContextProvider;
              break getTag;
            case REACT_CONTEXT_TYPE:
              fiberTag = ContextConsumer;
              break getTag;
			//.....
          }
        }
      }
    }
  }

After determining the corresponding type, we continue to look at the processing of Fiber: in the function beginWork , we will process different tags. Let's first look at the processing of ContextProvider:

  • First, we get the currently incoming pendingProps, which is the props we passed in, and then get the value we passed in
  • Then we call the pushProvider function, which modifies the _currentValue of the context, that is, updates the value of the context
  • The pushProvider function also performs a push operation, which we will describe in detail later
  • Then we judge whether it can be reused (we have mentioned this in detail before)
  • If it cannot be reused, we need to mark our update through the propagateContextChange method
function updateContextProvider(current, workInProgress, renderLanes) {
    
    
  const providerType = workInProgress.type
  const context = providerType._context
  const newProps = workInProgress.pendingProps
  const oldProps = workInProgress.memoizedProps
  const newValue = newProps.value
  pushProvider(workInProgress, context, newValue)
  // 是更新
  if (oldProps !== null) {
    
    
    const oldValue = oldProps.value
    // 可以复用
    if (is(oldValue, newValue)) {
    
    
      if (
        oldProps.children === newProps.children &&
        !hasLegacyContextChanged()
      ) {
    
    
        return bailoutOnAlreadyFinishedWork(
          current,
          workInProgress,
          renderLanes,
        )
      }
    } else {
    
    
      // 查找 consumer 消费组件,标记更新
      propagateContextChange(workInProgress, context, renderLanes)
    }
  }
  // 继续遍历
  const newChildren = newProps.children
  reconcileChildren(current, workInProgress, newChildren, renderLanes)
  return workInProgress.child
}

function pushProvider(providerFiber, context, nextValue) {
    
    
  // 压栈
  push(valueCursor, context._currentValue, providerFiber)
  // 修改 context 的值
  context._currentValue = nextValue
}

function push<T>(cursor: StackCursor<T>, value: T, fiber: Fiber): void {
    
    
  index++;
  valueStack[index] = cursor.current;
  cursor.current = value;
}

We found that if we need to update our context, it will call the propagateContextChange method to mark the update, so what is its specific logic? It mainly calls the function propagateContextChange_eager, let's take a look at this function:

Depth-first traverses all the descendant fibers, and then finds the attribute dependenciesinside , which mounts all the contexts that an element depends on. Its mounting will be mentioned in the next section. Compare dependenciesthe context in and the current Provider Whether the context is the same; if it is the same, it will create an update with a high fiber update priority, similar to the update brought by calling this.forceUpdate:

function propagateContextChange_eager<T>(
  workInProgress: Fiber,
  context: ReactContext<T>,
  renderLanes: Lanes,
): void {
    
    
  let fiber = workInProgress.child;
  if (fiber !== null) {
    
    
    fiber.return = workInProgress;
  }
  // 深度优先遍历整个 fiber 树
  while (fiber !== null) {
    
    
    let nextFiber;
    const list = fiber.dependencies;
    if (list !== null) {
    
    
      nextFiber = fiber.child;
      let dependency = list.firstContext;
      // 获取 dependencies
      while (dependency !== null) {
    
    
        // 如果是同一个 context
        if (dependency.context === context) {
    
    
          if (fiber.tag === ClassComponent) {
    
    
            const lane = pickArbitraryLane(renderLanes);
            const update = createUpdate(NoTimestamp, lane);
            // 高优先级,强制更新
            update.tag = ForceUpdate;
            const updateQueue = fiber.updateQueue;
            if (updateQueue === null) {
    
    
            } else {
    
    
              const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
              const pending = sharedQueue.pending;
              if (pending === null) {
    
    
                update.next = update;
              } else {
    
    
                update.next = pending.next;
                pending.next = update;
              }
              sharedQueue.pending = update;
            }
          }
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate;
          if (alternate !== null) {
    
    
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          scheduleContextWorkOnParentPath(
            fiber.return,
            renderLanes,
            workInProgress,
          );
          list.lanes = mergeLanes(list.lanes, renderLanes);
          break;
        }
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
    
    
      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
    } else if (fiber.tag === DehydratedFragment) {
    
    
      //... 省略
    } else {
    
    
      nextFiber = fiber.child;
    }
    // 深度优先遍历找到下一个节点
    if (nextFiber !== null) {
    
    
      nextFiber.return = fiber;
    } else {
    
    
      nextFiber = fiber;
      while (nextFiber !== null) {
    
    
        if (nextFiber === workInProgress) {
    
    
          nextFiber = null;
          break;
        }
        const sibling = nextFiber.sibling;
        if (sibling !== null) {
    
    
          sibling.return = nextFiber.return;
          nextFiber = sibling;
          break;
        }
        nextFiber = nextFiber.return;
      }
    }
    fiber = nextFiber;
  }
}

Consumption Context

Above we mentioned the processing of <Context.Provider>nodes , then we will talk about the source code processing methods of different consumption methods:

First of all, Context.Consumer is the most commonly used method. Its processing is still in beginWorkthe function . As we mentioned in the previous part, Consumer points to the context itself, so it is the REACT_CONTEXT_TYPE type field. When generating fiber, it will recognize the REACT_CONTEXT_TYPE type and add the ContextConsumer tag , when we recognize this tag, we will call updateContextConsumer for processing.

The logic in updateContextConsumer is to first obtain the latest context value through prepareToReadContext and readContext, and then pass the latest value to the subcomponent for update operation:

function beginWork(current, workInProgress, renderLanes) {
    
    
  switch (workInProgress.tag) {
    
    
    case ContextConsumer:
      return updateContextConsumer(current, workInProgress, renderLanes)
  }
}

function updateContextConsumer(current, workInProgress, renderLanes) {
    
    
  let context = workInProgress.type
  context = context._context
  const newProps = workInProgress.pendingProps
  const render = newProps.children
  // 准备读取 context
  prepareToReadContext(workInProgress, renderLanes)
  // 获取最新的 context
  const newValue = readContext(context)
  // 更新包裹的子组件
  let newChildren
  newChildren = render(newValue)
  reconcileChildren(current, workInProgress, newChildren, renderLanes)
  return workInProgress.child
}

In prepareToReadContext, set currentlyRenderingFiber as the current node, which is convenient for subsequent access. If the current node does not have a dependencies linked list, initialize a linked list, which is used for us to mount the context element.

In readContext, it collects all the different contexts that the component depends on, then adds the context to fiber.dependenciesthe linked list , and then returns our context._currentValue as the value we need. The generated dependencies will be used later when we update a context , we have mentioned above

function prepareToReadContext(workInProgress, renderLanes) {
    
    
  currentlyRenderingFiber = workInProgress
  lastContextDependency = null
  lastFullyObservedContext = null   // 重置
  const dependencies = workInProgress.dependencies
  if (dependencies !== null) {
    
    
    const firstContext = dependencies.firstContext
    if (firstContext !== null) {
    
    
      if (includesSomeLane(dependencies.lanes, renderLanes)) {
    
    
        markWorkInProgressReceivedUpdate()
      }
      dependencies.firstContext = null
    }
  }
}
export function readContext<T>(context: ReactContext<T>): T {
    
    
    
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  if (lastFullyObservedContext === context) {
    
    
      // 不是可以使用 context 的时机
  } else {
    
    
    const contextItem = {
    
    
      context: ((context: any): ReactContext<mixed>),
      memoizedValue: value,
      next: null,
    };
    if (lastContextDependency === null) {
    
    

      lastContextDependency = contextItem;
      currentlyRenderingFiber.dependencies = {
    
    
        lanes: NoLanes,
        firstContext: contextItem,
      };
    } else {
    
    
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  return value;
}

useContext

useContext is used as a hook for function components. Its function is basically the same as the direct consumption above, but the position of function is changed to the related function of hooks. We can see useContextthat OnMountthe and are OnUpdateactually calling the readContext function, which is our function above:

const HooksDispatcherOnMount: Dispatcher = 
  useContext: readContext,
  //....
};

const HooksDispatcherOnUpdate: Dispatcher = {
    
    
  useContext: readContext,
  //....
};

destroy context

Finally, we look at the commit stage. In completeWorkthe function , we call a function popProvider, which pushProviderechoes throws an element in the stack:

function popProvider(providerFiber) {
    
    
  var currentValue = valueCursor.current;
  pop(valueCursor, providerFiber);
  var context = providerFiber.type._context;

  {
    
    
    context._currentValue = currentValue;
  }
}

function pop<T>(cursor: StackCursor<T>, fiber: Fiber): void {
    
    
  if (index < 0) {
    
    
    return;
  }
  cursor.current = valueStack[index];
  valueStack[index] = null;
  index--;
}

Now let's analyze why we use this stack to store our context:

Suppose we have a piece of code like the following, because our context is cross-level, if we need to pass the context value in the component during depth-first traversal, we need to pass it down layer by layer, so that The extra overhead is unacceptable, so we use a global variable to record it. However, Providers may be nested, and there will be multiple Providers with different values ​​in the code.

Simply we use depth-first search to traverse Fiber, which is a first-in-last-out delivery process, so we can use a stack to record all our contexts, and when we read a Provider, put the value into the stack , so that its children can read this value during operation. If another Provider is nested in it, we add a bit to the stack and update the value, so that the child node of this nested Provider gets It is the new value closest to it. When this Provider is destroyed, we throw this value from the stack, and then other subcomponents wrapped by the upper-level Provider will get the value of the upper-level Provider.

render() {
    
    
  return (
    <>
      <TestContext.Provider value={
    
    10}>
        <Test1 />
        <TestContext.ProviderProvider value={
    
    100}>
          <Test2 />
        </TestContext.Provider>
      </TestContext.Provider>
    </>
  )
}

The above principle also explains one point: why when the value of Context.Provider changes, all components using Context will be forced to update, because it may involve nested Providers. If we update locally, it will not be normal To update our stack, we need to destroy the entire stack and then regenerate it to ensure that the calling order does not change, so we need to re-render all the sub-elements it wraps.

Summarize

The above is the context related content, let's summarize:

  • In react, using Context needs to create a context, provide , and then consume this context in the child component
  • The user createContextcreates a context, which initializes data, Consumer and Provider, and makes them point to the same ReactContext object to ensure that the user always gets the latest context. _currentValue The properties of ReactContext contain the data of this context
  • We use $$typeof to identify a component as Consumer or Provider, they will be processed as reactElement objects, and different tags are used to identify them when generating Fiber
  • When the Provider is initialized, we will push the value of our context onto the stack in beginWork
  • When Consumer is initialized, all contexts that a Fiber depends on will be put into a dependencieslinked list , because Consumer points to ReactContext itself, so we can get the required objects directly through _currentValue
  • When a context is updated, the Provider will make a judgment. If the value changes and cannot be reused, it will call recursively propagateContextChangeto traverse all child nodes. The node using this Provider will be marked as a mandatory update priority and will be updated in the subsequent process.
  • When a Provider is processed, in the commit phase, the value pushed onto the stack will be popped out, and then the corresponding context value will also be updated to the content of the previous node in the stack. This is done to ensure that in multiple nested contexts, The user always gets the value provided by the nearest Provider
  • As a hook, useContext itself is just to adapt the function component. What it does is to call the readContext function in the Consumer logic to get the value of the context

So far, we have updated the React source code tutorial section by section. The content is complicated and it took a lot of time to read and understand. After that, I will find time to write a summary and rational article as the end of this tutorial. Thanks to everyone who read this far.

Guess you like

Origin blog.csdn.net/weixin_46463785/article/details/131260862