React源码解析(三):初次渲染

前言

本篇是React源码解析系列第三篇。主要学习React的初始化和初次渲染。源码版本为v18.2.0。
阅读本篇需要Fiber基础,可以查看# React源码解析(二):Fiber架构

具体源码解析会略过诸如React校验、优先级调度、事件处理机制等复杂内容,也就是说本篇讲解的源码是一个简化版本,方便阅读理解。可以查看React源码进行对比。

初始化流程

从使用角度来说,我们初始化一个React App需要以下两个步骤:

import ReactDOM from "react-dom/client";
import App from "./App";
// 第一步 创建root类的实例
const root = ReactDOM.createRoot(document.getElementById("root"));
// 第二步 初次渲染
root.render(<App />);

第一步,我们先来了解 ReactDOM.createRoot 做了什么。

创建根

React的根是比较特殊的,因为创建时我们会传入指定的根容器结点,也就是说一开始根节点就是有真实DOM的,由此引入React对根的两个定义:FiberRootNodeHostRootFiber

  • FiberRootNode: 指的是root节点,它包含了root的真实DOM(containerInfo)。
  • HostRootFiber: 指的是root节点对应的Fiber。
    它们之间的关系如图所示,各有一个指针指向对方。FiberRootNode的containerInfo属性即为传入的根容器的真实DOM。
    React源码解析(三):初次渲染
    了解了这个我们来看下React创建根具体做了什么。

createRoot

// react-dom/src/client/ReactDOMRoot.js
import {createContainer} from "react-reconciler/src/ReactFiberReconciler";

// root类
function ReactDOMRoot(internalRoot){
    this._internalRoot = internalRoot;
}

// container即为传入的真实DOM div#root
export function createRoot(container){
    // 创建FiberRootNode
    const root = createContainer(container);
    return new ReactDOMRoot(root);
}

简化后可以发现createRoot做的事情很简单,创建了一个root对象(FiberRootNode),并将其挂在实例化的ReactDOMRoot上,方便后面使用,重点来看root所代表的FiberRootNode是如何创建的。

// react-reconciler/src/ReactFiberReconciler.js
import { createFiberRoot } from './ReactFiberRoot';
export function createContainer(containerInfo){
    return createFiberRoot(containerInfo);
}
// react-reconciler/src/ReactFiberRoot.js
import { createHostRootFiber } from "./ReactFiber";
import { initialUpdateQueue } from "./ReactFiberClassUpdateQueue";

function FiberRootNode(containerInfo){
    // containerInfo即为root的真实DOM div#root
    this.containerInfo = containerInfo;
}

export function createFiberRoot(containerInfo){
    const root = new FiberRootNode(containerInfo);
    //  uninitializedFiber即为HostRootFiber
    const uninitializedFiber = createHostRootFiber();
    // FiberRootNode和HostRootFiber互相指向
    root.current = uninitializedFiber;
    uninitializedFiber.stateNode = root;
    // 初始化更新队列
    initialUpdateQueue(uninitializedFiber);
    return root;
}

ReactFiber.js是为了专门去创建Fiber的文件

// react-reconciler/src/ReactFiber.js
import { HostRoot } from "./ReactWorkTags";

export function createHostRootFiber() {
  return createFiber(HostRoot, null, null);
}
// 创建Fiber实例
export function createFiber(tag, pendingProps, key) {
  return new FiberNode(tag, pendingProps, key);
}

/**
 * Fiber类
 * @param {*} tag Fiber的类型tag,如函数组件、原生组件、类组件等等虚拟dom对应编号
 * @param {*} pendingProps 新属性,等待处理或生效的属性
 * @param {*} key 唯一标识
 */
export function FiberNode(tag, pendingProps, key) {
  // 虚拟dom对应的标识
  this.tag = tag;
  this.key = key;
  // 虚拟dom的类型,如div、span、p 函数组件和类组件则是本身函数或类
  this.type = null;
  // 此Fiber对应的真实dom节点
  this.stateNode = null;
  // 父节点
  this.return = null;
  // 第一个子节点
  this.child = null;
  // 第一个弟弟节点
  this.sibling = null;
  // 等待生效的属性
  this.pendingProps = pendingProps;
  // 已经生效的属性
  this.memoizedProps = null;
  // 每种React元素存的类型是不一样的,如类组件对应的fiber存的就是实例的状态,HostRoot存的就是要渲染的元素
  this.memoizedState = null;
  // 更新队列
  this.updateQueue = null;
  // 替身 双缓冲机制 dom-diff的时候用
  this.alternate = null;
  this.ref = null;
  // 省略一些未涉及的属性
  ...
}

