react初次渲染源码解析

473 阅读8分钟

观察[[react]]应用初次调和的过程,整理关键路径,关键函数。

<!DOCTYPE html>
<html lang="en">
<head>
  <title>React App</title>
</head>
<body>
  <div id="container"></div>
</body>
</html>

import ReactDom from 'react-dom/client'

function FnComp(props) {
  return <p>{props.children}</p>
}
class ClazzComp extends React.Component {
  render() {
      return <p style={{ color: 'red' }}>{this.props.children}</p>
  }
}
ReactDOM.createRoot(document.getElementById('container')).render(
  <div id="root">
      <FnComp>hello</FnComp>
      <ClazzComp>react</ClazzComp>
  </div>
);

截屏2022-05-11 15.27.50.png 从性能面板上看,performConcurrentWorkOnRoot完成渲染工作,内部又调用renderRootSyncfinishConcurrentRender,所以react分2个阶段完成一次渲染:render阶段和commit阶段。

初始化工作

初始化工作包括新建fiberRoot对象,准备好workInProgress对象,为render阶段做准备。

// 首先运行`ReactDom.createRoot`,然后运行返回对象上的`render`方法。
// container就是渲染的dom节点,例子中是div#container
// 目的如函数名:创建fiberRoot对象和hostRootFiber对象
function createRoot(container) {
  // createContainer最终调用到createFiberRoot
  const root = createContainer(container);
  // 返回ReactDomRoot实例
  return new ReactDOMRoot(root);
}
function createFiberRoot(containerInfo) {
  // 新建fiberRoot实例
  const root: FiberRoot = new FiberRootNode(containerInfo);
  // 新建一个fiber,类型是HostRoot,表示宿主环境
  // fiber是react新调和方法的工作单元,不同的fiber类型有不同的调和方法
  // 例子中类组件、函数组件、浏览器标签都有对应的fiber类型
  const uninitializedFiber = createHostRootFiber();
  // firberRoot和hostRootFiber互相关联
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;
  return root;
}
// ReactDOMRoot的原型链有render方法,所以才能调用render方法
function ReactDOMRoot(internalRoot: FiberRoot) {
  this._internalRoot = internalRoot;
}
ReactDOMRoot.prototype.render = function(children: ReactNodeList) {
  const root = this._internalRoot;
  // updateContainer会请求scheduler模块回调performConcurrentWorkOnFiber
  // scheduler模块涉及到何时回调performConcurrentWorkOnFiber,本文暂不涉及
  // 本文主要讲解performConcurrentWorkOnFiber函数在初始化渲染过程中关键实现逻辑
  updateContainer(children, root);
}
目前可以得出fiberRootNodeuninitializedFiber对象(即:hostRootFiber对象)之间的引用关系:
          fiberRootNode -- containerInfo --> div#container
           |         ^
           |         |
        current  stateNode             
           |         |
           V         |
          hostRootFiber
// 上面讲到performConcurrentWorkOnRoot,初次渲染运行renderRootSync函数
// renderRootSync运行workLoopSync函数
function workLoopSync(root: FiberRoot) {
  // workInProgressRoot是一个全局变量,标记当前构建哪个fiberRoot
  // 初始化null,显然不等于root,那么就是准备一颗新的fiber树
  // react借鉴了git中的说法,将构建中的树成为workInProgress树
  // 等到workInProgress树构建完成,再一次性修改到页面上,这个过程也叫commit
  // 这也是为什么react的渲染过程分别叫做:render阶段和commit阶段
  // render阶段就是构建workInProgress树
  // commit阶段就是提交workInProgress树的所有修改
  if (workInProgressRoot !== root) {
    // 准备workInProgreess树
    prepareFreshStack(root);
  }

  // workInProgress是一个全局变量,表示当前的fiber节点。
  while(workInProgress !== null) {
    // 只要有workInProgress,则不停调用performUnitOfWork
    performUnitOfWork(workInProgress);
  }
}
function prepareFreshStack(root) {
  workInProgressRoot = root;
  // 
  const rootWorkInProgress = createWorkInProgress(root.current);
  // 这里将workInProgress设置成HostRootFiber,后续在workLoopSync中才能使用
  workInProgress = rootWorkInProgress;
}
function createWorkInProgress(current: Fiber) {
  // 创建一个HostRootFiber对象
  // 所以准备workInProgress树其实就是新建hostRootFiber对象
  const workInProgress = createFiber(current.tag, current.key);
  // 使用alternate互相引用
  workInProgress.alternate = current;
  current.alternate = workInProgress;
  // ...省略一些属性

  return workInProgress;
}
目前可以得出fiberRoot、current、workInProgress的引用关系:
                                      FiberRootNode
                                       |         ^
                                       |         |
   workInProgress                   current  stateNode             
         |                             |         |
         V                             V         |
   hostRootFiber   <-- alternate -->  hostRootFiber

