简易实现 React 页面初次渲染

先从单个原生标签节点开始实现,后面文章会陆续添加多个节点、文本节点、Fragment、类组件以及函数组件的实现。

js 复制代码
// 单个原生标签节点
const jsx = (
  <div className="box border">
    <h1 className="border">omg</h1>
  </div>
)

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(jsx);

一切从根节点开始。

首先创建根节点,函数为 createRoot,入参为根 Fiber。

js 复制代码
export function createRoot(container: Container): RootType {
  const root: FiberRoot = createFiberRoot(container); // createFiberRoot 参考文章 https://juejin.cn/post/7524141053446094875

  return new ReactDOMRoot(root);
}

createRoot 返回 ReactDOMRoot 的实例,实例上有三个属性 render、unmount 和 _internalRoot。

  1. render - 渲染
  2. unmount - 卸载
  3. _internalRoot - React 内部变量

render 和 unmount 都依赖 updateContainer 函数。

js 复制代码
function ReactDOMRoot(_internalRoot: FiberRoot) {
  this._internalRoot = _internalRoot;
}

ReactDOMRoot.prototype.render = function (children: ReactNodeList) {
  updateContainer(children, this._internalRoot);
};

// unmount 省略

updateContainer 调用 scheduleUpdateOnFiber 实现调度更新。

js 复制代码
export function updateContainer(element: ReactNodeList, container: FiberRoot) {
  // 1. 获取 current
  const current = container.current;
  current.memoizedState = { element };

  // 2. 调度更新
  scheduleUpdateOnFiber(container, current);
}

scheduleUpdateOnFiber 调用 performConcurrentWorkOnRoot。performConcurrentWorkOnRoot 包含了两个重要阶段:render 阶段和 commit 阶段。

js 复制代码
export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  isSync?: boolean
) {
  workInProgressRoot = root;
  workInProgress = fiber;

  if (isSync) {
    // queueMicrotask 是 JS 原生 API
    queueMicrotask(() => performConcurrentWorkOnRoot(root));
  }
}

export function performConcurrentWorkOnRoot(root: FiberRoot) {
  // 1. render, 构建 fiber 树,即 VDOM(beginWork|completeWork)
  renderRootSync(root);

  const finishedWork = root.current.alternate;
  root.finishedWork = finishedWork; // 根 Fiber

  // 2. commit, VDOM -> DOM
  commitRoot(root);
}

Render 阶段

Render 阶段创建 fiber 树,入口函数是 renderRootSync。

performUnitOfWork 函数包含了 render 阶段的两个重要子阶段 beginWork 和 completeWork。

js 复制代码
function renderRootSync(root: FiberRoot) {
  // 1. render 阶段开始
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext;
  // 2. 初始化
  prepareFreshStack(root);

  // 3. 遍历构建 fiber 树
  workLoopSync();
  // 4. render 结束
  executionContext = prevExecutionContext;
  workInProgressRoot = null;
}