ReactWorkTags.js是存放React虚拟DOM的tag标识

// react-reconciler/src/ReactWorkTags.js
// 函数组件
export const FunctionComponent = 0;
// 类组件
export const ClassComponent = 1;
// 未定组件 因为函数组件和类组件都是一个函数
export const IndeterminateComponent = 2;
// 容器根节点
export const HostRoot = 3;
// 原生节点
export const HostComponent = 5;
// 文本节点
export const HostText = 6;
// react-reconciler/src/ReactFiberClassUpdateQueue.js
// 初始化fiber的更新队列
export function initialUpdateQueue(fiber) {
  const queue = {
    shared: {
      pending: null,
    },
  };
  fiber.updateQueue = queue;
}

至此我们完成了创建root的流程,完成后真实DOM、ReactDOMRoot、FiberRootNode、HostRootFiber的关系如下:

React源码解析(三):初次渲染

初次渲染

初次渲染会调用 ReactDOMRootrender 方法。首先先添上这个方法。

// react-dom/src/client/ReactDOMRoot.js
import {
  createContainer,
  updateContainer,
} from "react-reconciler/src/ReactFiberReconciler";
ReactDOMRoot.prototype.render = function(children){
    // 取出FiberRootNode
    const root = this._internalRoot;
    // 初次渲染
    updateContainer(children, root);
}
// react-reconciler/src/ReactFiberReconciler.js
import { createUpdate, enqueueUpdate } from "./ReactFiberClassUpdateQueue";
import { scheduleUpdateOnFiber } from "./ReactFiberWorkLoop";
export function updateContainer(element, container) {
  // 获取HostRootFiber
  const current = container.current;
  // 创建更新对象
  const update = createUpdate();
  // 要更新的虚拟dom
  update.payload = { element };
  // 入队更新 把此更新对象添加到HostRootFiber的更新队列上
  const root = enqueueUpdate(current, update);
  // 对Fiber进行调度更新
  scheduleUpdateOnFiber(root, current);
}
// react-reconciler/src/ReactFiberClassUpdateQueue.js
export const UpdateState = 0;
export function createUpdate() {
  const update = {
    tag: UpdateState,
    next: null,
  };
  return update;
}

/*
    源码中,此处会入队并发队列,我们在这边进行简化处理,只需要知道
    这个方法会将新的更新对象串成一个循环链表,并返回FiberRootNode即可
*/
export function enqueueUpdate(fiber, update) {
  const updateQueue = fiber.updateQueue;
  const pending = updateQueue.shared.pending;
  // 将update串成一个循环链表
  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  updateQueue.shared.pending = update;
  /*
    向上找到FiberRootNode
  */
  let parent = sourceFiber.return;
  // FiberRootNode的parent为null
  while (parent !== null) {
    node = parent;
    parent = parent.return;
  }
  if (node.tag === HostRoot) {
    // FiberRootNode
    return node.stateNode;
  }
  return null;
}

至此,我们创建了更新对象并将其放到了Fiber的updateQueue上只剩下最后的调度更新

Fiber上的更新循环链表

此处穿插讲解一下Fiber上的更新是如何存储的,还记得我们初始化Fiber的updateQueue吗?它的shared属性有一个pending指向等待生效的更新,初始时pending为null。

React源码解析(三):初次渲染

当有更新入队时,pending便会指向这次更新,同时,这些更新会串成一个 循环链表,也就是说,pending指向的是 最后一次更新,最后一次更新会指向第一次更新,第一次更新会指向第二次更新,第二次更新指向第三次更新如此循环。用图表示会更清晰一些。

第一个更新入队

React源码解析(三):初次渲染

第二个更新入队

React源码解析(三):初次渲染

第三个更新入队

React源码解析(三):初次渲染

可以思考一下为什么要采用循环链表的形式。
因为循环链表的形式操作简便且性能高效,为什么不采用数组呢?因为为了高优先级打断低优先级的操作,更新不是一定按照顺序的,链表的拼接会更加简单高效。为什么不采用单向链表呢?可以思考一下有更新加入单链表是不是会更加复杂?需要循环到next为null的节点再插入。