render阶段

render阶段主要是在内容中构建出完整的workInProgress树,为commit阶段做准备

beginWork performUnitOfWork

function performUnitOfWork(unitOfWork: Fiber) {
  const current = unitOfWork.alternate;
  let next = beginWork(current, unitOfWork);
  if (next !== null) {
    // 目前的重点在这:只要next不为空,就一直执行performUnitOfWork
    workInProgress = next;
  } else {
    // 暂时忽略
  }
}
// beginWork是一个巨大的switch...case判断
// 不同的fiber类型,执行对应的updateXXXComponent的函数。
function beginWork(current: Fiber, workInProgress: Fiber) {
  switch(workInProgress.tag) {
    case ClassComponent: {
      // 类组件
      const Component = workInProgress.type;
      const nextProps = workInProgress.pendingProps;
      return updateClassComponent(
        current, workInProgress,
        Component, nextProps
      )
    }
    case FunctionComponent:
      // 函数组件
      const Component = workInProgress.type;
      const nextProps = workInProgress.pendingProps;
      return updateFunctionComponent(
        current, workInProgress,
        Component, nextProps
      )
    case HostComponent:
      // 浏览器内置标签
      return updateHostComponent(/**/)
    // ...
  }
}
// 针对类组件:实例化组件,通过render函数得到子虚拟dom,调和,返回workInProgress.child
function updateClassComponent(
  current: Fiber,
  workInProgress: Fiber,
  Component: any,
  nextProps: any
) {
  // 实例化
  constructClassInstance(workInProgress, Component, nextProps);
  mountClassInstance(workInProgress, Component, nextProps);
  // 取finishClassComponent的返回值作为下一个工作单元
  const nextUnitOfWork = finishClassComponent(current, workInProgress, Component)
  return nextUnitOfWork;
}
function constructClassInstance(workInProgress, ctor, props) {
  const instance = new ctor(props);
  workInProgress.stateNode = instance;
}
function mountClassInstance(workInProgress, ctor, newProps) {}
function finishClassComponent(current, workInProgress, Component) {
  const instance = workInProgress.stateNode;
  // 运行render方法,得到子虚拟dom,例子中就是h1对应的虚你dom
  let nextChildren = instance.render();
  // 调和
  reconcileChildren(current, workInProgress, nextChildren);
  // 返回workInProgress.child属性
  return workInProgress.child;
}

// 针对函数组件:运行函数得到子虚拟dom,调和,返回workInProgress.child
function updateFunctionComponent(current, workInProgress, Component, nextProps) {
  // 用新的props生成新的虚拟dom
  let nextChildren = renderWithHooks(current, workInProgress, Component, nextProps);
  // 调和
  reconcileChildren(current, workInProgress, nextChildren);
  // 返回child
  return workInProgress.child;
}
function renderWithHooks(current, workInProgress, Component, props) {
  // 运行函数组件函数,得到虚拟dom
  // 暂时可以忽略其他代码,作用是提高hooks相关api,以后再讲
  let children = Component(props);
  return children;
}

