简易实现 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 调用 performConcurrentWorkOnRootperformConcurrentWorkOnRoot 包含了两个重要阶段: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 阶段的两个重要子阶段 beginWorkcompleteWork

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 函数,初次渲染主要涉及两个函数:updateHostRootupdateHostComponent

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;
}

updateHostRootupdateHostComponent 都依赖 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 区分初次挂载和更新,对应函数 mountChildFiberreconcileChildFibers。两者都调用了 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;
}
相关推荐
pobu16815 分钟前
aksk前端签名实现
java·前端·javascript
烛阴20 分钟前
带参数的Python装饰器原来这么简单,5分钟彻底掌握!
前端·python
0wioiw025 分钟前
Flutter基础(前端教程⑤-组件重叠)
开发语言·前端·javascript
冰天糖葫芦38 分钟前
VUE实现数字翻牌效果
前端·javascript·vue.js
南岸月明1 小时前
我与技术无缘,只想副业搞钱
前端
gzzeason1 小时前
在HTML中CSS三种使用方式
前端·css·html
hnlucky1 小时前
《Nginx + 双Tomcat实战:域名解析、静态服务与反向代理、负载均衡全指南》
java·linux·服务器·前端·nginx·tomcat·web
huihuihuanhuan.xin1 小时前
前端八股-promise
前端·javascript
星语卿2 小时前
浏览器重绘与重排
前端·浏览器
西瓜_号码2 小时前
React中Redux基础和路由介绍
javascript·react.js·ecmascript