前言
本篇是React源码解析系列第五篇。主要学习beginWork
阶段的流程。源码版本为v18.2.0。
本篇讨论的源码是一个简化的版本,重点是先理解React整体的架构,一些复杂的内容会延后说明。
复习
首先我们回顾一下# React源码解析(三):初次渲染中的流程。这个流程是略过了函数组件、类组件、并行工作循环、hook调用、优先级队列的简化版本。
- root实例的
render
方法会调用updateContainer
创建更新对象并放到fiber的更新队列中,由此我们引出fiber上的更新队列是一个循环链表
。最后调用scheduleUpdateOnFiber
调度更新fiber。 - 向浏览器请求时间做调度(具体如何请求后续文章说明),
我们先设定第一次是同步更新
,执行performSyncWorkOnRoot
。 - 进行
renderRootSync
,先去准备一棵替身树(workInProgress)
,通过beginWork
进行当前fiber的子fiber链表构建,并返回下一个工作单元,如果下一个工作单元为null,则执行completeUnitOfWork
,完成该任务单元(completeWork
),再去执行该fiber的弟弟,如果没有弟弟则返回父节点,完成父节点,寻找父节点的弟弟,直到返回根节点。 - 最后进行
commitRoot
提交真实dom更新。
beginWork
接下来我们通过代码去了解beginWork具体细节,下面的代码的互相调用可能会看的七荤八素,可以查看mini-react 初次渲染 这个仓库。
首先需要知道的是,beginWork阶段React的目的是什么:
- 根据当前Fiber的孩子创建Fiber链表,根据react元素类型创建或复用Fiber,其之间通过sibling指向自己的弟弟,return指向父节点。
只需要创建一层的fiber链表,后续循环调用会完善这个链表
- 进行dom-diff,对Fiber节点标记更新、删除、插入等。
- 返回头Fiber结点。
- 在下次工作循环中继续进行构建。
具体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"
);
}
我们对照以下这张流程图来进行梳理
至此我们便大致明白了beginWork的工作流程。
本文正在参加「金石计划」