// 针对浏览器标签:直接拿到children,调和,返回workInProgress.child
function updateHostComponent(current, workInProgress) {
  const nextProps = workInProgress.pendingProps;
  // newChildren其实就是新的子虚你dom节点。
  const nextChildren = nextProps.children;
  // 调和
  reconcileChildren(current, workInProgress, nextChildren)
  return workInProgress.child;
}
// 可见updateXXXComponent的目的都是一样的:拿到新的虚拟dom,调和,最后返回workInProgress.child
// 所以reconcileChildren应该用新的虚拟dom做了一些事,同时将workInProgress.child赋值
// 调和子节点就是用最新的虚拟dom去更新旧fiber对象的过程
// 因为初次渲染过程是没有旧的渲染对象的,所以都是新建fiber对象
function reconcileChildren(current: Fiber, workInProgress: Fiber, nextChildren: any) {
  if (current === null) {
    // mountChildFiber的返回值会赋值给workInprogress.child
    // mountChildFiber本身是工厂函数创建的,真正实现是reconcileChildFibers
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren)
  }
}
function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
  if (typeof newChild === 'object' && newChild !== null) {
    // 子虚拟dom是单个元素的情况
    switch(newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        // 先看reconcileSingleElement,暂时忽略placeSingleChild
        return placeSingleChild(
          reconcileSingleElement(returnFibler, currentFirstChild, newChild)
        );
    }
    // 子虚你dom是一个数组的情况
    if (isArray(newChild)) {
      return reconcileChildArray(returnFiber, currentFirstChild, newChild);
    }
  }

}
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
  // 创建一个fiber对象
  const created = createFiberFromElement(element, returnFiber.mode);
  // return属性指向returnFiber,即:workInProgress
  created.return = returnFiber;
  return created;
}
function reconcileChildArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildren: Array<*>
): Fiber | null {
  let oldFiber = currentFirstChild;
  let newIdx = 0;
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    // 省略,因为初始化渲染时,oldFiber是null
  }
  if (oldFiber === null) {
    // 保存第一个fiber对象
    let resultingFirstChild;
    // 前一个fiber对象,用于设置sibling
    let previousNewFiber = null;

    for (; newIdx < newChildren.length; newIdx++) {
      // for循序,为每个虚拟dom新建对应的fiber对象
      const newFiber = createFiber(returnFiber, newChildren[newIdx]);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        // 使用sibling链接下一个fiber节点
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    // 返回第一个fiber对象
    return resultingFirstChild;
  }
  // 省略其他代码
}
// 不管是单个虚拟dom,还是虚拟dom数组,reconcileChildren都会创建对应的fiber节点
// fiber节点之间由return child sibling互相引用。
目前可以得出fiberRoot、current、workInProgress的引用关系:
                                       FiberRootNode
                                         |       ^
                                         |       |
   workInProgress                     current stateNode             
         |                               |       |
         V                               V       |
   hostRootFiber   <-- alternate -->   hostRootFiber
    |         ^
    |         |
  child     return
    |         |
    V         |
HostComponentFiber(div)<------------------------------+
    |         ^                                       ^
    |         |                                       |
  child     return                                  return
    |         |                                       |
    V         |                                       |
FunctionComponentFiber(FnComp) -- sibling -> ClassComponentFiber(ClazzComp)
    |         ^                                   |       ^
    |         |                                   |       |
  child     return                              child   return
    |         |                                   |.      |
    V         |                                   V       |
HostComponentFiber(p)                         HostComponentFiber(p)

completeWork completeUnitOfWork

// 当`beginWork`返回null时,会转而运行`completeUnitOfWork`
function performUnitOfWork(unitOfWork: Fiber) {
  const current = unitOfWork.alternate;
  let next = beginWork(current, unitOfWork);
  if (next !== null) {
    // 之前的逻辑
  } else {
    // 如果没有child,则调用completeUnitOfWork
    completeUnitOfWork(workInProgress)
  }
}
/*
控制执行completeWork的流程:
当sibling有值时返回执行beginWork
否则对父fiber节点执行completeWork
*/
function completeUnitOfWork(unitOfWork: Fiber) {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    let next = completeWork(current, completedWork);
    if (next !== null) {
      // completeWork大多数都会返回null,所以可以忽略这里的逻辑
      workInProgress = next;
      return;
    }
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // 如果有兄弟fiber节点,则函数返回,对兄弟节点调用beginWork
      workInProgress = siblingFiber;
      return
    }
    // 对父节点进行completeWork
    completedWork = returnFiber;
  } while (completedWork !== null)
}
function completeWork(current: Fiber, workInProgress: Fiber) {
  const newProps = workInProgress.pendingProps;
  // 也是switch...case,同beginWork一样
  switch(workInProgress.tag) {
    case HostComponent: {
      // 创建h1标签(或h2 p标签)
      const instance = createInstance(
        workInProgress.type,
        newProps,
        workInProgress,
      )
      // 如果子fiber也是浏览器组件,那么就挂载到instance下
      // 例如将h2和p标签设置为h1的子元素
      appendAllChildren(instance, workInProgress);
      // 使用stateNode关联
      workInProgress.stateNode = instance;
      // 设置css属性等等,如果是文字类型的子组件,也一同设置
      finilizeInitialChildren(instance, type, newProps);
      // 小结:如果是浏览器标签,就会创建对应的标签,设置对应的属性,并将append子标签
      return null;
    }
    case FunctionComponent:
    case ClassComponent:
      // 没有特别的处理
      return null
    // 省略其他类型
  }
}
// 各个fiber节点执行beginWork completeWork的顺序
   函数              fiber.type             对应的组件或标签
