【学习笔记】React18 Reconciler 更新

118 阅读6分钟

0. 示例项目代码

function App() {
  const [num, setNum] = useState(0)
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p onClick={() => setNum(num+1)}>
          {num}
        </p>
      </header>
    </div>
  );
}

点击p标签后工作流程,打开chrome performance点击录制,

image.png 可以看到是从flushSyncCallbacks开始,而这个方法执行的是performSyncWorkOnRoot

performSyncWorkOnRoot

由于之前的章节已经记录过就不再赘述

function performSyncWorkOnRoot(root: FiberRoot) {
  // 进入渲染阶段
  let exitStatus = renderRootSync(root, lanes);
}

renderRootSync

function renderRootSync(root, lanes)
{
  prepareFreshStack(root, lanes);
  // 工作循环
  workLoopSync();
}

prepareFreshStack

React 18 中的一个新功能,它是为了优化 React 的更新和渲染过程,具体来说,它在 React 中引入了一种新的数据结构,称为“Stack”,以替代旧有的“Fiber”数据结构。

prepareFreshStack 这个函数的作用就是创建一个全新的、空的 Stack,以供 React 在更新和渲染时使用。具体来说,这个函数会做以下几件事情:

  1. 创建一个空的 Stack 对象。
  2. 将这个 Stack 对象传递给 React 的调度器(Scheduler),以供调度器在更新和渲染过程中使用。
  3. 重置一些全局状态,以确保 React 在使用新的 Stack 时能够正常工作。
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
  const rootWorkInProgress = createWorkInProgress(root.current, null);
}

createWorkInProgress

创建一个 WIP 节点,以便在 WIP 树中描述这个组件的更新过程。然后,React 会基于 WIP 树中的节点执行更新,直到 WIP 树中所有节点的更新都已完成,最后再将更新结果同步到实际 DOM 树中,从而完成组件的更新过程。

function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
  // 如果不为空,根据current的属性创建
  workInProgress.type = current.type;
  return workInProgressv
}

return 出来以后会返回到 prepareFreshStack 再到 renderRootSync 然后执行 workLoopSync

performUnitOfWork

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
  let next;
  next = beginWork(current, unitOfWork, renderLanes);
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
  ReactCurrentOwner.current = null;
}

套到上面的代码里逻辑如下

  1. root节点进入beginWork
  2. function Component App()进入beginWork
  3. useState 节点进入beginWork
  4. div 节点进入beginWork
  5. header 节点进入beginWork
  6. img节点进入beginWork
  7. img没有子节点 进入 completeWork
  8. img 兄弟节点 p进入beginWork
  9. p 进入 completeWork

1. root节点进入beginWork

beginWork

return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );

attemptEarlyBailoutIfNoScheduledUpdate

可以看到直接将当前 workInProgress 的stateNode 赋值给了root

var root = workInProgress.stateNode;
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);

bailoutOnAlreadyFinishedWork

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
    cloneChildFibers(current, workInProgress);
    return workInProgress.child;
}

由此可知,如果fiber节点未改变,就会直接执行 cloneChildFibers 来创建子节点

2. app 进入beginWork

 // 没有挂起的更新或上下文。退出
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
  current,
  workInProgress,
  renderLanes,
);

进入一个swith判断,通过查询ReactWorkTags可知,App()对应的tag为FunctionComponent = 0;

function attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes) {
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

跟上面的逻辑一样,会进入cloneChild,根据current,生成WIP

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
    ) {
      // 如果 props 或 context 改变了, 将该fiber机诶安标记为已完成工作
      // 如果稍后确定props相等(备注),则可以取消设置。
      didReceiveUpdate = true;
    } else {
      //props和legacy context都不会改变。检查是否有挂起的
       //更新或上下文更改。
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
        current,
        renderLanes,
      );
      if (
        !hasScheduledUpdateOrContext &&
        //如果这是错误或悬疑边界的第二次通过
        //可能无法在“当前”上安排工作,因此我们检查此标志。
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
        // 没有挂起的更新或上下文。退出
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        // 这是一种仅适用于传统模式的特殊情况。
        didReceiveUpdate = true;
      } else {
       //已计划对此fiber进行更新,但没有新props
       //也不是legacy context。将此设置为false。如果更新队列或上下文
       //消费者产生一个改变的值,它会将其设置为true。否则
       //该组件将假设孩子没有改变并退出。
        didReceiveUpdate = false;
      }
    }
  }
 }

