react原理:completeWork阶段

897 阅读3分钟

react原理专栏

这篇文章讲一下render阶段的第二部分:completeUnitOfWork

reactrender阶段,会按照先序遍历的顺序构建fiber树,当workInProgress指针来到一个叶子节点时,就会执行completeUnitOfWork方法,这个方法会修改workInProgress指针,让其指向先序遍历中的下一个节点,下面,就来介绍一下completeUnitOfWork完成的工作。

completeUnitOfWork主要做了三件事:dom节点的创建和更新,effectList的收集和错误的处理。关于错误处理的内容,笔者也没有深入了解过,这篇文章就介绍一下前两部分内容:dom节点的创建和更新。

Dom节点的创建和更新

这部分的内容在completeWork函数中。completeWork会根据不同的组件类型进入不同的逻辑。由于completeWork主要会做dom相关的操作,因此我们可以主要看HostComponent部分的代码。

popHostContext(workInProgress);
var rootContainerInstance = getRootHostContainer();
var type = workInProgress.type;

if (current !== null && workInProgress.stateNode != null) {
  // 节点更新
  updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);

  if (current.ref !== workInProgress.ref) {
    markRef$1(workInProgress);
  }
} else {
  // 新增dom节点
  // ...
  var currentHostContext = getHostContext();

  var _wasHydrated = popHydrationState(workInProgress);

  if (_wasHydrated) {
    // 服务端渲染相关
  } else {
    var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
    appendAllChildren(instance, workInProgress, false, false);
    workInProgress.stateNode = instance;

    if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
      markUpdate(workInProgress);
    }
  }

  if (workInProgress.ref !== null) {
    markRef$1(workInProgress);
  }
}

return null;

可以看到,对于HostComponent的处理分为dom更新和dom创建两个过程。针对更新,主要是dom属性的更新,对于新增的dom节点,会先创建一个dom节点,之后将该节点挂载到fiberstateNode属性上,之后插入该节点(appendAllChildren)。由于在fiber架构中,组件树和dom并不是对应的,比如函数组件和类组件不存在对应的dom节点,因此appendAllChildren这部分的逻辑比较繁琐,这里就不展开讲解了,有兴趣的可以看一下@Axizs大佬的文章。

effectList收集

在完成dom相关的更新之后,回到completeUnitOfWork中,会开始effectList链的收集。effectList链的作用就是将有副作用的fiber节点收集为一条链表,这样在commit阶段中,就可以根据effectList来进行操作,不需要再遍历一次fiber树。下面看一下这部分代码

// returnFiber就是当前正在处理的workInProgress节点的父节点
// completedWork就是正在处理的fiber节点
if (returnFiber.firstEffect === null) {
  returnFiber.firstEffect = completedWork.firstEffect;
}

// 将completedWork身上的effectList拼接到returnFiber的后面
if (completedWork.lastEffect !== null) {
  if (returnFiber.lastEffect !== null) {
    returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
  }
  returnFiber.lastEffect = completedWork.lastEffect;
}

var flags = completedWork.flags;

// 将completedWork自己拼接到returnFiber的后面
if (flags > PerformedWork) {
  if (returnFiber.lastEffect !== null) {
    returnFiber.lastEffect.nextEffect = completedWork;
  } else {
    returnFiber.firstEffect = completedWork;
  }

  returnFiber.lastEffect = completedWork;
}

也就是说,effectList的顺序是从层次最深的子节点开始向上收集,层次最深的fiber节点位于effectList链表的头部。最终,workInProgress树的rootFiber节点会得到完整的effectList。不是很清楚的同学可以看一下b站的这个视频

注意点

关于completework部分有一些需要注意的地方:

completeWork中创建了新的dom节点,但是此时,这些新的dom节点并没有被插入到dom树中,比如看下面的例子

import { useState, useCallback } from 'react'

const App = () => {
  const [visible, change] = useState(false)
  const changeVisible = useCallback(() => change((pre) => !pre), [change])
  return (
    <div>
      {visible && (
        <div>
          <h1>新的div</h1>
        </div>
      )}
      <p>老的内容</p>
      <button onClick={changeVisible}>click</button>
    </div>
  )
}

export default App

点击按钮,visible变为true,此时,在completework中,h1被添加到了div中,但是div并没有被添加到dom树中。相反,这个div会被打上placementtag,从而被收集到effectList中,在之后的commit阶段中,这个div会被添加到真实的dom树中,从而实现页面的更新。

completeWork之后,render阶段结束,进入commit阶段,之后的文章会为大家讲解。