1  beginWork         HostRoot                  -
2  beginWork         HostComponent             div#root
3  beginWork         FunctionComponent         FnComp
4  beginWork         HostComponent             p
5  completeWork      HostComponent             p
6  completeWork      FunctionComponent         FnComp
7  beginWork         ClassComponent            ClazzComp
8  beginWork         HostComponent             p
9  completeWork      HostComponent             p
10 completeWork      ClassComponent            ClazzComp
11 completeWork      ClassComponent            div#root
12 completeWork      HostRoot                  -

小结

  • 深度优先的过程
  • 从根节点到叶子节点执行beginWork
  • 没有叶子节点或所有叶子节点执行完completeWork,则父节点执行completeWork

commit阶段

// render阶段结束后,会进入commit阶段
// finishConcurrentRender调用commitRoot,最终调用commitRootImpl
function commitRootImpl(root: FiberRoot) {
  // finishedWork就是render阶段构建的workInProgress树
  const finishedWork = root.finishedWork;
  // 非初次渲染会执行getSnapshotBeforeUpdate,初次渲染可以忽略
  commitBeforeMutationEffects(root, finishedWork);
  // 提交修改,更新到页面,初次渲染仅仅是将workInProgress挂载到div#container上
  commitMutationEffects(root, finishedWork);
  // 将workInProgress树赋值到current
  root.current = finishedWork;
  // commitMutationEffects之后,所有的dom操作都已经完成,就可以访问dom
  commitLayouteEffects(finishedWork, root);
}

commitEffects

// commitMutationEffects调用commitMutationEffectsOnFiber
function commitMutationEffectsOnFiber(finishedWork: Fiber) {
  switch(finishedWork.tag) {
    case HostComponent: {
   	  // 提交修改操作,重新渲染将在这里设置,初次渲染这里没有进入,可以忽略
      recursivelyTraverseMutationEffects(root, finishedWork);
      // 首次渲染将finishedWork插入
      // todo: 那为什么不放在上面的函数里一起做呢?非要拆成2个函数
      commitReconciliationEffects(finishedWork);
    }
  }
}
function commitReconciliationEffects(finishedWork: Fiber) {
  const flags = finishedWork.flags;
  // 这里的Placement标签就是上面reconcileChildFibers中的placeSingleChild中设置的
  // 如果根节点是类组件、函数组件也都是有地方设置的
  // finishedWork就是workInProgress的根,例子中是div#root对应的dom元素
  // 初始化渲染,只需要将div#root对应的dom元素挂载到div#container即可
  // 因为div#root所有的子节点在render阶段就已经append到根节点上了
  if (flags & Placement) {
    // 将根节点插入到容器中
    commitPlacement(finishedWork);
    finishedWork.flags &= ~Placement;
  }
}
function commitPlacement(finishedWork: Fiber) {
  const parentFiber = getHostParentFiber(finishedWork);
  switch(parentFiber.tag) {
    case HostRoot: {
      // 这个就是div#container
      const parent = parentFiber.stateNode.containerInfo;
      const before = getHostSibling(finishedWork);
      // 将finishedWork.stateNode插入到parent下面,即首次渲染到页面上
      insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
    }
  }
}
function insertOrAppendPlacementNodeIntoContainer(node: Fiber, before, parent) {
  const stateNode = node.stateNode;
  if (before) {
    insertInContainerBefore(parent, stateNode, before);
  } else {
    appendChildToContainer(parent, stateNode);
  }
}

commitLayoutEffects

// commitLayoutEffect最终会调用commitLayoutEffectOnFiber
function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber
) {
  // 如果fiber有LayoutMask,例如:
  // 函数组件
  //  在调用useLayoutEffect时会在fiber的flags上置HookLayout
  //  LayoutMask包含HookLayout,所以匹配,最终执行useLayoutEffect中的回调
  // 类组件
  //   添加了componentDidMount,那么在mountClassInstance函数中置一个标记
  //   所以这里同样也是可以匹配上LayoutMask,类组件就会执行componentDidMount函数
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent: {
        // 会调用useLayoutEffect的第一个参数,在Hook中说明
        commitHookEffectListMount();
        break;
      }
      case ClassComponent: {
        const instance = finishedWork.stateNode;
        // LayoutMask包含了Update
        if (finishedWork.flags & Update) {
          instance.componentDidMount();
        }
        break;
      }
    }
  }
}

小结

  • react将真正提交变化分为3步走
    • commitBeforeMutationEffects 所有提交变化之前做的事。本文未涉及
    • commitMutationEffects 真正将render阶段的变化应用到页面
    • commitLayoutEffects 将修改应用到页面上后做的操作