2. p 进入beginWork

 case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
 // 进入update逻辑

 // p节点的子节点只有个number,所以 isDirectTextChild = true
 if (isDirectTextChild) {
    nextChildren = null;
  } 
 // 所以 nextChildren = null, 开始执行completeWork的逻辑
updatePayload = prepareUpdate(instance, type, oldProps, newProps, currentHostContext)

export function prepareUpdate(
  domElement: Instance,
  type: string,
  oldProps: Props,
  newProps: Props,
  hostContext: HostContext,
): null | Array<mixed> {
  return diffProperties(domElement, type, oldProps, newProps);
}

3. diffProperties

在React中,当组件需要更新时,React使用一种称为“协调(reconciliation)”的算法来比较当前的虚拟DOM树和之前的虚拟DOM树的差异,并确定应该更新哪些部分的DOM树。其中一个重要的步骤是比较组件的属性(props)。

在比较属性时,React使用一个名为diffProperties的函数。diffProperties函数接收两个参数:之前的属性对象和当前的属性对象。函数返回一个对象,该对象包含应添加、删除和更新的属性。

当React在进行属性比较时,它首先比较新旧属性对象中的key,如果key不同,React会认为这是不同的属性,会将旧属性全部替换为新属性。

如果新旧属性对象中有相同的key,React会比较它们的值,如果值相同,React不需要进行任何操作,否则React将把新值设置为DOM属性的新值。

此外,diffProperties函数还处理了一些特殊情况,例如style属性和事件处理程序,以确保它们被正确地添加、删除和更新。

