React源码解析(五):beginWork

437 阅读5分钟

前言

本篇是React源码解析系列第五篇。主要学习beginWork阶段的流程。源码版本为v18.2.0。
本篇讨论的源码是一个简化的版本,重点是先理解React整体的架构,一些复杂的内容会延后说明。

复习

首先我们回顾一下# React源码解析(三):初次渲染中的流程。这个流程是略过了函数组件、类组件、并行工作循环、hook调用、优先级队列的简化版本。

  1. root实例的render方法会调用updateContainer创建更新对象并放到fiber的更新队列中,由此我们引出fiber上的更新队列是一个循环链表。最后调用scheduleUpdateOnFiber调度更新fiber。
  2. 向浏览器请求时间做调度(具体如何请求后续文章说明),我们先设定第一次是同步更新,执行performSyncWorkOnRoot
  3. 进行renderRootSync,先去准备一棵替身树(workInProgress),通过beginWork进行当前fiber的子fiber链表构建,并返回下一个工作单元,如果下一个工作单元为null,则执行completeUnitOfWork,完成该任务单元(completeWork),再去执行该fiber的弟弟,如果没有弟弟则返回父节点,完成父节点,寻找父节点的弟弟,直到返回根节点。
  4. 最后进行commitRoot提交真实dom更新。

beginWork

接下来我们通过代码去了解beginWork具体细节,下面的代码的互相调用可能会看的七荤八素,可以查看mini-react 初次渲染 这个仓库。
首先需要知道的是,beginWork阶段React的目的是什么:

  1. 根据当前Fiber的孩子创建Fiber链表,根据react元素类型创建或复用Fiber,其之间通过sibling指向自己的弟弟,return指向父节点。只需要创建一层的fiber链表,后续循环调用会完善这个链表
  2. 进行dom-diff,对Fiber节点标记更新、删除、插入等。
  3. 返回头Fiber结点。
  4. 在下次工作循环中继续进行构建。

具体dom-diff流程由于篇幅问题我们先进行简化,后续文章详细说明,我们先来了解大致流程

// react-reconciler/src/ReactFiberBeginWork.js

import { HostComponent, HostRoot, HostText } from "./ReactWorkTags";
import { shouldSetTextContent } from "react-dom-bindings/src/client/ReactDOMHostConfig";

/*
    开始任务
    current: 旧的Fiber,也就是当前页面上展示的真实dom对应的Fiber节点
    workInProgress: 当前工作单元,也就是替身Fiber(alternate)
    
    此方法目前未考虑函数组件等,我们先用最简单的原生节点来看它的流程
*/
export function beginWork(current, workInProgress) {
  switch (workInProgress.tag) {
    // 根节点
    case HostRoot:
      return updateHostRoot(current, workInProgress);
    // 原生dom节点
    case HostComponent:
      return updateHostComponent(current, workInProgress);
    default:
      return null;
  }
}

function updateHostRoot(current, workInProgress) {
  // 处理更新队列
  processUpdateQueue(workInProgress);
  // 处理后workInprogress.memoizedState就是最新的状态
  const nextState = workInProgress.memoizedState;
  // 拿到虚拟dom
  const nextChildren = nextState.element;
  // 协调子节点 dom-diff算法
  // 根据新的虚拟DOM生成子fiber链表
  reconcileChildren(current, workInProgress, nextChildren);
  return workInProgress.child;
}

// 更新根节点
function updateHostComponent(current, workInProgress) {
  const { type } = workInProgress;
  // 当前工作单元待生效的属性
  const nextProps = workInProgress.pendingProps;
  // 拿到子虚拟dom
  let nextChildren = nextProps.children;
  /*
      React针对只有文本独生子的原生dom节点做了优化,会将父节点+文本独生子当做一个工作单元进行处理
  */
  // 判断当前虚拟dom的儿子是否是一个文本独生子
  const isDirectTextChild = shouldSetTextContent(type, nextProps);
  if (isDirectTextChild) {
    // 文本独生子与父节点一起处理
    nextChildren = null;
  }
  // 协调子节点 dom-diff算法
  // 根据新的虚拟DOM生成子fiber链表
  reconcileChildren(current, workInProgress, nextChildren);
  return workInProgress.child;
}

/**
 * 根据新的虚拟DOM生成子fiber链表
 * @param {*} current 老的fiber
 * @param {*} workInProgress 新的fiber
 * @param {*} nextChildren 新的子虚拟dom
 */
function reconcileChildren(current, workInProgress, nextChildren) {
  // 如果没有老fiber,说明新fiber是新创建的
  if (current === null) {
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren);
  } else {
    // 有老fiber,需要做dom-diff,拿老的子fiber链表和新的子虚拟dom进行比较,进行最小化的更新
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren
    );
  }
}
// react-reconciler/src/ReactFiberClassUpdateQueue.js

export const UpdateState = 0;
/**
 * 此方法进行了简化
 * 根据老状态和更新链表中的更新计算最新的状态
 * @param {*} workInProgress 要计算的fiber
 */
