react 源码之render阶段

459 阅读6分钟

一、前言

react runtime分为三个阶段

  1. scheduler调度,调度首次渲染、更新等任务
  2. reconcile协调,创建fiber(fiber是什么fiber如何生成),进行diff算法
  3. render渲染,最终渲染到页面 此次文章主要讲的是react如何将fiber渲染到页面,以及渲染中都发生了什么。

二、effectList

1. effectList是什么

  • effectList正如这个单词表达的意思一样,它是记录此次更新中的副作用的,这里的副作用指的是在render阶段需要操作的dom,包含新增、删除、更新等。
  • effectList是一个环状链表结构的数据,其中每个effect的nextEffect都指向下一个effect,而这里的effect其实就是存在flags(flags是以32位二进制存储的,采用的是位运算)不等于NoFlags的fiber,就如我们在diff算法中所说的一致
  • 在reconcile结束后,workInProgress rootFiber(即rootFiber.alternate)的firstEffect指针指向effectList的第一个effect

2. effectList解决什么问题

我们知道,reactDom在render阶段是处理存在effect的fiber,如果是从workInProgress rootFiber开始遍历寻找存在effect的fiber势必会造成性能的损耗,所以在reconcile阶段会生成一个effectList,在render阶段只要遍历这个effectList,提升性能

3. effectList何时生成

diff算法中,有提到effectList是在completeWork时生成的,每当completeWork处理完一个fiber时,如果这个fiber存在effect,就会将effect加入effectList这个链表。

  // do while循环处理effect
  // 使每个returnFiber上的firstEffect都指向effectList的第一个effect
  // lastEffect指针指向effectList的最后一个effect
  // 每个effect的nextEffect都指向下一个effect
  // lastEffect的nextEffect指向firstEffect
  do {
    var current = completedWork.alternate;
    var returnFiber = completedWork.return; 

    if ((completedWork.flags & Incomplete) === NoFlags) {
      setCurrentFiber(completedWork);
      var next = void 0;
      // completeWork,返回next
      if ( (completedWork.mode & ProfileMode) === NoMode) {
        next = completeWork(current, completedWork, subtreeRenderLanes);
      } else {
        startProfilerTimer(completedWork);
        next = completeWork(current, completedWork, subtreeRenderLanes); 

        stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
      }

      resetCurrentFiber();
      // 如果存在next fiber,退出调用栈,进行beginWork
      if (next !== null) {
        workInProgress = next;
        return;
      }

      resetChildLanes(completedWork);
      // 处理effect
      if (returnFiber !== null && (returnFiber.flags & Incomplete) === NoFlags) {
      	// 如果已存在的effect
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = completedWork.firstEffect;
        }

        if (completedWork.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
          }

          returnFiber.lastEffect = completedWork.lastEffect;
        } 
        var flags = completedWork.flags; 
        // 处理此次completedWork的effect,如果有的话
        if (flags > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            returnFiber.firstEffect = completedWork;
          }

          returnFiber.lastEffect = completedWork;
        }
      }
    } else {
      // 此次completeWork存在异常
    }

    var siblingFiber = completedWork.sibling;

    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    completedWork = returnFiber; 
    workInProgress = completedWork;
  } while (completedWork !== null); 

如上代码所示,最终我们在render阶段就可以通过workInProgress rootFiber的firstEffect拿到effectList,然后开始渲染

三、commit三次循环

render开始于commitRootImpl函数

function commitRootImpl(root, renderPriorityLevel) {
  if (firstEffect !== null) {
	// ...
	// 第一次循环,操作dom之前
    do {
      {
        invokeGuardedCallback(null, commitBeforeMutationEffects, null);
        // ...
      }
    } while (nextEffect !== null); 

    // ...
    nextEffect = firstEffect;
	// 第二次循环,操作dom
    do {
      {
        invokeGuardedCallback(null, commitMutationEffects, null, root, renderPriorityLevel);
       // ...
      }
    } while (nextEffect !== null);
    // ...
    
    root.current = finishedWork;
    
    nextEffect = firstEffect;
	// 第三次循环,操作dom之后
    do {
      {
        invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
        // ...
      }
    } while (nextEffect !== null);
   // ...
  } else {
    // do something
  }
}

