快来看 React 渲染过程(下)

引言

书接上回,上一篇文章主要是介绍了 React 初次渲染过程中在内存中生成 workInProgress树的过程。主要是经历了两个阶段,一个是 beginWork,另一个是 completeWork。那React 是如何将已经生成的 WorkInProgress 树渲染到宿主环境中的呢?接下来说一说 commit 阶段,也就是将 workInProgress 树渲染到页面上的过程。

原理

demo

因为 commit 阶段涉及到副作用的执行,比如 useLayoutEffect 等。 所以我们改造一下调试源码用的组件代码。

jsx 复制代码
import React, { useEffect, useState } from "react"
const Index = ()=>{
    debugger;
    const [state,setState] = useState(0)
    useEffect(()=>{
        console.log(123)
    },[])
    console.log('渲染吗??')
    const [demo,setDemo] = useState('demo')

    return <div>
        {state}
        <button onClick={()=>{
            // debugger
            // setState(1)
            // setState(10)
            // setState(100)
            setState(0)
        }}>触发渲染</button>    
    </div>
}
export default Index

我们的组件大概是这个样子的。下面我们来看一下 React 整个过程是怎么样的。

过程

首先看一下当走完beginWork 和completeWork 之后,生成的树是怎么样的。

可以看到,最后是生成了一个这样的树,我们的 wip 树保存在finishedWork 中,可以打印出来看一下

进入 commit 阶段的入口函数是 commitRoot

js 复制代码
function commitRoot(root) {

  // TODO: This no longer makes any sense. We already wrap the mutation and
  // layout phases. Should be able to remove.
  var previousUpdateLanePriority = getCurrentUpdatePriority();
  var prevTransition = ReactCurrentBatchConfig$3.transition;

  try {
    ReactCurrentBatchConfig$3.transition = 0;
    setCurrentUpdatePriority(DiscreteEventPriority);
    commitRootImpl(root, previousUpdateLanePriority);
  } finally {
    ReactCurrentBatchConfig$3.transition = prevTransition;
    setCurrentUpdatePriority(previousUpdateLanePriority);
  }

  return null;
}

下面从这个函数出发具体来看看 React 做了什么

commitRoot

commitRoot 主要的逻辑就是调用了一个commitRootImpl,同时传入了 root 树和previousUpdateLanePriority。

commitRootImpl

commitRootImpl 的作用就大了许多,我们分别来看commitRootImpl 做了哪些事情。