export function processUpdateQueue(workInProgress) {
  const queue = workInProgress.updateQueue;
  // 取出更新队列
  const pendingQueue = queue.shared.pending;
  // 如果有更新
  if (pendingQueue !== null) {
    // 清空更新链表
    queue.shared.pending = null;
    // 更新链表的指针为最后一次更新
    const lastPendingUpdate = pendingQueue;
    // 更新链表为循环链表,最后一次更新的next指向第一次更新
    const firstPendingUpdate = lastPendingUpdate.next;
    // 将更新链表剪开,成为一个单链表
    lastPendingUpdate.next = null;
    // 获取老状态
    let newState = workInProgress.memoizedState;
    let update = firstPendingUpdate;
    while (update) {
      // 根据老状态和更新 计算新状态
      newState = getStateFromUpdate(update, newState);
      update = update.next;
    }
    // 把最终计算到的状态赋值给memoizedState
    workInProgress.memoizedState = newState;
  }
}

/**
 * 根据老状态和更新计算新状态
 * @param {*} update 更新的对象
 * @param {*} prevState 老状态
 */
function getStateFromUpdate(update, prevState) {
  switch (update.tag) {
    case UpdateState:
      // 
      const { payload } = update;
      return Object.assign({}, prevState, payload);

    default:
      break;
  }
}
// react-reconciler/src/ReactChildFiber.js
/**
 * 此处使用了函数的柯里化
 * @param {*} shouldTracksSideEffects 是否跟踪副作用
 */
function createChildReconciler(shouldTracksSideEffects) {
  /**
   * 协调单个元素 为单个元素创建Fiber
 */
  function reconcileSingleElement(returnFiber, currentFirstFiber, newChild) {
    // 通过虚拟dom创建fiber
    const created = createFiberFromElement(newChild);
    // 将创建的Fiber的父节点进行指向
    created.return = returnFiber;
    return created;
  }

  // 插入单个子Fiber节点
  function placeSingleChild(newFiber) {
    if (shouldTracksSideEffects) {
      // 添加插入副作用
      // 要在最后的提交阶段插入此节点, react渲染分成渲染(创建Fiber树)和提交(更新真实dom)两个阶段
      newFiber.flags |= Placement;
    }
    return newFiber;
  }

  // 根据newChild类型创建对应的Fiber
  function crateChild(returnFiber, newChild) {
    // 创建文本节点对应的Fiber
    if (
      (typeof newChild === "string" && newChild !== "") ||
      typeof newChild === "number"
    ) {
      const created = createFiberFromText(`${newChild}`);
      created.return = returnFiber;
      return created;
    }
    // 创建其他类型的Fiber
    if (typeof newChild === "object" && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          const created = createFiberFromElement(newChild);
          created.return = returnFiber;
          return created;

        default:
          break;
      }
    }
  }

  // 新增插入操作 此处先简化 与placeSingleChild差别在dom-diff的时候
  function placeChild(newFiber, newIndex) {
    newFiber.index = newIndex;
    if (shouldTracksSideEffects) {
      // 如果父Fiber是初次挂载,shouldTracksSideEffects是false,不需要添加flags
      // 这种情况下会在完成阶段把所有的子节点全部添加到自己身上
      newFiber.flags |= Placement;
    }
  }

  /*
      协调多个子节点
      源码此处进行了复杂的dom-diff 此处先简化
  */
  function reconcileChildrenArray(returnFiber, currentFirstFiber, newChildren) {
    // 返回的第一个新孩子
    let resultingFirstChild = null;
    // 上一个新Fiber
    let previousNewFiber = null;
    let newIndex = 0;
    for (; newIndex < newChildren.length; newIndex++) {
      // 根据元素类型创建对应的Fiber
      const newFiber = crateChild(returnFiber, newChildren[newIndex]);
      if (newFiber === null) continue;
      // 标记插入更新
      placeChild(newFiber, newIndex);
      // previousNewFiber为null的话说明这个fiber是第一个fiber
      if (previousNewFiber === null) {
        // 这个fiber是大儿子
        resultingFirstChild = newFiber;
      } else {
        // 不是大儿子 添加为上个fiber的兄弟节点
        previousNewFiber.sibling = newFiber;
      }
      // 让newFiber成为上一个fiber
      previousNewFiber = newFiber;
    }
    // 返回第一个孩子
    return resultingFirstChild;
  }

  /**
   * 比较子Fiber dom-diff 老Fiber和新的虚拟dom进行对比
   * @param {*} returnFiber 新的父Fiber
   * @param {*} currentFirstFiber 老Fiber的第一个子Fiber
   * @param {*} newChild 新的子虚拟dom
   */
  function reconcileChildFibers(returnFiber, currentFirstFiber, newChild) {
    if (typeof newChild === "object" && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(returnFiber, currentFirstFiber, newChild)
          );
        default:
          break;
      }
    }
    if (isArray(newChild)) {
      return reconcileChildrenArray(returnFiber, currentFirstFiber, newChild);
    }

    return null;
  }
  return reconcileChildFibers;
}

// 有老fiber 更新的时候用这个
export const reconcileChildFibers = createChildReconciler(true);
// 没有老fiber 初次挂载用这个
export const mountChildFibers = createChildReconciler(false);
// react-dom-bindings/src/client/ReactDOMHostConfig.js

// 判断是否是文本独生子
export function shouldSetTextContent(type, props) {
  return (
    typeof props.children === "string" || typeof props.children === "number"
  );
}

我们对照以下这张流程图来进行梳理

image.png

至此我们便大致明白了beginWork的工作流程。

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