This is the 19th day of my participation in the August Wenwen Challenge.More challenges in August

In react component development, key is a common attribute value, which is mainly used for list development. This article analyzes how key is used in React and whether key can be omitted from the perspective of source code.

ReactElement object

JSX code that we write directly at programming time is actually compiled into the ReactElement object, so key is an attribute of the ReactElement object.

The constructor

There is a compatibility issue when converting JSX into the syntax of a ReactElement object. Depending on the compiler’s strategy, two schemes are compiled.

  1. The latest translation strategy is to translate JSX syntactic code into JSX () function packages

    JSX functions: Keep only key-related code (the rest of the source code is not discussed in this section)

       /**
       * https://github.com/reactjs/rfcs/pull/107
       * @param {*} type
       * @param {object} props
       * @param {string} key* /
       export function jsx(type, config, maybeKey) {
         let propName;
    
         // 1. The default value of key is null
         let key = null;
    
         // Currently, key can be spread in as a prop. This causes a potential
         // issue if key is also explicitly declared (ie. 
            
    // or
    ). We want to deprecate key spread,
    // but as an intermediary step, we will use jsxDEV for everything except //
    , because we aren't currently able to tell if
    // key is explicitly declared to be undefined or not. if(maybeKey ! = =undefined) { key = ' ' + maybeKey; } if (hasValidKey(config)) { // 2. Convert the key to a string key = ' ' + config.key; } // 3. Pass key to the constructor return ReactElement( type, key, ref, undefined.undefined, ReactCurrentOwner.current, props, ); } Copy the code
  2. The traditional translation strategy is to wrap JSX syntax code in the react.createElement () function

    The react.createElement () function: keep only the code related to the key.

    /** * Create and return a new ReactElement of the given type. * See https://reactjs.org/docs/react-api.html#createelement */
    export function createElement(type, config, children) {
      let propName;
    
      // Reserved names are extracted
      const props = {};
    
      let key = null;
      let ref = null;
      let self = null;
      let source = null;
    
      if(config ! =null) {
        if (hasValidKey(config)) {
          key = ' ' + config.key; // Key is converted to a string}}return ReactElement(
        type,
        key,
        ref,
        self,
        source,
        ReactCurrentOwner.current,
        props,
      );
    }
    Copy the code

You can see that the core logic is the same no matter which compilation method is used:

  1. keyThe default value fornull
  2. If the outside world has a display specifiedkey, it willkeyTo a string type.
  3. callReactElementThe constructor, and willkeyThe incoming.
// ReactElement constructor: this section focuses on the key attribute
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };
  return element;
};
Copy the code

Source code see here, although still only a superficial, but at least know the key default value is null. So any reactElement object has a key inside it, but in general (non-list structure) no one is shown passing a key.

Fiber object

The core operational logic of React is a process from input to output (reviewing reconciler’s operational processes). The JSX for direct programming is the reactElement object, our data model is THE JSX, and the React kernel’s data model is a Fiber tree. To understand key, you need to look at it from fiber’s point of view.

The Fiber object is constructed during the Fiber tree construction cycle, and its constructor is as follows:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {

  this.tag = tag;
  this.key = key;  Key: key is also an attribute of the 'fiber' object

  // ... 
  this.elementType = null;
  this.type = null;
  this.stateNode = null;
  / /... Omit irrelevant code
}
Copy the code

As you can see, key is also a property of the Fiber object. The situation is different here with reactElement:

  1. reactElementIn thekeyIs made up ofjsxCompiled,keyIs directly controlled by the programmer (even if it is dynamically generated, that is also direct control)
  2. fiberObject is areactThe kernel is created at runtime, sofiber.keyIs alsoreactThe kernel does this, but the programmer has no direct control.

Logic comes here with two questions:

  1. fiber.keyIs made up ofreactKernel Settings, then its value andreactElement.keyThe same?
  2. ifreactElement.key = null, thenfiber.keyTo be innull?

To follow up on these issues, start with the creation of Fiber. As mentioned earlier, the creation of the Fiber object takes place during the Fiber tree construction cycle, specifically in the reconcilerChildren reconciliation function.

ReconcilerChildren harmonic functions

ReconcilerChildren is a star function in React, and the most popular issue is the principle of the Diff algorithm. In fact, the key works entirely for the DIFF algorithm.

Note: This section only analyzes the logic related to keys. For the algorithm principle of the harmonic function, please review the harmonic algorithm of the React algorithm in algorithm section

Harmonic function source code (this section example, only a partial excerpt of the code):

function ChildReconciler(shouldTrackSideEffects) {

  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ) :Fiber | null {
    // Handle object types
    const isObject = typeof newChild === 'object'&& newChild ! = =null;

    if (isObject) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          // newChild is a single node
          returnplaceSingleChild( reconcileSingleElement( returnFiber, currentFirstChild, newChild, lanes, ), ); }}// newChild is multi-node
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }
    // ...
  }

  return reconcileChildFibers;
}
Copy the code