scheduleUpdateOnFiber

接下来便进入我们本篇的重点,workLoop,再次强调一下,本篇源码讲解略过了如优先级调度、hooks等复杂内容,以先了解react大概架构为主,后续文章会再补充进行解析。
默认情况下,react的渲染都是并行的(Concurrent),为了简便,第一次渲染我们先认为是同步的。

// react-reconciler/src/ReactFiberWorkLoop.js
// 当前工作中的Fiber树 即替身Fiber树
let workInProgress = null;

export function scheduleUpdateOnFiber(root) {
  ensureRootIsScheduled(root);
}
function ensureRootIsScheduled(root) {
  /*
      此处Scheduler_scheduleCallback会去向浏览器每帧拿5ms时间执行performSyncWorkOnRoot
      本篇先略过,后续讲解
  */
  Scheduler_scheduleCallback(performSyncWorkOnRoot.bind(null, root));
}
// 执行同步任务
function performSyncWorkOnRoot(root) {
  // 同步render
  renderRootSync(root);
  // 同步render完成后 root.current.alternate即为新的fiber树
  const finishedWork = root.current.alternate;
  root.finishedWork = finishedWork;
  // 开始进入提交阶段,就是执行副作用修改真实dom 下一篇讲解
  commitRoot(root);
}
function renderRootSync(root) {
  // 准备一棵新的Fiber树
  prepareFreshStack(root);
  // 同步工作循环
  workLoopSync();
}
function prepareFreshStack(root) {
  // root.current即为HostRootFiber
  workInProgress = createWorkInProgress(root.current, null);
}
// 同步工作循环类似Stack reconciler,不会中断,执行到清空工作单元为止
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}
function performUnitOfWork(unitOfWork) {
  // 获取新fiber对应的老fiber
  const current = unitOfWork.alternate;
  // 当前fiber的子fiber链表构建 dom-diff 本篇略过 后续讲解
  const next = beginWork(current, unitOfWork);
  // beginWork会将pendingProps更新 此处将当前待生效的属性标记为已生效的属性
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  // 没有子节点代表当前的fiber已经完成了
  if (next === null) {
    // 完成工作单元
    completeUnitOfWork(unitOfWork);
  } else {
    // 有子节点则成为下一个工作单元
    workInProgress = next;
  }
}

// 完成一个工作单元
function completeUnitOfWork(unitOfWork) {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    // 最后的HostRootFiber的return 为null
    const returnFiber = completedWork.return;
    // 执行此fiber的完成工作 如果是原生组件的话需要创建真实dom节点 本篇先略过 后续讲解
    completeWork(current, completedWork);
    // 执行当前fiber的弟弟节点
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    // 如果没有弟弟,说明当前fiber是父fiber的最后一个节点
    // 再次进入while循环 完成父fiber
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
}
// react-reconciler/src/ReactFiber.js
/*
    根据当前Fiber树创建新的Fiber树
*/
export function createWorkInProgress(current, pendingProps){
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    workInProgress = createFiber(current.tag, pendingProps, current.key);
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    workInProgress.pendingProps = pendingProps;
    workInProgress.type = current.type;
  }
  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;
  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;
  return workInProgress;
}

这么一长串代码看下来肯定有些晕头晕脑,我们来梳理一下。

  1. 调度开始,每帧向浏览器请求5ms时间调度任务,如何请求先略过,后面讲解。
  2. 执行renderRootSync,根据当前的Fiber树创建一棵新的树,如果有替身树则复用,称为workInProgress,执行工作循环workLoopSync
  3. 循环每个工作单元,还记得上一篇文章的Fiber树的执行顺序吗?是的,深度遍历,先遍历节点的第一个孩子,如果当前节点没有孩子则完成当前节点,寻找当前节点的弟弟,如果也没有弟弟则返回父节点,完成父节点,寻找父节点的弟弟,直到根节点。其中beginWorkcompleteWork我们留到下一篇讲解。
  4. 进入提交阶段 commitRoot,去处理节点的副作用,如插入、删除等。

至此,我们大概了解了React初次渲染的步骤,这些步骤后续更新时也会进行复用。

本文正在参加「金石计划」

原文链接:https://juejin.cn/post/7216965516878807096 作者:inky

(0)
上一篇 2023年4月2日 下午5:01
下一篇 2023年4月2日 下午5:12

相关推荐

发表回复

登录后才能评论