从上面代码可知,commitRootImpl经历了三次对effectList的do while循环,分别处理不同的逻辑,reactDom的渲染阶段也是在这三个循环中完成,三个循环分别对应的处理函数是commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects,接下来看看这三个处理函数都干了什么

1.commitBeforeMutationEffects

function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    var current = nextEffect.alternate;
    // 处理input focus blur相关
    if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
      
    }

    var flags = nextEffect.flags;
    // 主要调用classComponent类型组件的getSnapshotBeforeUpdate
    if ((flags & Snapshot) !== NoFlags) {
      setCurrentFiber(nextEffect);
      commitBeforeMutationLifeCycles(current, nextEffect);
      resetCurrentFiber();
    }
    // 异步调度useEffect产生的effect
    if ((flags & Passive) !== NoFlags) {
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        // scheduleCallBack是调度器schedule相关,可以异步调度一个函数
        scheduleCallback(NormalPriority$1, function () {
          // 处理useEffect产生的effect
          flushPassiveEffects();
          return null;
        });
      }
    }

    nextEffect = nextEffect.nextEffect;
  }
}

从代码可知,在render的第一次循环中,主要做了三件事

  • 如果是input,处理input相关的focus,blur相关事件
  • 如果是classComponent类型组件,也就是通过class方式创建的组件,会调用getSnapshotBeforeUpdate这个生命周期函数,也就是在dom未发生改变之前执行
  • 如果存在useEffect,异步调度effect的处理函数flushPassiveEffects,reactDom render阶段是同步执行的,所以从而得知useEffect的回调函数是在组件渲染完成后执行的

2.commitMutationEffects

function commitMutationEffects(root, renderPriorityLevel) {
  while (nextEffect !== null) {
    setCurrentFiber(nextEffect);
    var flags = nextEffect.flags;
    // 清空子节点是文本节点内容,如textarea、option、noscript、dangerouslySetInnerHtml等
    if (flags & ContentReset) {
      commitResetTextContent(nextEffect);
    }
	// 如果存在ref,清空原来的ref
    if (flags & Ref) {
      var current = nextEffect.alternate;

      if (current !== null) {
        commitDetachRef(current);
      }
    }

    var primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
    // 根据effect flags,调用不同的处理逻辑
    switch (primaryFlags) {
      // 插入
      case Placement:
        {
          commitPlacement(nextEffect); 
          nextEffect.flags &= ~Placement;
          break;
        }
      // 插入更新
      case PlacementAndUpdate:
        {
          commitPlacement(nextEffect);
          nextEffect.flags &= ~Placement;
          var _current = nextEffect.alternate;
          commitWork(_current, nextEffect);
          break;
        }
      // 服务端渲染相关
      case Hydrating:
        {
          nextEffect.flags &= ~Hydrating;
          break;
        }
      // 服务端渲染相关
      case HydratingAndUpdate:
        {
          nextEffect.flags &= ~Hydrating; 

          var _current2 = nextEffect.alternate;
          commitWork(_current2, nextEffect);
          break;
        }
      // 更新
      case Update:
        {
          var _current3 = nextEffect.alternate;
          commitWork(_current3, nextEffect);
          break;
        }
      // 删除
      case Deletion:
        {
          commitDeletion(root, nextEffect);
          break;
        }
    }

    resetCurrentFiber();
    nextEffect = nextEffect.nextEffect;
  }
}

第二次循环,commitBeforeMutationEffects也做了四件事

  • 文本类型节点的清空操作
  • ref的清除
  • 函数组件执行useLayoutEffect销毁函数,如果有的话
  • 根据effect的flags,进行相应的dom操作,这里的相应的处理函数不做详细介绍,逻辑也比较简单,大家可以自行去看一下,commitPlacement最终调用的是appendChild或者insertBefore,commitWork最终根据effect的updateQueue更新dom属性,commitDeletion最终调用removeChild 至此,render阶段已经发生了dom的变化,包含更新、新增、删除