A single node

This reconcileSingleElement is the case for single nodes, with only the logic relating to keys preserved:

function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ) :Fiber {
    const key = element.key;
    let child = currentFirstChild;
    while(child ! = =null) {
      // Key 1: Key is the first condition for determining whether a single node can be multiplexed
      if (child.key === key) { 
        switch (child.tag) {
          default: {
            if (child.elementType === element.type) { // Second judgment condition
              deleteRemainingChildren(returnFiber, child.sibling);
              // Node multiplexing: call useFiber
              const existing = useFiber(child, element.props);
              existing.ref = coerceRef(returnFiber, child, element);
              existing.return = returnFiber;
              return existing;
            }
            break; }}// Didn't match.
        deleteRemainingChildren(returnFiber, child);
        break;
      } 
      child = child.sibling;
    }
    // Key 2: the fiber node is created. 'key' is the constructor of 'fiber' along with the 'element' object
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
  }
Copy the code

As you can see, there are two key points for a single node:

  1. keyIs the first judgment condition of whether a single node is multiplexed (the second judgment condition istypeChange or not).
    • ifkeyNo, the other conditions are completely ignored
  2. When you create a new node,keyAs theelementObject passed infiberConstructor of.

Therefore, this is the core role of key, which is the first judgment condition for whether a single node can be reused in harmonic function.

Fiber. Key is a copy of reactElement. Key, and they are identical (including null defaults).

multi-node

Moving on to the multi-node logic:

 function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ) :Fiber | null {

    if (__DEV__) {
      // First, validate keys.
      let knownKeys = null;
      for (let i = 0; i < newChildren.length; i++) {
        const child = newChildren[i];
        // 1. In the dev environment, run warnOnInvalidKey.
        // - If the key is not set, a warning message will be displayed
        // - If the key is repeated, an error message will be displayed.knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber); }}let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    // First loop: only occurs during the update phase
    for(; oldFiber ! = =null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      // 1. Call updateSlot to process fiber in the public sequence
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
    }

    // The second loop
    if (oldFiber === null) {
      for (; newIdx < newChildren.length; newIdx++) {
        // 2. Call createChild to create a new fiber
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      }
      return resultingFirstChild;
    }

    for (; newIdx < newChildren.length; newIdx++) {
      // 3. Call updateFromMap to handle fiber in a non-public sequence
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
    }

    return resultingFirstChild;
  }
Copy the code

In reconcileChildrenArray, there are three calls to The reconcileChildrenArray that relate to Fiber (and, incidentally, to key), and they are distributed:

  1. updateSlot

        function updateSlot(
          returnFiber: Fiber,
          oldFiber: Fiber | null,
          newChild: any,
          lanes: Lanes,
        ) :Fiber | null {
          constkey = oldFiber ! = =null ? oldFiber.key : null;
    
          if (typeof newChild === 'object'&& newChild ! = =null) {
            switch (newChild.$$typeof) {
              case REACT_ELEMENT_TYPE: {
                // key: key is used to determine whether to reuse the first condition
                if (newChild.key === key) {
                  return updateElement(returnFiber, oldFiber, newChild, lanes);
                } else {
                  return null; }}}}return null;
        }
    Copy the code
  2. createChild

      function createChild(returnFiber: Fiber, newChild: any, lanes: Lanes,) :Fiber | null {
            if (typeof newChild === 'object'&& newChild ! = =null) {
              switch (newChild.$$typeof) {
                case REACT_ELEMENT_TYPE: {
                  // important: call the constructor to create
                  const created = createFiberFromElement(
                    newChild,
                    returnFiber.mode,
                    lanes,
                  );
                  returncreated; }}}return null;
          }
    Copy the code
  3. updateFromMap

        function updateFromMap(
          existingChildren: Map<string | number, Fiber>,
          returnFiber: Fiber,
          newIdx: number,
          newChild: any,
          lanes: Lanes,
        ) :Fiber | null {
    
          if (typeof newChild === 'object'&& newChild ! = =null) {
            switch (newChild.$$typeof) {
              case REACT_ELEMENT_TYPE: {
                // key: key is used to determine whether to reuse the first condition
                const matchedFiber =
                  existingChildren.get(
                    newChild.key === null ? newIdx : newChild.key,
                  ) || null;
                returnupdateElement(returnFiber, matchedFiber, newChild, lanes); }}return null;
        }
    Copy the code

If the reactElement does not display the set key (newChild.key === NULL), then the updateFromMap element (newChild.key == null) does not display the set key (newChild.key == null). It’s going to look it up with index.

Therefore, in the case of multi-nodes, the key is still the first judgment condition for whether to reuse or not. If the key is different, it will definitely not be reused.

conclusion

From the perspective of source code, this section analyzes the use of key in the React kernel from the perspectives of reactElement object and Fiber object. Finally, in the reconcilerChildren reconcilerChildren function, key is finally applied as the first judgment condition for node reuse.