function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber) {
  const current = unitOfWork.alternate;
  // 1. beginWork
  let next = beginWork(current, unitOfWork);
  // 把 pendingProps 更新到 memoizedProps
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  // 1.1 执行自己
  // 1.2 (协调,bailout)返回子节点

  if (next === null) {
    // 没有产生新的 work
    // 2. completeWork
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

BeginWork 阶段

beginWork 主要做两件事:

  1. 更新自己
  2. 协调子节点

实际上,begingWork 要处理的事情很多:

  1. 区分单个节点和多个节点
  2. 区分不同的节点类型,比如类组件、函数组件等
  3. 区分渲染和更新
js 复制代码
// 1. 处理当前 fiber,因为不同组件对应的 fiber 处理方式不同,
// 2. 返回子节点 child
export function beginWork(
  current: Fiber | null,
  workInProgress: Fiber
): Fiber | null {
  switch (workInProgress.tag) {
    // 根节点
    case HostRoot:
      return updateHostRoot(current, workInProgress);
    // 原生标签,div、span...
    case HostComponent:
      return updateHostComponent(current, workInProgress);
  }
  throw new Error(
    `Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
      "React. Please file an issue."
  );
}

beginWork 根据 tag 调用不同的 update 函数,初次渲染主要涉及两个函数:updateHostRoot 和 updateHostComponent。

js 复制代码
// 根节点
function updateHostRoot(current: Fiber | null, workInProgress: Fiber) {
  const nextChildren = workInProgress.memoizedState.element;

  reconcileChildren(current, workInProgress, nextChildren);

  return workInProgress.child;
}

function shouldSetTextContent(type: string, props: any): boolean {
  return (
    type === "textarea" ||
    type === "noscript" ||
    isStr(props.children) ||
    isNum(props.children) ||
    (typeof props.dangerouslySetInnerHTML === "object" &&
      props.dangerouslySetInnerHTML !== null &&
      props.dangerouslySetInnerHTML.__html != null)
  );
}

// 原生标签,div、span...
// 初次渲染 协调
function updateHostComponent(current: Fiber | null, workInProgress: Fiber) {
  const { type, pendingProps } = workInProgress;
  const isDirectTextChild = shouldSetTextContent(type, pendingProps);
  if (isDirectTextChild) {
    // 文本属性没有子节点
    return null;
  }
  
  // 如果原生标签只有一个文本,这个时候文本不会再生成 fiber 节点,而是当做这个原生标签的属性
  const nextChildren = pendingProps.children;
  reconcileChildren(current, workInProgress, nextChildren);

  return workInProgress.child;
}

updateHostRoot 和 updateHostComponent 都依赖 reconcileChildren 函数。

js 复制代码
// 协调子节点,构建新的 fiber 树
function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any
) {
  if (current === null) {
    // 初次挂载
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren);
  } else {
    // 更新
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren
    );
  }
}

reconcileChildren 区分初次挂载和更新,对应函数 mountChildFiber 和 reconcileChildFibers。两者都调用了 createChildReconciler 只是参数不同。

js 复制代码
export const reconcileChildFibers: ChildReconciler =
  createChildReconciler(true);
  
export const mountChildFibers: ChildReconciler = createChildReconciler(false);

createChildReconciler 是一个 wrapper function(函数里套函数)。

js 复制代码
// 协调子节点
function createChildReconciler(shouldTrackSideEffects: boolean) {
  // 给 fiber 节点添加 flags
  function placeSingleChild(newFiber: Fiber) {
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.flags |= Placement; // 节点可能既修改属性又移动位置,所以用位运算,可叠加
    }
    return newFiber;
  }
  
  // 协调单个节点,对于页面初次渲染,创建 fiber,不涉及对比复用老节点
  function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement
  ) {
    let createdFiber = createFiberFromElement(element);
    createdFiber.return = returnFiber;
    return createdFiber;
  }
  
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any
  ) {
    // 检查 newChild 类型,单个节点
    if (typeof newChild === "object" && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE: {
          return placeSingleChild(
            reconcileSingleElement(returnFiber, currentFirstChild, newChild)
          );
        }
      }
    }

    return null;
  }

  return reconcileChildFibers;
}

CompleteWork 阶段

completeUnitOfWork 阶段使用了深度优先遍历,其中重点函数是 completeWork。

js 复制代码
// 深度优先遍历,子节点、兄弟节点、叔叔节点、爷爷的兄弟节点...(王朝的故事)
function completeUnitOfWork(unitOfWork: Fiber) {
  let completedWork = unitOfWork;

  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    let next = completeWork(current, completedWork);
    if (next !== null) {
      workInProgress = next;
      return;
    }

    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }

    completedWork = returnFiber as Fiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
}

completeWork 函数实现了根节点和原生标签两个 case。

js 复制代码
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  switch (workInProgress.tag) {
    case HostRoot: {
      return null;
    }
    case HostComponent: {
      // 原生标签,type 是标签名
      const { type } = workInProgress;
        // 1. 创建真实 DOM
        const instance = document.createElement(type);
        // 2. 初始化 DOM 属性
        finalizeInitialChildren(instance, newProps);
        // 3. 把子 dom 挂载到父 dom 上
        appendAllChildren(instance, workInProgress);
        workInProgress.stateNode = instance;
      return null;
    }
  }

  throw new Error(
    `Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
      "React. Please file an issue."
  );
}

// 初始化、更新属性
function finalizeInitialChildren(
  domElement: Element,
  props: any
) {
  // 遍历 props
  for (const propKey in props) {
    const value = props[propKey];
    if (propKey === "children") {
      if (isStr(value) || isNum(value)) {
        // 属性
        domElement.textContent = value + "";
      }
    } else {
      // 设置属性
      (domElement as any)[propKey] = value;
    }
  }
}

function appendAllChildren(parent: Element, workInProgress: Fiber) {
  let nodeFiber = workInProgress.child; // 链表结构
  if (nodeFiber) {
      parent.appendChild(nodeFiber.stateNode); 
  }
}

Commit 阶段

Commit 阶段将 VDOM 转换成 DOM,入口函数是 commitRoot。

js 复制代码
function commitRoot(root: FiberRoot) {
  // 1. commit 阶段开始
  const prevExecutionContext = executionContext;
  executionContext |= CommitContext;
  // 2 mutation 阶段, 渲染 DOM 树
  commitMutationEffects(root, root.finishedWork as Fiber); //Fiber,HostRoot=3

  // 3. commit 结束
  executionContext = prevExecutionContext;
  workInProgressRoot = null;
}

function commitMutationEffects(root: FiberRoot, finishedWork: Fiber) {
  // 1. 遍历
  recursivelyTraverseMutationEffects(root, finishedWork);
  commitReconciliationEffects(finishedWork);
}

function recursivelyTraverseMutationEffects(
  root: FiberRoot,
  parentFiber: Fiber
) {
  let child = parentFiber.child;
  // 遍历单链表
  while (child !== null) {
    commitMutationEffects(root, child);
    child = child.sibling;
  }
}

// 提交协调的产生的 effects,比如 flags,Placement、Update、ChildDeletion
function commitReconciliationEffects(finishedWork: Fiber) {
  const flags = finishedWork.flags;
  if (flags & Placement) {
    // 页面初次渲染 新增插入 appendChild
    commitPlacement(finishedWork);
    finishedWork.flags &= ~Placement; // 删除 Placement
  }
}

function commitPlacement(finishedWork: Fiber) {
  if (finishedWork.stateNode && finishedWork.tag === HostComponent) {
    // finishedWork 是有 dom 节点
    const domNode = finishedWork.stateNode
    // 找 domNode 的父 DOM 节点对应的 fiber
    const parentFiber = getHostParentFiber(finishedWork);

    let parentDom = parentFiber.stateNode;

    if (parentDom.containerInfo) {
      // HostRoot
      parentDom = parentDom.containerInfo;
    }

    parentDom.appendChild(domNode)
  }
}

function getHostParentFiber(fiber: Fiber): Fiber {
  let parent = fiber.return;
  while (parent !== null) {
    if (isHostParent(parent)) {
      return parent;
    }
    parent = parent.return;
  }
  throw new Error(
    "Expected to find a host parent. This error is likely caused by a bug " +
      "in React. Please file an issue."
  );
}

// 检查 fiber 是 HostParent
function isHostParent(fiber: Fiber): boolean {
  return fiber.tag === HostComponent || fiber.tag === HostRoot;
}
相关推荐
卸任6 小时前
Vibe coding工具使用心得
前端·ai编程
brien97 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance7 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M7 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写7 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李7 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易7 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马8 小时前
前沿视界
前端
kyriewen8 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易8 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员