js 复制代码
function commitRootImpl(root, renderPriorityLevel) {
  do {
    // 清空副作用列表
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

  flushRenderPhaseStrictModeWarningsInDEV();

  
    //  判断当前 fiber 节点的子孙节点是否有副作用
  var subtreeHasEffects = (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;
  //  判断当前节点是否有副作用
  var rootHasEffect = (finishedWork.flags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;

  if (subtreeHasEffects || rootHasEffect) {
   

    var shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(root, finishedWork);


    commitMutationEffects(root, finishedWork, lanes);

   

    commitLayoutEffects(finishedWork, root, lanes);

    {
      markLayoutEffectsStopped();
    }
    // opportunity to paint.


    requestPaint();
  
  return null;
}

从源码可以看到,commitRootImpl 主要做了三件事情

  1. 通过一个while 循环去调用flushPassiveEffects去清除副作用,防止因为有没有运行的副作用影响 commit过程。
  2. 判断子孙节点以及自己是否有副作用
    • 如果有副作用,依次执行commitBeforeMutationEffects,commitMutationEffects,commitLayoutEffects,requestPaint
    • 如果没有副作用,直接切树
  3. 其他一些收尾工作。

下面我们依次进行说明

判断副作用

在completeWork 阶段,React 将子组件的 flags 都冒泡到了父组件的subtreeFlags 中,所以可以通过subtreeFlags来检查子组件是否有副作用已经自己本身是否有副作用。

js 复制代码
  var subtreeHasEffects = (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;
  var rootHasEffect = (finishedWork.flags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;

如果有副作用的话,就依次运行commitBeforeMutationEffects ,commitMutationEffects,commitLayoutEffects,requestPaint。下面分别进行说明。 如果友友们对位运算不太熟悉,可以自行百度了解一下~

commitBeforeMutationEffects

js 复制代码
function commitBeforeMutationEffects(root, firstChild) {
  focusedInstanceHandle = prepareForCommit(root.containerInfo);
  nextEffect = firstChild;
  commitBeforeMutationEffects_begin(); // We no longer need to track the active instance fiber

  var shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;
  return shouldFire;
}

主要流程是commitBeforeMutationEffects_begin处理的,我们直接进入commitBeforeMutationEffects_begin查看 React 做了什么。

commitBeforeMutationEffects_begin
js 复制代码
function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    var fiber = nextEffect; // This phase is only used for beforeActiveInstanceBlur.

    var child = fiber.child;

    if ((fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && child !== null) {
      ensureCorrectReturnPointer(child, fiber);
      nextEffect = child;
    } else {
      commitBeforeMutationEffects_complete();
    }
  }
}

主要逻辑在commitBeforeMutationEffects_complete中

commitBeforeMutationEffects_complete
js 复制代码
function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    var fiber = nextEffect;
    setCurrentFiber(fiber);

    try {
     
      commitBeforeMutationEffectsOnFiber(fiber);
    } catch (error) {
      reportUncaughtErrorInDEV(error);
      captureCommitPhaseError(fiber, fiber.return, error);
    }

    resetCurrentFiber();
    var sibling = fiber.sibling;

    if (sibling !== null) {
      ensureCorrectReturnPointer(sibling, fiber.return);
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }
}

这里解释一下,这个循环的作用主要是运行了commitBeforeMutationEffectsOnFiber,同时如果当前 fiber 节点有sibling节点的话,也会运行commitBeforeMutationEffects_complete。整个逻辑是一个从下到上的过程。

commitBeforeMutationEffectsOnFiber
js 复制代码
function commitBeforeMutationEffectsOnFiber(finishedWork) {
  var current = finishedWork.alternate;
  var flags = finishedWork.flags;

  if ((flags & Snapshot) !== NoFlags) {
    setCurrentFiber(finishedWork);

    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent:
        {
          break;
        }

      case ClassComponent:
        {
          if (current !== null) {
            var prevProps = current.memoizedProps;
            var prevState = current.memoizedState;
            var instance = finishedWork.stateNode; // We could update instance props and state here,
            // but instead we rely on them being set during last render.
            // TODO: revisit this when we implement resuming.

            {
              if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {
                if (instance.props !== finishedWork.memoizedProps) {
                  error('Expected %s props to match memoized props before ' + 'getSnapshotBeforeUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                }

                if (instance.state !== finishedWork.memoizedState) {
                  error('Expected %s state to match memoized state before ' + 'getSnapshotBeforeUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.state`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                }
              }
            }

            var snapshot = instance.getSnapshotBeforeUpdate(finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState);

            {
              var didWarnSet = didWarnAboutUndefinedSnapshotBeforeUpdate;

              if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) {
                didWarnSet.add(finishedWork.type);

                error('%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + 'must be returned. You have returned undefined.', getComponentNameFromFiber(finishedWork));
              }
            }

            instance.__reactInternalSnapshotBeforeUpdate = snapshot;
          }

          break;
        }

      case HostRoot:
        {
          {
            var root = finishedWork.stateNode;
            clearContainer(root.containerInfo);
          }

          break;
        }

      case HostComponent:
      case HostText:
      case HostPortal:
      case IncompleteClassComponent:
        // Nothing to do for these component types
        break;

      default:
        {
          throw new Error('This unit of work tag should not have side-effects. This error is ' + 'likely caused by a bug in React. Please file an issue.');
        }
    }

    resetCurrentFiber();
  }
}

这里判断当前 fiber 是否有Snapshot副作用,如果有副作用的话,根据 tag 进入不同的 case,主要有作用的 case 就两个,其余都是凑数的,没什么实际用途。

js 复制代码
      case ClassComponent:
        {
          if (current !== null) {
            var prevProps = current.memoizedProps;
            var prevState = current.memoizedState;
            var instance = finishedWork.stateNode; // We could update instance props and state here,
            // but instead we rely on them being set during last render.
            // TODO: revisit this when we implement resuming.

            {
              if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {
                if (instance.props !== finishedWork.memoizedProps) {
                  error('Expected %s props to match memoized props before ' + 'getSnapshotBeforeUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                }

                if (instance.state !== finishedWork.memoizedState) {
                  error('Expected %s state to match memoized state before ' + 'getSnapshotBeforeUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.state`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                }
              }
            }

            var snapshot = instance.getSnapshotBeforeUpdate(finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState);

            {
              var didWarnSet = didWarnAboutUndefinedSnapshotBeforeUpdate;

              if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) {
                didWarnSet.add(finishedWork.type);

                error('%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + 'must be returned. You have returned undefined.', getComponentNameFromFiber(finishedWork));
              }
            }

            instance.__reactInternalSnapshotBeforeUpdate = snapshot;
          }

          break;
        }

      case HostRoot:
        {
          {
            var root = finishedWork.stateNode;
            clearContainer(root.containerInfo);
          }

          break;
        }

如果是一个类组件的话,会执行getSnapshotBeforeUpdate这个函数来做一些事情。因为对类组件了解不够深,这里跳过说明。 如果是HostRoot,会清除节点内容。 具体可看下面代码

js 复制代码
function clearContainer(container) {
  if (container.nodeType === ELEMENT_NODE) {
    container.textContent = '';
  } else if (container.nodeType === DOCUMENT_NODE) {
    var body = container.body;

    if (body != null) {
      body.textContent = '';
    }
  }
} 

这里是为了防止本身 html 节点中有内容,影响我们 React 的整个渲染。 下面我们用图来看一下整个执行过程

commitMutationEffects

和上面的流程差不多,看代码

js 复制代码
function commitMutationEffects(root, firstChild, committedLanes) {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  nextEffect = firstChild;
  commitMutationEffects_begin(root);
  inProgressLanes = null;
  inProgressRoot = null;
}

依旧是commitMutationEffects_begin来进行一些逻辑的处理。

commitMutationEffects_begin
js 复制代码
function commitMutationEffects_begin(root) {
  while (nextEffect !== null) {
    var fiber = nextEffect; // TODO: Should wrap this in flags check, too, as optimization

    var deletions = fiber.deletions;

    if (deletions !== null) {
      for (var i = 0; i < deletions.length; i++) {
        var childToDelete = deletions[i];

        try {
          commitDeletion(root, childToDelete, fiber);
        } catch (error) {
          reportUncaughtErrorInDEV(error);
          captureCommitPhaseError(childToDelete, fiber, error);
        }
      }
    }

    var child = fiber.child;

    if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) {
      ensureCorrectReturnPointer(child, fiber);
      nextEffect = child;
    } else {
      commitMutationEffects_complete(root);
    }
  }
}

该函数主要做了两件事:

  1. 如果存在deletions ,先处理这部分的逻辑。
  2. 然后进入commitMutationEffects_complete。
相关推荐
Small-K2 分钟前
前端框架中@路径别名原理和配置
前端·webpack·typescript·前端框架·vite
bin91536 分钟前
【EXCEL数据处理】000009 案列 EXCEL单元格数字格式。文本型数字格式和常规型数字格式的区别
大数据·前端·数据库·信息可视化·数据分析·excel·数据可视化
山语山25 分钟前
C语言——文件读写操作
java·c语言·前端·microsoft·visual studio
太阳火神的美丽人生41 分钟前
Vant WeApp 开启 NPM 遇到的问题总结
前端·npm·node.js
哈哈哈哈cwl1 小时前
一篇打通浏览器储存
前端·面试·浏览器
凌云行者2 小时前
使用rust写一个Web服务器——async-std版本
服务器·前端·rust
等什么君!2 小时前
JavaScript数据类型
开发语言·前端·javascript
DK七七2 小时前
【PHP陪玩系统源码】游戏陪玩系统app,陪玩小程序优势
前端·vue.js·游戏·小程序·php·uniapp
yqcoder2 小时前
css 选择除第一个子元素之外的所有子元素
前端·css
blaizeer2 小时前
深入浅出 CSS 定位:全面解析与实战指南
前端·css