3.commitLayoutEffects

在第三次循环之前,有一行代码

root.current = finishedWork;

这也就验证了在react fiber概念及原理所说的,reactDom存在两颗fiber树,在渲染结束后,会将rootFiberNode的current指针指向workInProgress fiber,而current fiber将作为下一次的更新所用

function commitLayoutEffects(root, committedLanes) {

  {
    markLayoutEffectsStarted(committedLanes);
  } 
  while (nextEffect !== null) {
    setCurrentFiber(nextEffect);
    var flags = nextEffect.flags;

    if (flags & (Update | Callback)) {
      var current = nextEffect.alternate;
      // 处理useLayoutEffect
      // 将useEffect回调函数及销毁函数推入队列,等待异步调用
      commitLifeCycles(root, current, nextEffect);
    }
    // ref赋值的相关操作
    {
      if (flags & Ref) {
        commitAttachRef(nextEffect);
      }
    }

    resetCurrentFiber();
    nextEffect = nextEffect.nextEffect;
  }

  {
    markLayoutEffectsStopped();
  }
}

进入最后一次循环,主要是做了两件事

  • 调用commitLifeCycles
  • ref赋值的相关操作 这里我们重点看一下commitLifeCycles这个函数
function commitLifeCycles(finishedRoot, current, finishedWork, committedLanes) {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
      {
        {
          commitHookEffectListMount(Layout | HasEffect, finishedWork);
        }
        schedulePassiveEffects(finishedWork);
        return;
      }

    case ClassComponent:
      {
        var instance = finishedWork.stateNode;

        if (finishedWork.flags & Update) {
          if (current === null) {
            {
             // ...
            {
              instance.componentDidMount();
            }
          } else {
            // ...
            {
              instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);
            }
          }
        } 
        // ...
        return;
      }

    case HostRoot:
      {
          // ...
          commitUpdateQueue(finishedWork, _updateQueue, _instance);
        }

        return;
      }

    case HostComponent:
      {
       // ...
      }
    case HostText:
      {
        // ...
      }
    case HostPortal:
      {
        // ...
      }
    case Profiler:
      {
        // ...
      }
    case SuspenseComponent:
      {
        // ...
      }
    case SuspenseListComponent:
    case IncompleteClassComponent:
    case FundamentalComponent:
    case ScopeComponent:
    case OffscreenComponent:
    case LegacyHiddenComponent:
      return;
  }
  // ...
}

commitLifeCycles根据不同类型的组件调用相应的处理逻辑,这里主要分析一下FunctionComponent,ClassComponent以及HostComponent

  • FunctionComponent 主要是做了两件事
    第一件事是调用commitHookEffectListMount,处理useLayoutEffect,可以发现,useLayoutEffect是同步调用的,也就如官方文档所描述的一样,具体逻辑大家可以详细看一下。
    第二件事是将所有的useEffect和上次更新的useEffect销毁函数(也就是useEffect的两个参数)分别推入队列pendingPassiveHookEffectsMount和pendingPassiveHookEffectsUnmount,这两个队列的第i项是对应的effect,第i+1项是对应的fiber。而这两个队列的作用就是提供给第一次循环中的第三件事使用,即flushPassiveEffects,它会循环遍历这两个队列,执行其中的副作用。
  • ClassComponent 如果是ClassComponent类型组件,如果是第一次渲染,会调用componentDidMount生命周期,如果是更新,则调用componentDidUpdate生命周期,最后如果存在setState第二个参数,则执行它
  • HostComponent HostComponent即ReactDom.render(或其他模式模式下render,如concurrent模式下createRoot模式创建的应用)组件,在此次循环会执行该组件的回调函数,对于ReactDom.render来说,就是对应的第三个参数

四、render最后

在render最后,reactDom会再次调度一次更新

ensureRootIsScheduled(root, now());

至此,react reactDom的reconcile、render两个阶段有了一个完整解读。当然,几篇文章不足以描述react源码,其中还有许多细节需要去多多debugger。