// Calculate the diff between the two objects.
export function diffProperties(
  domElement: Element,
  tag: string,
  lastRawProps: Object,
  nextRawProps: Object,
): null | Array<mixed> {
  if (__DEV__) {
    validatePropertiesInDevelopment(tag, nextRawProps);
  }

  let updatePayload: null | Array<any> = null;

  let lastProps: Object;
  let nextProps: Object;
  switch (tag) {
    case 'input':
      lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps);
      nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps);
      updatePayload = [];
      break;
    case 'select':
      lastProps = ReactDOMSelectGetHostProps(domElement, lastRawProps);
      nextProps = ReactDOMSelectGetHostProps(domElement, nextRawProps);
      updatePayload = [];
      break;
    case 'textarea':
      lastProps = ReactDOMTextareaGetHostProps(domElement, lastRawProps);
      nextProps = ReactDOMTextareaGetHostProps(domElement, nextRawProps);
      updatePayload = [];
      break;
    default:
      // p 标签进入的是这里
 
      lastProps = lastRawProps; // {}
      nextProps = nextRawProps;
      if (
        typeof lastProps.onClick !== 'function' &&
        typeof nextProps.onClick === 'function'
      ) {
        // TODO: This cast may not be sound for SVG, MathML or custom elements.
        trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
      }
      break;
  }

  assertValidProps(tag, nextProps);

  let propKey;
  let styleName;
  let styleUpdates = null;
  for (propKey in lastProps) {
    if (
      nextProps.hasOwnProperty(propKey) ||
      !lastProps.hasOwnProperty(propKey) ||
      lastProps[propKey] == null
    ) {
      continue;
    }
    if (propKey === STYLE) {
      const lastStyle = lastProps[propKey];
      for (styleName in lastStyle) {
        if (lastStyle.hasOwnProperty(styleName)) {
          if (!styleUpdates) {
            styleUpdates = ({}: {[string]: $FlowFixMe});
          }
          styleUpdates[styleName] = '';
        }
      }
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {
      // Noop. This is handled by the clear text mechanism.
    } else if (
      propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
      propKey === SUPPRESS_HYDRATION_WARNING
    ) {
      // Noop
    } else if (propKey === AUTOFOCUS) {
      // Noop. It doesn't work on updates anyway.
    } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      // This is a special case. If any listener updates we need to ensure
      // that the "current" fiber pointer gets updated so we need a commit
      // to update this element.
      if (!updatePayload) {
        updatePayload = [];
      }
    } else {
      // For all other deleted properties we add it to the queue. We use
      // the allowed property list in the commit phase instead.
      (updatePayload = updatePayload || []).push(propKey, null);
    }
  }
  for (propKey in nextProps) {
    const nextProp = nextProps[propKey];
    const lastProp = lastProps != null ? lastProps[propKey] : undefined;
    if (
      !nextProps.hasOwnProperty(propKey) ||
      nextProp === lastProp ||
      (nextProp == null && lastProp == null)
    ) {
      continue;
    }
    if (propKey === STYLE) {
      if (__DEV__) {
        if (nextProp) {
          // Freeze the next style object so that we can assume it won't be
          // mutated. We have already warned for this in the past.
          Object.freeze(nextProp);
        }
      }
      if (lastProp) {
        // Unset styles on `lastProp` but not on `nextProp`.
        for (styleName in lastProp) {
          if (
            lastProp.hasOwnProperty(styleName) &&
            (!nextProp || !nextProp.hasOwnProperty(styleName))
          ) {
            if (!styleUpdates) {
              styleUpdates = ({}: {[string]: string});
            }
            styleUpdates[styleName] = '';
          }
        }
        // Update styles that changed since `lastProp`.
        for (styleName in nextProp) {
          if (
            nextProp.hasOwnProperty(styleName) &&
            lastProp[styleName] !== nextProp[styleName]
          ) {
            if (!styleUpdates) {
              styleUpdates = ({}: {[string]: $FlowFixMe});
            }
            styleUpdates[styleName] = nextProp[styleName];
          }
        }
      } else {
        // Relies on `updateStylesByID` not mutating `styleUpdates`.
        if (!styleUpdates) {
          if (!updatePayload) {
            updatePayload = [];
          }
          updatePayload.push(propKey, styleUpdates);
        }
        styleUpdates = nextProp;
      }
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      const nextHtml = nextProp ? nextProp[HTML] : undefined;
      const lastHtml = lastProp ? lastProp[HTML] : undefined;
      if (nextHtml != null) {
        if (lastHtml !== nextHtml) {
          (updatePayload = updatePayload || []).push(propKey, nextHtml);
        }
      } else {
        // TODO: It might be too late to clear this if we have children
        // inserted already.
      }
    } else if (propKey === CHILDREN) {
      if (typeof nextProp === 'string' || typeof nextProp === 'number') {
        (updatePayload = updatePayload || []).push(propKey, '' + nextProp);
      }
    } else if (
      propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
      propKey === SUPPRESS_HYDRATION_WARNING
    ) {
      // Noop
    } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        // We eagerly listen to this even though we haven't committed yet.
        if (__DEV__ && typeof nextProp !== 'function') {
          warnForInvalidEventListener(propKey, nextProp);
        }
        if (propKey === 'onScroll') {
          listenToNonDelegatedEvent('scroll', domElement);
        }
      }
      if (!updatePayload && lastProp !== nextProp) {
        // This is a special case. If any listener updates we need to ensure
        // that the "current" props pointer gets updated so we need a commit
        // to update this element.
        updatePayload = [];
      }
    } else {
      // For any other property we always add it to the queue and then we
      // filter it out using the allowed property list during the commit.
      (updatePayload = updatePayload || []).push(propKey, nextProp);
    }
  }
  if (styleUpdates) {
    if (__DEV__) {
      validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE]);
    }
    (updatePayload = updatePayload || []).push(STYLE, styleUpdates);
  }
  // ['children', '4']
  return updatePayload;
}