写在专栏开头(叠甲)
-
作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。
-
本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。
-
本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。
-
本教程是基于 React v18.1.0 版本的解读,如果后续版本 React 的机制有了一定的更新迭代,这个教程可能会缺乏时效性
本一节的内容
本节是整个React 的源码与原理解读解读教程的最后一章,作者会用自己的理解带着大家梳理一下整个 React 的运行过程,因为篇幅的问题,作者会主要讲述一下一下重点的内容和要点的信息,侧重给读者一个完整的运行逻辑,对于讲解不到位的部分可以阅读之前的详细教程,教程肯定还是会有疏漏的部分请大家海涵
前置基础知识
Fiber架构
Fiber 是 React 16.x 开始新增的一个数据结构,React 将每个节点都封装到了一个 Fiber 中,使得整个 DOM 树的渲染任务被分成了一个一个小片,每个 Fiber 中通过这样的指针相互联系 ,最后形成一个链表树的结构:
return: Fiber | null, //表述一个元素的父亲
child: Fiber | null, //表述一个元素的第一个孩子
sibling: Fiber | null, //表述一个元素的兄弟
index: number, //在兄弟节点中的位置
如果你想了解 Fiber 的详细数据结构可以看这篇 React 的源码与原理解读(二):Fiber 与 Fiber 链表树
引入 Fiber 架构的好处是:
-
在引入它之前,React 每个更新需要遍历整个数据结构,当一个页面比较复杂的时候,js 主线程就不得不停止其他工作开始进行渲染的逻辑,用户的点击输入等交互事件、页面动画等都不会得到响应当。
-
引入 Fiber 后,React 会从根节点开始一个一个去更新每一个 Fiber ,每当处理完一个 Fiber ,在处理下一个 Fiber 之前,js 可以转而去处理优先级更高、更需要快速响应的任务,因为 Fiber 中有指向孩子和兄弟的指针,所以在处理完紧急任务后可以很容易的找到下一个需要遍历的节点,从而继续我们的任务。
双缓冲架构
我们的 React 中,存在两颗上述的 Fiber 链表树,一颗是用于渲染页面的 current Fiber 树,一颗是 workInProgress Fiber 树,我们用于渲染当前页面的是 current Fiber 树,而我们在整个更新过程中会构建一颗叫做 workInProgress Fiber 树。
使用双缓冲树的原因是:
- 上面提到了,我们操作 Fiber 的过程是可能被中断的,所以我们不能直接更新当前的 Fiber ,这样可能会产生更新了一部分另一部分没更新的操作
- 在遍历 Fiber 树过程中,我们大量遇到节点不需要更新的情况出现,此时我们可以直接复用另一颗树上对应的 Fiber 来提升系统的性能和效率,复用 也是我们 React Diff 算法的精髓之一
在整个架构的顶部,有一个 FiberRootNode 节点,它存储了一些运行过程中的标志和数据等,它最重要的作用是通过 current 属性指向了当前工作的 Fiber 树的根,当我们的 workInProgress Fiber 生成完毕后,我们通过切换 current 属性的指向可以直接切换我们的整个工作树,使得 current 树和 workInProgress 树实现交换:
Lanes
上面提到了,我们的 Fiber 操作过程是可以中断的,当有紧急事进入时,React 会优先处理紧急任务,而判断任务的紧急程度就需要一套指标,他就是 Lanes 系统:
lanes 使用31位二进制来表示优先级车道,共31条, 位数越小(1的位置越靠右)表示优先级越高。在实际使用中,我们会有一个 31位的二进制数来标识我们的任务,,如果他的某一位对应的是 1 ,那么他的某个优先级就有任务了,反之就是空闲的,你可以理解成我们把任务看成我一辆辆汽车,他们需要在不同的车道上行驶才不会相互影响,但是右边的车道可以向左边超车(优先级高)。这就是为什么我们称之为 车道模型。
// lane使用31位二进制来表示优先级车道共31条, 位数越小(1的位置越靠右)表示优先级越高
export const TotalLanes = 31;// 没有优先级
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;// 同步优先级,表示同步的任务一次只能执行一个,例如:用户的交互事件产生的更新任务
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;// 连续触发优先级,例如:滚动事件,拖动事件等
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000000100;// 默认优先级,例如使用setTimeout,请求数据返回等造成的更新
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000010000;// 过度优先级,例如: Suspense、useTransition、useDeferredValue等拥有的优先级(React18 的新机制)
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111111000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000001000000;
const TransitionLane2: Lane = /* */ 0b0000000000000000000000010000000;
const TransitionLane3: Lane = /* */ 0b0000000000000000000000100000000;
const TransitionLane4: Lane = /* */ 0b0000000000000000000001000000000;
const TransitionLane5: Lane = /* */ 0b0000000000000000000010000000000;
const TransitionLane6: Lane = /* */ 0b0000000000000000000100000000000;
const TransitionLane7: Lane = /* */ 0b0000000000000000001000000000000;
const TransitionLane8: Lane = /* */ 0b0000000000000000010000000000000;
const TransitionLane9: Lane = /* */ 0b0000000000000000100000000000000;
const TransitionLane10: Lane = /* */ 0b0000000000000001000000000000000;
const TransitionLane11: Lane = /* */ 0b0000000000000010000000000000000;
const TransitionLane12: Lane = /* */ 0b0000000000000100000000000000000;
const TransitionLane13: Lane = /* */ 0b0000000000001000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000000010000000000000000000;
const TransitionLane15: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane16: Lane = /* */ 0b0000000001000000000000000000000;// 重试车道
const RetryLanes: Lanes = /* */ 0b0000111110000000000000000000000;
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000;
const RetryLane5: Lane = /* */ 0b0000100000000000000000000000000;export const SomeRetryLane: Lane = RetryLane1;
// 可选的车道
export const SelectiveHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;//非空闲车道
const NonIdleLanes: Lanes = /* */ 0b0001111111111111111111111111111;
// 空闲车道
export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000;
export const IdleLane: Lane = /* */ 0b0100000000000000000000000000000;
// 屏幕外车道
export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
lanes 之所以这么设计,主要是方便我们使用位操作来实现一些比较、合并、重置等功能,因为我们可以使用或非和等操作方便的筛选、合并和删除对应的 lane,在 React 的每个阶段中,我们都需要通过 lane 进行一些判定操作。
如果你想详细的了解 Lanes 体系可以看这篇: React 的源码与原理解读(九):Lanes
React 的更新流程
在 React 从我们的代码生成我们的页面和更新我们的页面的过程中,分为 Schedule
【调度】、Reconcile
【协调】、commit
【提交】三个阶段:
-
Schedule
阶段用于通过 lane 调度任务的优先级,使高优先级任务优先进入Reconcile
,并且提供中断和恢复机制。 -
reconcile
阶段用于fiber
树结构的更新,它深度优先递归了整个 React 结构树,为每一个节点创建 Fiber,通过 diff算法 和当前的结构进行对比,判断对应节点需要进行了新增,修改,删除或者直接复用,并在 Fiber 上给他们打上对应的标记 -
commit
阶段又称render
【渲染】阶段,顾名思义它做的就是把刚刚在 Fiber 上做的标记内容同步到对应的真实 DOM 中去,这个过程又分为三个阶段:before mutation
阶段 (dom
操作之前)、mutation
阶段 (dom
操作)、layout
阶段 (dom
操作之后)
从 JSX 代码开始
在了解了一些基础知识和 React 的整体运行层次后,我们来完整的梳理一次从我们的代码到最后成为 HTML 的过程中发生了什么,我们还是从我们编写的代码开始讲起:
在 React 中,对于用户而言,我们一般都编写对应的 jsx 代码来生成我们的组件
function App() {return <h1 class="test" key="122" >Hello World</h1>;
}
但是在 React 中 jsx 代码是不能直接被处理的, jsx 代码需要被转变为对应的 jsx 结构(也就是之前版本的 React.createElement 函数),这个过程由 Raect 和 babel 合作完成,jsx 函数的第一个参数是类型,第二个是配置项和子元素,第三个是 key
function App() {return jsx("h1", {class: "test",children: "Hello World"}, "122");
}
而 jsx 这个函数的作用是使用将传入的参数生成一个 ReactElement 结构,而这个结构可以被我们的 React 进行处理,关于这个 ReactElement 的结构的具体信息可以看这篇:React 的源码与原理解读(一):从创建React元素出发
export function jsx(type, config, maybeKey) {let propName;const props = {};let key = null;let ref = null;// 若设置了key,则使用该keyif (maybeKey !== undefined) {if (__DEV__) {checkKeyStringCoercion(maybeKey);}key = '' + maybeKey;}// 若config中设置了key,则使用config中的keyif (hasValidKey(config)) {if (__DEV__) {checkKeyStringCoercion(config.key);}key = '' + config.key;}// 提取设置的ref属性if (hasValidRef(config)) {ref = config.ref;}// 剩余属性将添加到新的props对象中for (propName in config) {if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {props[propName] = config[propName];}}if (type && type.defaultProps) {const defaultProps = type.defaultProps;for (propName in defaultProps) {if (props[propName] === undefined) {props[propName] = defaultProps[propName];}}}return ReactElement(type, key, ref, undefined, undefined, ReactCurrentOwner.current, props);
}
Render 挂载
之后,当我们编写完了我们的代码后,我们会把它挂载到我们的 Root 上,类似于这样:
import ReactDOM from 'react-dom/client';
import App from './App';const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
createRoot
这个函数的作用是:
- 创建我们的 FiberRootNode 节点,FiberRootNode 我们在上文提过了它用于指向我们正在运行的 Fiber 树,由于我们提供了用于挂载的 DOM 节点,那么我们的 FiberRootNode 就可以完成初始化了,它会指向我们的提供的 DOM 所生成的 Fiber ,同时,我们用一个随机的标记来唯一标识 DOM 已经被使用了,这样当有其他的组件被挂载在这个 root 上时会被报错。
- 获取并监听该 DOM 的所有事件,在监听的过程中,通过
getEventPriority
这个函数,将传入的事件根据类别设定了 Lane ,而这些 Lane 会在后续调度的时候发挥作用。 - 最后返回一个 ReactDOMRoot 对象,它提供一个
render
和一个unmount
函数,用于我们挂载和卸载我们的组件,render
中有一个updateContainer
函数,这个函数中完成我们整个组件的解析和生成 DOM 树的逻辑
想要了解这部分的详细逻辑可以看这篇:React 的源码与原理解读(三):从 Render 开始,理解元素的挂载和解析
更新列表 updateQueue
因为后面需要进入我们的 Schedule
调度阶段,我们先来讲讲一些它需要的前置内容:
因为 Schedule
主要负责任务的调度,我们需要将我们所有的更新任务保存起来,这些任务被称为 update,它的数据结构如下:
const update: Update<*> = {eventTime, // 当前操作的时间lane, // 优先级tag: UpdateState, // 执行的操作payload: null, //更新挂载的数据callback: null, //更新的回调函数next: null, // next指针
};
因为在进入 Schedule
的时候,由他来决定我们的谁会被调度,还会决定任务中断和继续,所以我们就需要一个数据结构来保存这些任务,,他就是 updateQueue
更新队列,每一个 fiber 节点都有一个 updateQueue 的更新队列,它的数据结构如下:
export type UpdateQueue<State> = {|baseState: State, // 当前 state (props)firstBaseUpdate: Update<State> | null, // 上次渲染时遗的链表头节点lastBaseUpdate: Update<State> | null, // 上次渲染时遗的链表尾节点shared: SharedQueue<State>, // 本次渲染时要执行的任务,effects: Array<Update<State>> | null, // 有回调函数的update
};
其中有两个链表,一个 firstBaseUpdate
和 lastBaseUpdate
组成的链表,也就是遗留链表,因为我们每次调度时都会判断当前任务是否有足够的优先级来执行,若优先级不够,则重新存储到链表中,用于下次渲染时重新调度;一个是 shared 中存储的更新链表,里面存放了本次需要执行的任务。
对应的我们使用 enqueueUpdate
函数将我们的 update 函数添加到 UpdateQueue 中;使用 processUpdateQueue
来执行其中可以执行的任务,这两个函数我们在之后的运行过程中会遇到
关于这个 updateQueue 的详细内容可以参考这篇:React 的源码与原理解读(十):updateQueue 与 processUpdateQueue
初始化时创建更新
有了更新相关的基础知识,在我们可以开始解读 updateContainer
这个函数了:
它首先获取了我们的根 Fiber,它在之前的 render 挂载中被创建,
之后创建了一个更新然后把它放到了根 Fiber 的更新列表中,
之后调用 scheduleUpdateOnFiber
对这个任务进行调度
export function updateContainer(element: ReactNodeList,container: OpaqueRoot,parentComponent: ?React$Component<any, any>,callback: ?Function,
): Lane {// 获取 FiberRootNode 的根节点const current = container.current;//创建一个更新const eventTime = requestEventTime();const lane = requestUpdateLane(current);const update = createUpdate(eventTime, lane);update.payload = { element };// 处理 callback,不过从React18开始,render不再传入callback了,即这里的if就不会再执行了callback = callback === undefined ? null : callback;if (callback !== null) {update.callback = callback;}//把创建的添加到 current 的更新链表中enqueueUpdate(current, update, lane);//开始在 fiber 上进行调度const root = scheduleUpdateOnFiber(current, lane, eventTime);if (root !== null) {entangleTransitions(root, current, lane);}return lane;
}
scheduleUpdateOnFiber
函数中,首先我们将这个更新的 lane 合并到 fiber 上,之后寻找这个节点的父节点的 Fiber,自底向上,把这个 lane 放到 childLanes 中,直到同步到根节点为止,之后在 FiberRootNode 中标记这个节点需要更新,最后调用 ensureRootIsScheduled
正式进行调度
export function scheduleUpdateOnFiber(fiber: Fiber,lane: Lane,eventTime: number,
): FiberRoot | null {// 检查是否有循环更新checkForNestedUpdates();// 自底向上更新整个优先级const root = markUpdateLaneFromFiberToRoot(fiber, lane);if (root === null) {return null;}// 标记 root 有更新,将 update 的 lane 插入到 root.pendingLanes 中markRootUpdated(root, lane, eventTime); // 注册调度任务, 由 Scheduler 调度, 进行 Fiber 构造ensureRootIsScheduled(root, eventTime);return root;
}
状态改变时触发更新
除了在初次挂载的时候需要创建更新,我们在页面的内容发生改变的时候,比如 setState 、forceUpdate 等,也会触发更新,当这些 API被触发的时候,他们都会调用 enqueueSetState
函数来创建一个更新,不过根据触发的 API 不同,这些更新会带上不一样的 tag,而从产生不一样的运行效果,enqueueSetState
的运行机制和 updateContainer
大体相同:
Component.prototype.setState = function(partialState, callback) {// ... 省略this.updater.enqueueSetState(this, partialState, callback, 'setState');
};enqueueSetState(inst, payload, callback) {// 获得 fiberconst fiber = getInstance(inst);// 获取当前事件触发的时间const eventTime = requestEventTime();// 获取到当前事件对应的 Laneconst lane = requestUpdateLane(fiber);// 创建更新对象const update = createUpdate(eventTime, lane);// 挂载update.payload = payload;if (callback !== undefined && callback !== null) {//省略 DEV 模式代码update.callback = callback;}// 将更新对象添加进更新队列中enqueueUpdate(fiber, update, lane);const root = scheduleUpdateOnFiber(fiber, lane, eventTime);if (root !== null) {entangleTransitions(root, fiber, lane);} // ....if (enableSchedulingProfiler) {markStateUpdateScheduled(fiber, lane);}
},
Scheduler 调度更新
在 ensureRootIsScheduled
函数中,我们决定了接下来哪一个任务将会被调度:
- 它首先调用了 markStarvedLanesAsExpired 来判断是不是有即将过期的任务,它会先根据任务的 lane 来设定过期时间,然后找出有没有过期任务,如果有则设置高优先级,这样做的目的的防止饥饿
- 之后通过
getNextLanes
和getHighestPriorityLane
选择我们需要的执行的任务中优先级最高的 - 如果现在有执行的任务,判断这个正在执行的任务和新选出的任务哪个优先级更高,如果新任务优先级更高,则取消现在的任务,执行新任务
- 如果执行新任务,先判定我们是不是同步优先级,如果是同步模式我们直接走同步模式进行 reconcile 过程,此模式下调度的任务是不可以被打断的,有这几种情况下是使用的同步模式:1. 任务很紧急的,比如饥饿的任务不能在被延期了 2. 老版本的 React 不支持并发模式的或者没有开始并发模式的就会同步渲染
- 如果不是同步优先级,我们走并发模式进行 reconcile 过程,它可以在运行过程中被打断,我们通过
lanesToEventPriority
将我们的 lane 优先级转为调度的优先级,传入我们的调度函数中,在最新版本的 React 中,默认是使用并发模式进行渲染的。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {const existingCallbackNode = root.callbackNode;/*** 判断 pendingLanes 中的任务,是否有过期的,* 若该任务没有设置过期时间,则根据该任务的lane设置过期时间,* 若任务已过期,则将该任务放到 expiredLanes 中,表示马上就要执行,* 在后续任务执行中以同步模式执行,避免饥饿问题*/markStarvedLanesAsExpired(root, currentTime);// 确定本次执行的 lanesconst nextLanes = getNextLanes(root,root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,);// 如果nextLanes为空则表示没有任务需要执行,则直接中断更新if (nextLanes === NoLanes) {if (existingCallbackNode !== null) {cancelCallback(existingCallbackNode);}root.callbackNode = null;root.callbackPriority = NoLane;return;}// 获取这批任务中优先级最高的一个const newCallbackPriority = getHighestPriorityLane(nextLanes);const existingCallbackPriority = root.callbackPriority;if (existingCallbackPriority === newCallbackPriority) {//....// 若新任务的优先级与现有任务的优先级一样,则继续正常执行之前的任务return;}// 新任务的优先级大于现有的任务优先级,取消现有的任务的执行if (existingCallbackNode != null) {cancelCallback(existingCallbackNode);}// 开始调度任务,判断新任务的优先级是否是同步优先级let newCallbackNode;if (newCallbackPriority === SyncLane) {if (root.tag === LegacyRoot) {scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));} else {scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));}//.....newCallbackNode = null;} else {//不是同步优先级,获取对应的事件优先级let schedulerPriorityLevel;switch (lanesToEventPriority(nextLanes)) {case DiscreteEventPriority:schedulerPriorityLevel = ImmediateSchedulerPriority;break;case ContinuousEventPriority:schedulerPriorityLevel = UserBlockingSchedulerPriority;break;case DefaultEventPriority:schedulerPriorityLevel = NormalSchedulerPriority;break;case IdleEventPriority:schedulerPriorityLevel = IdleSchedulerPriority;break;default:schedulerPriorityLevel = NormalSchedulerPriority;break;}newCallbackNode = scheduleCallback(schedulerPriorityLevel,performConcurrentWorkOnRoot.bind(null, root),);}root.callbackPriority = newCallbackPriority;root.callbackNode = newCallbackNode;
}
在 scheduleCallback
中,我们根据是否配置了 delay
(延迟) ,将任务分化到 taskQueue
(可执行任务) 和 timerQueue
(延时任务)两个队列中,然后根据优先级计算出任务的开始和过期时间;在可执行队列中,过期时间越早的任务优先级越高,因为它需要尽快被执行,而延时任务中,开始时间越早的优先级越高,因为它会尽快开始。
var taskQueue = [];
var timerQueue = [];function unstable_scheduleCallback(priorityLevel, callback, options) {var currentTime = getCurrentTime();//任务开始调度的时间,options 是一个可选项,其中有一个 delay 属性,表示这是一个延时任务,要多少毫秒后再安排执行。 var startTime;if (typeof options === 'object' && options !== null) {var delay = options.delay;if (typeof delay === 'number' && delay > 0) {startTime = currentTime + delay;} else {startTime = currentTime;}} else {startTime = currentTime;}// timeout 跟优先级相互对应,表示这个任务能被拖延执行多久。var timeout;switch (priorityLevel) {//计算 timeout }// expirationTime 表示这个任务的过期时间,这个值越小,说明越快过期,任务越紧急,越要优先执行。var expirationTime = startTime + timeout;// 创建一个任务,其 sortIndex 越小,在排序中就会越靠前var newTask = {id: taskIdCounter++,callback,priorityLevel,startTime,expirationTime,sortIndex: -1,};//如果有设置 delay 时间,那么它就会被放入 timerQueue 中,表示延期执行的任务;否则放入 taskQueue 表示现在就要执行的任务。if (startTime > currentTime) {// 更新 sortIndex 为开始时间,这样越晚的任务开始的任务优先级越低newTask.sortIndex = startTime;push(timerQueue, newTask);if (peek(taskQueue) === null && newTask === peek(timerQueue)) {if (isHostTimeoutScheduled) {cancelHostTimeout();} else {isHostTimeoutScheduled = true;}// 调度requestHostTimeout(handleTimeout, startTime - currentTime);}} else {// 更新 sortIndex 为过期时间,这样越紧急的任务优先级越高newTask.sortIndex = expirationTime;push(taskQueue, newTask);if (enableProfiling) {markTaskStart(newTask, currentTime);newTask.isQueued = true;}if (!isHostCallbackScheduled && !isPerformingWork) {isHostCallbackScheduled = true;// 调度requestHostCallback(flushWork);}}return newTask;
}
在把任务放入我们的队列后,我们调用 requestHostCallback
函数用于执行任务,它使用了 MessageChannel
这个 API 来创建了一个宏任务,因为宏任务会在下次事件循环中执行调用 performWorkUntilDeadline
逻辑:
let isMessageLoopRunning = false;function requestHostCallback(callback) {scheduledHostCallback = callback;if (!isMessageLoopRunning) {isMessageLoopRunning = true;schedulePerformWorkUntilDeadline();}
}
// 初始化了一个 MessageChannel
const channel = new MessageChannel();
const port = channel.port2;
// 当我们调用 schedulePerformWorkUntilDeadline 的时候会触发 performWorkUntilDeadline
channel.port1.onmessage = performWorkUntilDeadline;
let schedulePerformWorkUntilDeadline = () => {port.postMessage(null);
};let startTime = -1;
const performWorkUntilDeadline = () => {//.....
};
而 performWorkUntilDeadline
中则调用了 flushWork
函数通过 workLoop
函数执行我们的任务,这个函数的作用是:
- 我们首先判断在当前时间下,有没有 timerQueue 队列的任务满足了执行时间,如果有的话,我们需要把他们放入到我们的 taskQueue 队列中等待调度
- 之后我们的循环调度队列中的任务执行它,如果它将返回了一个回调函数则说明它被中断了,我们将这个回调函数给予我们当前的任务,等待下次继续执行;否则说明我们的任务执行完毕,我们从队列里面移除我们的任务
- 我们循环执行我们的任务直到任务过期、没有任务或者 shouldYieldToHost 返回 true
- 之后我们做最后的处理,如果还有任务,我们返回 true;否则我们找到下一个的延时队列里的任务(没有可执行任务了,但是还有延时任务需要去执行),调用 requestHostTimeout 函数重新开始我们的调用过程,这个函数就是我们延时任务的调度函数;如果找不到下一个任务,说明没有剩下的任务了,我们返回 false。
function workLoop(hasTimeRemaining, initialTime) {let currentTime = initialTime;// 判断 timerQueue 的 startTime 是不是到了,如果到了将它插入我们的 taskQueue 中advanceTimers(currentTime);// 弹出第一个任务currentTask = peek(taskQueue);//不断执行任务列表里的任务while (currentTask !== null &&!(enableSchedulerDebugging && isSchedulerPaused)) {// 判断是不是要退出本次任务执行if (currentTask.expirationTime > currentTime &&(!hasTimeRemaining || shouldYieldToHost())) {break;}// 获取这个任务的内容const callback = currentTask.callback;if (typeof callback === 'function') {currentTask.callback = null;currentPriorityLevel = currentTask.priorityLevel;// 计算任务是不是过期const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;if (enableProfiling) {markTaskRun(currentTask, currentTime);}//获取任务函数的执行结果const continuationCallback = callback(didUserCallbackTimeout);currentTime = getCurrentTime();if (typeof continuationCallback === 'function') {// 检查callback的执行结果返回的是不是函数,如果返回的是函数,则将这个函数作为当前任务新的回调。currentTask.callback = continuationCallback;if (enableProfiling) {markTaskYield(currentTask, currentTime);}} else {if (enableProfiling) {markTaskCompleted(currentTask, currentTime);currentTask.isQueued = false;}// 任务做完了,抛出这个任务if (currentTask === peek(taskQueue)) {pop(taskQueue);}}advanceTimers(currentTime);} else {pop(taskQueue);}// 下一个任务currentTask = peek(taskQueue);}if (currentTask !== null) {return true;} else {// 找到最近的延时任务const firstTimer = peek(timerQueue);if (firstTimer !== null) {requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);}return false;}
}
shouldYieldToHost
用于我们判定要不要中断我们一批任务的执行,把进程还给我们的浏览器。执行时间如果小于帧间隔时间(frameInterval,通常为 5ms),不需要让出进程,否则让出。关于这个内容的详细过程可以看这篇:React 的源码与原理解读(八):Scheduler
Reconciler 深度优先遍历 Fiber
在说完了我们任务的调度之后,我们来看看任务的执行,之前在 ensureRootIsScheduled
函数中,我们注册了 SyncWork
和ConcurrentWork
两类任务,他们分别调用了自己的performSyncWorkOnRoot
和 performConcurrentWorkOnRoot
函数来执行任务,他们分别调用了一系列函数来处理,以 performSyncWorkOnRoot
为例子
- 它调用了
renderRootConcurrent
函数,其中的逻辑是通过当前的current
树来初始化一个WorkInProgress
树, - 之后它调用了
workLoopConcurrent
函数,其中是我们的遍历逻辑, shouldYield 函数和上文的 shouldYieldToHost 一样,是判定任务是不是调用时间过长而应该被中断的,而workLoopSync
中则没有这个逻辑,因为同步任务不能被打断
function workLoopConcurrent() {while (workInProgress !== null && !shouldYield()) {performUnitOfWork(workInProgress);}
}
function workLoopSync() {while (workInProgress !== null) {performUnitOfWork(workInProgress);}
}
performUnitOfWork
中,我们调用了 beginWork
函数来创建新的 Fiber 节点,同时返回其孩子节点,如果它返回 null ,说明它没有孩子了;如果没有孩子节点,我们会调用completeUnitOfWork
函数处理,其循环判定当前节点是不是有兄弟节点,如果有则返回其兄弟节点,如果没有兄弟节点了,那么把当前节点设置为其父节点,然后继续循环,遍历的结束条件是当前节点不是空,也就是不是根节点。
function performUnitOfWork(unitOfWork: Fiber): void {//取出 current 树,传入的是 workInProgress 树的 root 节点,所以它的 alternate 指向的是 current 树的 root 节点const current = unitOfWork.alternate;//调用 beginWork 创建 Fiber 节点,它是遍历整个 element 后得到的下一个节点,第一次调用则是我们传入的 element 的根节点let next;if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {startProfilerTimer(unitOfWork);next = beginWork(current, unitOfWork, subtreeRenderLanes);stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);} else {next = beginWork(current, unitOfWork, subtreeRenderLanes);}unitOfWork.memoizedProps = unitOfWork.pendingProps;//如果没有下一个节点了(遍历结束了),那么结束整个流程,否则将 workInProgress 更新为刚刚新建的那个节点if (next === null) {completeUnitOfWork(unitOfWork);} else {workInProgress = next;}ReactCurrentOwner.current = null;
}function completeUnitOfWork(unitOfWork: Fiber): void {let completedWork = unitOfWork;do {const current = completedWork.alternate;const returnFiber = completedWork.return; // 该节点的父级节点//省略....//如果当前节点还有兄弟节点,获得兄弟const siblingFiber = completedWork.sibling;if (siblingFiber !== null) {//继续执行我们的 beginWorkworkInProgress = siblingFiber;return;}//返回父节点completedWork = returnFiber;workInProgress = completedWork;} while (completedWork !== null);//遍历结束,标记遍历完成if (workInProgressRootExitStatus === RootInProgress) {workInProgressRootExitStatus = RootCompleted;}
}
performUnitOfWork
和 completeUnitOfWork
合作完成了一个深度优先搜索的逻辑,关于更详细的逻辑可以看这一篇文章:React 的源码与原理解读(四):updateContainer 内如何通过深度优先搜索构造 Fiber 树
Reconciler 生成 Fiber 节点
在 beiginWork
这个函数中,我们首先根据传入的 props 是不是相同,判定是不是可以复用我们之前的 Fiber,这也是我们使用双缓冲架构的原因,在强制更新模式和 props 不一样的情况下,标记为需要更新,否则标记为不需要更新,直接复用之前的节点,要注意如果是第一次调用生成则我们不存在 current 树,所以一定需要更新。在处理完更新逻辑后,我们对每一个不同类型的节点需要一个单独的处理:
- 当节点是
HostRoot
类型的时候,我们则需要把 current 树的的 updateQueue 队列复制给 workInProgress 树,然后使用processUpdateQueue
执行其中的更新,我们在之前创建更新的时候说明了,在 Update 的 payload 中存放了它指向的 element 元素,所以我们可以拿到之前的 element,在更新完毕后对比之前的 element 和更新完毕的 element,相同则复用,不同则使用新的 element 调用reconcileChildren
来创建一个 Fiber - 当节点是
FunctionComponent
也就是函数组件,我们需要先挂载 hooks 和 context(这两个部分可以查看作者之前的 hooks 专题,有很详细的讲解),之后我们获取函数组件的函数,它在 type 中,如果可以复用则直接复用节点,如果不能在调用renderWithHooks
运行我们函数组件的函数,将返回值传入reconcileChildren
来生成我们的 Fiber - 当我们传入是类组件的时候,我们先通过 stateNode 属性获取到组件的实例,如果没有则创建一个实力,因为类组件本身就是一个类,显然可以 new 一个它的实例出来,之后我们通过 context、props 和 state 我们计算出新的 state 更新给节点,然后执行其生命周期例如
componentWillMount
,在一切前置操作完毕后,通过实例的 render 方法获取到了其返回的 DOM 元素,传入reconcileChildren
方法中。 - 如果你传入一个原生的 HTML 节点,它会判定它是不是文本节点,如果是则停止后续的处理,否则传入
reconcileChildren
;如果你传入的是文本节点,那么直接停止后续的处理。 - 有一种特殊情况是,因为 React 中的函数组件有多种写法,有些形似函数组件的也可能是类组件,为了保险起见,函数组件会先被设定为
IndeterminateComponent
,然后在对这个类型的处理中,会识别你写的组件是函数组件还是类组件。
这部分的逻辑总结来说就是,首先判断当前节点是不是复用:如果可以直接使用;否则先处理 state、hooks 等信息,然后调用 reconcileChildren
来生成我们的 Fiber 节点,因为篇幅很长,这里我们不具体展开了,感兴趣的可以去阅读这篇:React 的源码与原理解读(五):beginWork 分类处理逻辑
这部分的重点是 reconcileChildren
函数,其中调用了了 DIFF 算法来判断节点的更新,它的主要逻辑在 reconcileChildFibers
中,它处理我们放入的 element 结构,我们把他和 Fiber 树中同一层的 Fiber 进行比较,主要使用三个函数进行处理:
( PS: 要注意,下文我们所说的新增,删除和移动等操作,我们只是在 Fiber 上打上了标记,而不是真的进行了相关的操作,这些标记会在之后的 commit 进行具体的处理 )
reconcileSingleElement
用于处理一般的React组件,比如函数组件、类组件、html 标签等,
- 首先提取出当前 element 的 key 属性,找到在 Fiber 树同一层中,有没有和他一个 key 的元素
- 之后我们判断这个 key 相同的元素和当前元素的类型是不是一致,如果一致则复用这个元素
- 如果没有匹配元素则创建一个新的节点
function reconcileSingleElement(returnFiber: Fiber,currentFirstChild: Fiber | null,element: ReactElement,lanes: Lanes,
): Fiber {//当前节点的keyconst key = element.key;let child = currentFirstChild;// 循环检测一层中和当前节点的 keywhile (child !== null) {// 找到 key 相等的元素if (child.key === key) {const elementType = element.type;//元素类型相等if (child.key === key) {const elementType = element.type;// REACT_FRAGMENT_TYPE,特判if (elementType === REACT_FRAGMENT_TYPE) {if (child.tag === Fragment) {deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用Fiber子节点且确认只有一个子节点,因此标记删除掉该child节点的所有sibling节点const existing = useFiber(child, element.props.children); // 该节点是fragment类型,则复用其childrenexisting.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点//Fragment没有 ref属性if (__DEV__) {existing._debugSource = element._source;existing._debugOwner = element._owner;}return existing;}} else {if (child.elementType === elementType ||(__DEV__? isCompatibleFamilyForHotReloading(child, element): false) ||(typeof elementType === 'object' &&elementType !== null &&elementType.$$typeof === REACT_LAZY_TYPE &&resolveLazy(elementType) === child.type)) {deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用Fiber子节点且确认只有一个子节点,因此标记删除掉该child节点的所有sibling节点const existing = useFiber(child, element.props); // 复用 child 节点和 element.props 属性existing.ref = coerceRef(returnFiber, child, element); // 处理refexisting.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点if (__DEV__) {existing._debugSource = element._source;existing._debugOwner = element._owner;}return existing;}}// key一样,类型不同,直接删除该节点和其兄弟节点deleteRemainingChildren(returnFiber, child);break;} else {// 若key不一样,不能复用,标记删除当前单个child节点deleteChild(returnFiber, child);}// 指针指向下一个sibling节点child = child.sibling; }// 创建一个新的fiber节点if (element.type === REACT_FRAGMENT_TYPE) {// REACT_FRAGMENT_TYPE,特判const created = createFiberFromFragment(element.props.children, returnFiber.mode, lanes, element.key);created.return = returnFiber; // 新节点的 return 指向到父级节点// FRAGMENT 节点没有 refreturn created;} else {// 普通的html元素、函数组件、类组件等// 从 element 创建 fiber 节点const created = createFiberFromElement(element, returnFiber.mode, lanes);created.ref = coerceRef(returnFiber, currentFirstChild, element); // 处理refcreated.return = returnFiber;return created;}
}
对于一个纯 html 文本节点,我们调用 reconcileSingleTextNode
这个函数,因为在比较当前一层的数据时,文本节点不能有兄弟节点,所以只有当 current 的第一个结点是文本节点时才能复用,否则就删除所有元素。
// 调度文本节点
function reconcileSingleTextNode(returnFiber: Fiber,currentFirstChild: Fiber | null,textContent: string,lanes: Lanes,
): Fiber {// 不再判断文本节点的key,因为文本节点就来没有keyif (currentFirstChild !== null && currentFirstChild.tag === HostText) {// 若当前节点是文本,则直接删除后续的兄弟节点deleteRemainingChildren(returnFiber, currentFirstChild.sibling);const existing = useFiber(currentFirstChild, textContent); // 复用这个文本的fiber节点,重新赋值新的文本existing.return = returnFiber;return existing;}// 若不存在子节点,或者第一个子节点不是文本节点,直接将当前所有的节点都删除,然后创建出新的文本fiber节点deleteRemainingChildren(returnFiber, currentFirstChild);const created = createFiberFromText(textContent, returnFiber.mode, lanes);created.return = returnFiber;return created;
}
当我们需要处理的元素不是单个数据,而是一组数据的时候,我们使用reconcileChildrenArray
进行处理:
- 我们先按照顺序遍历 Fiber 链表和我们的数组,使用
updateSlot
函数判定在相同的对应位置的两个元素是不是能够复用,它判断的依据还是两个元素的 key 是不是相同 - 如果循环结束之后,旧的链表还没遍历完,说明剩下的节点已经不需要了,直接删除即可
- 如果经过上面操作后,旧的 Fiber 用完了,但 element 元素没有全部访问到,说明剩下的元素没有对应的可以复用的节点,直接新建节点即可
- 如果新旧元素都没遍历完,说明出现了元素乱序的情况,我们需要把旧节点放到 Map 中,然后根据 key 或者 index 获取。
let resultingFirstChild: Fiber | null = null; // 用于返回的链表
let previousNewFiber: Fiber | null = null; let oldFiber = currentFirstChild; // 旧 Fiber 链表的节点,开始指向同一层中的第一个节点
let lastPlacedIndex = 0; // 表示当前已经新建的 Fiber 长度
let newIdx = 0; // 表示遍历 newChildren 的索引指针
let nextOldFiber = null; // 下一个 fiber 节点for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {// 如果旧的节点大于新的if (oldFiber.index > newIdx) {nextOldFiber = oldFiber;oldFiber = null;} else {// 旧 fiber 的索引和n ewChildren 的索引匹配上了,获取 oldFiber 的下一个兄弟节点nextOldFiber = oldFiber.sibling;}// 比较旧的节点和将要转换的 element const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);// 匹配失败,不能复用if (newFiber === null) {if (oldFiber === null) {oldFiber = nextOldFiber;}break;}if (shouldTrackSideEffects) {if (oldFiber && newFiber.alternate === null) {// newFiber 不是基于 oldFiber 的 alternate 创建的,销毁旧节点deleteChild(returnFiber, oldFiber);}}// 更新lastPlacedIndexlastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);// 更新返回的链表if (previousNewFiber === null) {// 若整个链表为空,则头指针指向到newFiberresultingFirstChild = newFiber;} else {// 若链表不为空,则将newFiber放到链表的后面previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber; oldFiber = nextOldFiber; // 继续下一个节点
}// 遍历结束(访问相同数量的元素了)
if (newIdx === newChildren.length) {// 删除旧链表中剩余的节点deleteRemainingChildren(returnFiber, oldFiber);// 返回新链表的头节点指针return resultingFirstChild;
}// 若旧数据中所有的节点都复用了
if (oldFiber === null) {//创建新元素for (; newIdx < newChildren.length; newIdx++) {const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);if (newFiber === null) {continue;}lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//拼接链表if (previousNewFiber === null) {resultingFirstChild = newFiber;} else {previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;}return resultingFirstChild;
}
//如果新旧元素都没遍历完, mapRemainingChildren 生成一个以 oldFiber 的 key 为 key, oldFiber 为 value 的 map
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);for (; newIdx < newChildren.length; newIdx++) {// 从 map 中查找是否存在可以复用的fiber节点,然后生成新的fiber节点const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);if (newFiber !== null) {if (shouldTrackSideEffects) {if (newFiber.alternate !== null) {//newFiber.alternate指向到current,若current不为空,说明复用了该fiber节点,这里我们要在 map 中删除,因为后面会把 map 中剩余未复用的节点删除掉的existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);}}lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);// 接着之前的链表进行拼接if (previousNewFiber === null) {resultingFirstChild = newFiber;} else {previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;}
}if (shouldTrackSideEffects) {// 将 map 中没有复用的 fiber 节点添加到删除队列中,等待删除existingChildren.forEach(child => deleteChild(returnFiber, child));
}
// 返回新链表的头节点指针
return resultingFirstChild;
如果你想要了解更加详细的整体过程,你可以看这篇文章::React 的源码与原理解读(六):reconcileChildren 与 DIFF 算法
Reconciler DIFF 算法
上述的就是 Reconciler 的 DIFF 逻辑,我们来梳理一下整个流程:
React 只对虚拟 DOM 树进行分层比较,不考虑节点的跨层级比较。如此只需要遍历一次虚拟 Dom 树,就可以完成整个的对比。对比时,每次都选择同一个父节点的孩子进行比较,如下图,同一个父亲节点下的所有孩子会进行比较
如果出现的跨层移动的情况,那么在父亲节点的比较时,就会删除跨层移动的节点,比如下图,在对root 进行比较的时候,A已经被标记为删除了,之后在对 B 进行比较的时候,虽然组件 A 出现了,但是我们并不会复用它,而会新建一个组件A
React 对于不同类型的组件,默认不需要进行比较操作,直接重新创建。对于同类型组件,使用 diff 策略进行比较,比如下图:两个组件的根节点不同,也就是说不是一个组件,但是组件的内容相同,这种情况下,React 并不会进行复用,而是直接新建:
对于位于同层的节点,通过唯一 key 来判断老集合中是否存在相同的节点,如果没有则创建;如果有,则判断是否需要进行移动操作。整个操作分为:删除、新增、移动 三种。如下图:我们对 A 进行了移动,对 C 进行了删除,对 E 进行了 新增。
在 React 中这样处理可以提升效率的原因是:
- 在 web 开发中,很少会有跨层级的元素变化,,比如现在我有一个 p 标签,我经常做的操作是:改变这个标签的内容,或者在 p 标签的同一级再插入一个 p 标签,我们很少有在更新页面的时候在 p 标签的外围嵌套一个 div 这样的操作。
- 两个不同类型的组件会产生两棵不同的树形结构
- React 中的 key 是唯一的,一个 key 可以唯一标识一个元素
因而经过上述逻辑的优化,React 把经典 DIFF 算法(暴力递归比较)的 O( n^3 ) 优化到了近乎 O( n ) 。
Commit 提交更新
在经过了 DIFF 的处理之后,我们的新的 Fiber 树已经生成了,但是我们的真实 DOM 还没有更新,展示给用户的页面还是原本的样子,现在我们就需要将刚刚 DIFF 出来的更新同步到真实 DOM 上,这个阶段就是 commit 阶段,它在 performSyncWorkOnRoot
和 performConcurrentWorkOnRoot
函数中进入:
export function performSyncWorkOnRoot(root: FiberRoot): null {let exitStatus = renderRootSync(root, lanes);// 生成完毕,返回我们的 WorkInProgress 树的根节点//....const finishedWork: Fiber = (root.current.alternate: any);root.finishedWork = finishedWork;root.finishedLanes = lanes;// 进入 commit 阶段commitRoot(root,workInProgressRootRecoverableErrors,workInProgressTransitions,);ensureRootIsScheduled(root);return null;
}
这个阶段总的来说它可以分为五个部分来进行分析讲解:
首先是 before mutation
阶段之前,我们主要进行一些准备工作:
- 首先是处理之前遗留下来的 effect 任务,因为这些任务执行过程中可能被高优先级的任务中断,但是他们未执行会影响我们的 commit,具体可以查看 useEffect 那节的讲解。
- 之后是判断我们这次调度完成了哪些 lane 上的任务,把他们收集起来,从我们的 lanes 列表中删除,表示这些任务完成了,然后重置我们的 FiberRoot 上的标志位
- 最后是开始了一个处理 Effect 相关内容的调度,注意这里只是开始了调度,它正在执行并不在这里,具体可以查看 useEffect 那节的讲解。
// 调用flushPassiveEffects执行完所有effect的任务do {flushPassiveEffects();} while (rootWithPendingPassiveEffects !== null);// 判断哪些任务完成了const finishedWork = root.finishedWork;const lanes = root.finishedLanes;if (enableSchedulingProfiler) {markCommitStarted(lanes);}if (finishedWork === null) {// 异常处理}//重置 FiberRoot root.finishedWork = null;root.finishedLanes = NoLanes;root.callbackNode = null;root.callbackPriority = NoLane;// 重置全局变量 if (root === workInProgressRoot) {workInProgressRoot = null;workInProgress = null;workInProgressRootRenderLanes = NoLanes;} else {}// 处理 useEffect相关内容if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||(finishedWork.flags & PassiveMask) !== NoFlags) {if (!rootDoesHavePassiveEffects) {rootDoesHavePassiveEffects = true;pendingPassiveEffectsRemainingLanes = remainingLanes;pendingPassiveTransitions = transitions;scheduleCallback(NormalSchedulerPriority, () => {flushPassiveEffects();return null;});}}
之后是进入了 before mutation
阶段,这个阶段主要做一些前期准备工作,它遍历了整个 Fiber 树,做了以下操作:
- 首先它触发了
getSnapShotBeforeUpdate
快照机制这个生命周期 - 处理 DOM 渲染/删除后的
autoFocus
、blur
等逻辑
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {const current = finishedWork.alternate;const flags = finishedWork.flags;// ...focus blur相关if ((flags & Snapshot) !== NoFlags) {setCurrentDebugFiberInDEV(finishedWork);switch (finishedWork.tag) {case FunctionComponent:case ForwardRef:case SimpleMemoComponent: {break;}case ClassComponent: {if (current !== null) {const prevProps = current.memoizedProps;const prevState = current.memoizedState;const instance = finishedWork.stateNode;//触发 getSnapshotBeforeUpdateconst snapshot = instance.getSnapshotBeforeUpdate(finishedWork.elementType === finishedWork.type? prevProps: resolveDefaultProps(finishedWork.type, prevProps),prevState,);instance.__reactInternalSnapshotBeforeUpdate = snapshot;}break;}case HostRoot: {if (supportsMutation) {const root = finishedWork.stateNode;clearContainer(root.containerInfo);}break;}//....}resetCurrentDebugFiberInDEV();}
}
之后是进入了 MutationEffects
这个阶段,这个阶段我们就需要将我们的修改同步到我们的 DOM 上了,这个阶段不同类型的 Fiber 会有不同的操作,但是也有都需要调用的部分,我们先看总体的逻辑:
首先是调用了 recursivelyTraverseMutationEffects
进行删除操作,对于需要删除的节点,我们这样操作:
- 先将它 ref 指向的元素设置为 null
- 之后对于类组件调用它的
componentWillUnmount
生命周期函数,函数组件则处理它的useLayoutEffect
的销毁逻辑,具体可以查看 useEffect 那节的讲解。 - 如果是原生的 HTML 节点,我们调用 removeChild 方法删除真实 DOM
- 最后向下遍历子节点,执行删除逻辑
function commitDeletionEffectsOnFiber(finishedRoot: FiberRoot,nearestMountedAncestor: Fiber,deletedFiber: Fiber,
) {onCommitUnmount(deletedFiber);switch (deletedFiber.tag) {case HostComponent: {if (!offscreenSubtreeWasHidden) {// ref 设置回 nullsafelyDetachRef(deletedFiber, nearestMountedAncestor);}}case HostText: {if (supportsMutation) {const prevHostParent = hostParent;const prevHostParentIsContainer = hostParentIsContainer;hostParent = null;// 往下遍历子节点,执行删除recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);hostParent = prevHostParent;hostParentIsContainer = prevHostParentIsContainer;// 删除真正的 DOM,调用了原生的 removeChild 方法if (hostParent !== null) {if (hostParentIsContainer) {removeChildFromContainer(((hostParent: any): Container),(deletedFiber.stateNode: Instance | TextInstance),);} else {removeChild(((hostParent: any): Instance),(deletedFiber.stateNode: Instance | TextInstance),);}}} else {recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);}return;}// ....// 函数组件case FunctionComponent:case ForwardRef:case MemoComponent:case SimpleMemoComponent: {if (!offscreenSubtreeWasHidden) {// 读取 updateQueue 队列,队列用链表的方式保存const updateQueue: FunctionComponentUpdateQueue | null = (deletedFiber.updateQueue: any);if (updateQueue !== null) {const lastEffect = updateQueue.lastEffect;if (lastEffect !== null) {const firstEffect = lastEffect.next;let effect = firstEffect;do {const {destroy, tag} = effect;if (destroy !== undefined) {if ((tag & HookInsertion) !== NoHookEffect) {// 处理 useInsertionEffect 副作用safelyCallDestroy(deletedFiber,nearestMountedAncestor,destroy,);} else if ((tag & HookLayout) !== NoHookEffect) {// 处理 useLayoutEffect 副作用if (enableSchedulingProfiler) {markComponentLayoutEffectUnmountStarted(deletedFiber);}if (enableProfilerTimer &&enableProfilerCommitHooks &&deletedFiber.mode & ProfileMode) {startLayoutEffectTimer();safelyCallDestroy(deletedFiber,nearestMountedAncestor,destroy,);recordLayoutEffectDuration(deletedFiber);} else {safelyCallDestroy(deletedFiber,nearestMountedAncestor,destroy,);}if (enableSchedulingProfiler) {markComponentLayoutEffectUnmountStopped();}}}effect = effect.next;} while (effect !== firstEffect);}}}// 遍历子节点执行删除逻辑recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);return;}// 类组件case ClassComponent: {if (!offscreenSubtreeWasHidden) {// 移除 refsafelyDetachRef(deletedFiber, nearestMountedAncestor);const instance = deletedFiber.stateNode;if (typeof instance.componentWillUnmount === 'function') {// 调用类组件实例的 componentWillUnmount 方法safelyCallComponentWillUnmount(deletedFiber,nearestMountedAncestor,instance,);}}//遍历子节点执行删除逻辑recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);return;}// ....省略default: {recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);return;}}
}
另一个是插入的逻辑 commitReconciliationEffects
:
- 判断节点的父 Fiber 是不是需要重置,如果是则调用
parent.textContent = ''
来重置它的内容 - 之后寻找它有没有兄弟,如果有则调用
insertBefore
方法将内容插入到兄弟节点之前,如果没有,则调用父节点的appendChild
进行插入
function commitPlacement(finishedWork: Fiber): void {if (!supportsMutation) {return;}//获取父 fiberconst parentFiber = getHostParentFiber(finishedWork);switch (parentFiber.tag) {case HostComponent: {const parent: Instance = parentFiber.stateNode;// 判断父 fiber 是否有 ContentReset(内容重置)标记if (parentFiber.flags & ContentReset) {// 通过 parent.textContent = '' 的方式重置resetTextContent(parent);parentFiber.flags &= ~ContentReset;}// 找它的下一个兄弟 DOM 节点,const before = getHostSibling(finishedWork);// 如果存在,用 insertBefore 方法;如果没有,就调用原生的 appendChild 方法insertOrAppendPlacementNode(finishedWork, before, parent);break;}case HostRoot:case HostPortal: {const parent: Container = parentFiber.stateNode.containerInfo;const before = getHostSibling(finishedWork);insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);break;}default:throw new Error('Invalid host parent fiber. This error is likely caused by a bug ' +'in React. Please file an issue.',);}
}
最后回到 commitMutationEffectsOnFiber
再进行分类处理:
- 对于我们的类组件,我们不做处理
- 对于我们的函数组件,我们处理 useInsertionEffect 的全部逻辑,useLayoutEffect 的销毁逻辑
- 对于原生组件,如果可以复用,我们调用 commitUpdate 执行更新逻辑,否则直接重置这个原生节点
- 如果是原生文本,我们调用 commitTextUpdate 函数,更新其文本内容
switch (finishedWork.tag) {case FunctionComponent:case ForwardRef:case MemoComponent:case SimpleMemoComponent: {// ....if (flags & Update) {try {// 找出 useInsertionEffect 的 destroy 方法去调用commitHookEffectListUnmount(HookInsertion | HookHasEffect,finishedWork,finishedWork.return,);// 执行 useInsertionEffect 的回调函数,并将返回值保存到 effect.destory 里。commitHookEffectListMount(HookInsertion | HookHasEffect,finishedWork,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}if (enableProfilerTimer &&enableProfilerCommitHooks &&finishedWork.mode & ProfileMode) {try {startLayoutEffectTimer();// 执行 useLayoutEffect 对应的 destroy 方法commitHookEffectListUnmount(HookLayout | HookHasEffect,finishedWork,finishedWork.return,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}recordLayoutEffectDuration(finishedWork);} else {try {commitHookEffectListUnmount(HookLayout | HookHasEffect,finishedWork,finishedWork.return,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}}return;}// 类组件不会进行操作case ClassComponent: {// ...if (flags & Ref) {if (current !== null) {safelyDetachRef(current, current.return);}}return;}//原生组件case HostComponent: {//...if (flags & Ref) {if (current !== null) {safelyDetachRef(current, current.return);}}if (supportsMutation) {// 判断是不是需要重置if (finishedWork.flags & ContentReset) {const instance: Instance = finishedWork.stateNode;try {resetTextContent(instance);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}// 判断是不是需要更新if (flags & Update) {const instance: Instance = finishedWork.stateNode;if (instance != null) {const newProps = finishedWork.memoizedProps;const oldProps =current !== null ? current.memoizedProps : newProps;const type = finishedWork.type;const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);finishedWork.updateQueue = null;if (updatePayload !== null) {try {// 更新操作commitUpdate(instance,updatePayload,type,oldProps,newProps,finishedWork,);} catch (error) {captureCommitPhaseError(finishedWork,finishedWork.return,error,);}}}}}return;}//原生文本case HostText: {//....if (flags & Update) {if (supportsMutation) {if (finishedWork.stateNode === null) {throw new Error('This should have a text node initialized. This error is likely ' +'caused by a bug in React. Please file an issue.',);}const textInstance: TextInstance = finishedWork.stateNode;const newText: string = finishedWork.memoizedProps;const oldText: string =current !== null ? current.memoizedProps : newText;try {// 更新操作commitTextUpdate(textInstance, oldText, newText);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}}return;}//...
}
再经过这些操作后,我们的真实 DOM 已经生成完毕了,我们也通过改变了 current
的指向来改变了我们当前工作的 Fiber 树了
// 新的树生成完毕了,改变 current 的指向root.current = finishedWork;
所以在 LayoutEffects
这个阶段,我们已经可以调用到我们的真实 DOM 了,这个阶段,我们的主要工作是:
- 如果是函数组件,我们处理它的
useLayoutEffect
的创建逻辑,具体可以查看 useEffect 那节的讲解。 - 如果是类组件,我们需要执行它的
componentDidMount
或者componentDidUpdate
生命周期和状态更新的回调函数 - 如果根节点,我们需要处理其回调,也就是是
ReactDOM.render
的调回函数 - 如果是原生组件,我们处理其自动更新的情况
function commitLayoutEffectOnFiber(finishedRoot: FiberRoot,current: Fiber | null,finishedWork: Fiber,committedLanes: Lanes,
): void {if ((finishedWork.flags & LayoutMask) !== NoFlags) {switch (finishedWork.tag) {case FunctionComponent:case ForwardRef:case SimpleMemoComponent: {if (!enableSuspenseLayoutEffectSemantics ||!offscreenSubtreeWasHidden) {if (enableProfilerTimer &&enableProfilerCommitHooks &&finishedWork.mode & ProfileMode) {try {startLayoutEffectTimer();// 提交 useLayoutEffect commitHookEffectListMount(HookLayout | HookHasEffect,finishedWork,);} finally {recordLayoutEffectDuration(finishedWork);}} else {commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);}}break;}case ClassComponent: {const instance = finishedWork.stateNode;if (finishedWork.flags & Update) {if (!offscreenSubtreeWasHidden) {if (current === null) {if (enableProfilerTimer &&enableProfilerCommitHooks &&finishedWork.mode & ProfileMode) {try {startLayoutEffectTimer();// 首次渲染,触发 componentDidMount 生命周期instance.componentDidMount();} finally {recordLayoutEffectDuration(finishedWork);}} else {instance.componentDidMount();}} else {const prevProps =finishedWork.elementType === finishedWork.type? current.memoizedProps: resolveDefaultProps(finishedWork.type,current.memoizedProps,);const prevState = current.memoizedState;if (enableProfilerTimer &&enableProfilerCommitHooks &&finishedWork.mode & ProfileMode) {try {startLayoutEffectTimer();// 非首次渲染,触发 componentDidUpdate 生命周期instance.componentDidUpdate(prevProps,prevState,instance.__reactInternalSnapshotBeforeUpdate,);} finally {recordLayoutEffectDuration(finishedWork);}} else {instance.componentDidUpdate(prevProps,prevState,instance.__reactInternalSnapshotBeforeUpdate,);}}}}const updateQueue: UpdateQueue<*,> | null = (finishedWork.updateQueue: any);if (updateQueue !== null) {// 执行 commitUpdateQueue 处理回调commitUpdateQueue(finishedWork, updateQueue, instance);}break;}case HostRoot: {const updateQueue: UpdateQueue<*,> | null = (finishedWork.updateQueue: any);if (updateQueue !== null) {let instance = null;if (finishedWork.child !== null) {switch (finishedWork.child.tag) {case HostComponent:instance = getPublicInstance(finishedWork.child.stateNode);break;case ClassComponent:instance = finishedWork.child.stateNode;break;}}// 调用 commitUpdateQueue 处理 ReactDOM.render 的回调commitUpdateQueue(finishedWork, updateQueue, instance);}break;}case HostComponent: {const instance: Instance = finishedWork.stateNode;// commitMount 处理 input 标签有 auto-focus 的情况if (current === null && finishedWork.flags & Update) {const type = finishedWork.type;const props = finishedWork.memoizedProps;commitMount(instance, type, props, finishedWork);}break;}case HostText: break;}case HostPortal: {break;}default:throw new Error('This unit of work tag should not have side-effects. This error is ' +'likely caused by a bug in React. Please file an issue.',);}}
}
在LayoutEffects
这个阶段结束之后,我们将 rootWithPendingPassiveEffects
赋值。根据上面的我们在 commit 的三个阶段开始前,已经把开始了一个 flushPassiveEffects
的调度 Effect 相关内容,而这个调度使用了 flushPassiveEffects
函数,它只有在 rootWithPendingPassiveEffects
不是 null 的情况下才会运行,而此时,我们将其进行赋值,这样就可以开始执行 useEffect
相关副作用的处理,包括其挂载和更新
if (rootDoesHavePassiveEffects) {rootDoesHavePassiveEffects = false;rootWithPendingPassiveEffects = root;pendingPassiveEffectsLanes = lanes;} else {//....}export function flushPassiveEffects(): boolean {if (rootWithPendingPassiveEffects !== null) {// .... 省略优先级和DEV操作try {// 省略return flushPassiveEffectsImpl();} finally {//省略}}return false;
}
至此,我们的整个 React 的运行流程已经讲解完毕了,关于 commit 这一节的更加详细的内容可以查看这一篇:React 的源码与原理解读(七):commit 阶段
Hook 的原理的作用方式
最后我们来讲解以下 hooks 相关的内容,他的作用在 renderWithHooks 函数中,我们之前已经提到了,其中根据 current === null
的判断来决定我们是使用初始化的 hooksDispatcher 还是更新的 hooksDispatcher ,Dispatcher ,其中每一项都包含了我们用到的各种 hooks 的操作函数
export function renderWithHooks<Props, SecondArg>(current: Fiber | null,workInProgress: Fiber,Component: (p: Props, arg: SecondArg) => any,props: Props,secondArg: SecondArg,nextRenderLanes: Lanes,
): any {// 获取当前函数组件对应的 fiber,并且初始化currentlyRenderingFiber = workInProgress;workInProgress.memoizedState = null;workInProgress.updateQueue = null;if (__DEV__) {// ....} else {// 根据当前阶段决定是初始化hook,还是更新hookReactCurrentDispatcher.current =current === null || current.memoizedState === null? HooksDispatcherOnMount: HooksDispatcherOnUpdate;}// 调用函数组件let children = Component(props, secondArg);// 重置hook链表指针currentHook = null;workInProgressHook = null;return children;
}const HooksDispatcherOnMount: Dispatcher = {...useCallback: mountCallback,useContext: readContext,useEffect: mountEffect,useImperativeHandle: mountImperativeHandle,useLayoutEffect: mountLayoutEffect,useMemo: mountMemo,useReducer: mountReducer,useRef: mountRef,useState: mountState,...
};const HooksDispatcherOnUpdate: Dispatcher = {...useCallback: updateCallback,useContext: readContext,useEffect: updateEffect,useImperativeHandle: updateImperativeHandle,useLayoutEffect: updateLayoutEffect,useMemo: updateMemo,useReducer: updateReducer,useRef: updateRef,useState: updateState,...
};
之后我们看到每一个 mount 和 update 逻辑,他们分别都调用了 mountWorkInProgressHook
和 updateWorkInProgressHook
进行操作,前者用于创建一个 hook 元素,挂载到 Fiber 的 hook 链表上;而当我们要更新一个 hooks 的时候,我们首先获取两棵树的下一个 hook, 我们用 nextCurrentHook
和 nextWorkInProgressHook
来标识下一个需要操作的 hooks 。如果 nextWorkInProgressHook
已经存在,我们直接使用它即可,否则,我们需要从我们的 nextCurrentHook
处克隆一份我们的 hook 放入其中
function mountWorkInProgressHook(): Hook {const hook: Hook = {memoizedState: null, // 上次渲染时所用的 statebaseState: null, // 已处理的 update 计算出的 statebaseQueue: null, // 未处理的 update 队列(上一轮没有处理完成的)queue: null, // 当前的 update 队列next: null, // 指向下一个hook};// 保存到链表中if (workInProgressHook === null) {currentlyRenderingFiber.memoizedState = workInProgressHook = hook;} else {workInProgressHook = workInProgressHook.next = hook;}return workInProgressHook;
}function updateWorkInProgressHook(): Hook {// 获取 current 树上的 Fiber 的 hook 链表let nextCurrentHook: null | Hook;if (currentHook === null) {const current = currentlyRenderingFiber.alternate;if (current !== null) {nextCurrentHook = current.memoizedState;} else {nextCurrentHook = null;}} else {nextCurrentHook = currentHook.next;}// workInProgress 树上的 Fiber 的 hook 链表let nextWorkInProgressHook: null | Hook;if (workInProgressHook === null) {nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;} else {nextWorkInProgressHook = workInProgressHook.next;}// 如果 nextWorkInProgressHook 不为空,直接使用if (nextWorkInProgressHook !== null) {workInProgressHook = nextWorkInProgressHook;nextWorkInProgressHook = workInProgressHook.next;currentHook = nextCurrentHook;} else {//否则我们克隆一份 hookscurrentHook = nextCurrentHook;const newHook: Hook = {memoizedState: currentHook.memoizedState,baseState: currentHook.baseState,baseQueue: currentHook.baseQueue,queue: currentHook.queue,next: null,};if (workInProgressHook === null) {currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;} else {workInProgressHook = workInProgressHook.next = newHook;}}return workInProgressHook;
}
这部分的详细讲解可以查看这篇 React 的源码与原理解读(十一):hooks 的原理
useCallback
useCallback
用于优化代码, 可以让对应的函数只有在依赖发生变化时才重新定义:
useCallback(fn, dependencies)
mountCallback
的原理很简单,如果是第一次运行,我们获取传入的 callback 和 deps,直接把数据缓存到 memoizedState 即可,之后我们返回我们的 callback ,更新它则是将我们缓存的 deps 和传入的进行比较,如果相同则返回我们缓存的 callback,否则把新的 callback 和 deps 进行缓存:
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps;hook.memoizedState = [callback, nextDeps]; return callback;
}function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {const hook = updateWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;const prevState = hook.memoizedState; if (prevState !== null) {if (nextDeps !== null) {const prevDeps: Array<mixed> | null = prevState[1];if (areHookInputsEqual(nextDeps, prevDeps)) {return prevState[0];}}}hook.memoizedState = [callback, nextDeps];return callback;
}
useMemo
useMemo
与 useCallback
的功能很像,只不过 useMemo
用来缓存函数执行的结果
const cachedValue = useMemo(calculateValue, dependencies)
它们的源码也很类似,和 useCallback
唯一的区别是,我们需要通过传入的 nextCreate
函数算出结果,然后将结果进行缓存:
function mountMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps;const nextValue = nextCreate();hook.memoizedState = [nextValue, nextDeps]; return nextValue;
}function updateMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {const hook = updateWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;const prevState = hook.memoizedState;if (prevState !== null) {if (nextDeps !== null) {const prevDeps: Array<mixed> | null = prevState[1];if (areHookInputsEqual(nextDeps, prevDeps)) {return prevState[0];}}}const nextValue = nextCreate();hook.memoizedState = [nextValue, nextDeps];return nextValue;
}
关于这两个 hooks 的详细解读和用法讲解可以看这篇:React 的源码与原理解读(十二):Hooks解读之一 useCallback&useMemo
useRef
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
),返回的 ref 对象在组件的整个生命周期内持续存在,我们也可以使用 useRef
去保存对 DOM
对象的引用。
const refContainer = useRef(initialValue);
useRef
的源码非简洁:
-
mountRef
获取我们传入的初始值,然后把它存到我们的memoizedState
中,因为我们返回了我们的 ref,它是一个对象,也就是引用类型,所以我们可以直接修改这个 ref 值,它也会作用在memoizedState
上 -
updateRef
则是返回我们的memoizedState
function mountRef<T>(initialValue: T): {| current: T |} {const hook = mountWorkInProgressHook();// 存储数据,并返回这个数据const ref = { current: initialValue };hook.memoizedState = ref;return ref;
}function updateRef<T>(initialValue: T): {| current: T |} {const hook = updateWorkInProgressHook();return hook.memoizedState;
}
而关于它操作 DOM 的能力,则是在解析为 React 节点的时候,成为了节点上的 ref 属性
const element = {$$typeof: REACT_ELEMENT_TYPE, type: type, key: key, ref: ref, //对组件实例的引用props: props, _owner: owner
}
在 beginWork
这个函数中,我们调用了 markRef
这个函数,它的作用是告诉我们的 React ,这个fiber 节点对应的 DOM 被 ref 了,我们通过 effectTag
中的一个标志位来标识我们的这个 fiber 有没有 ref,这些用于我们之后的逻辑判定要不要进行 ref 的相关操作。
function markRef(current: Fiber | null, workInProgress: Fiber) {const ref = workInProgress.ref;if ((current === null && ref !== null) ||(current !== null && current.ref !== ref)) {workInProgress.effectTag |= Ref;}
}
而在 commit
阶段的 recursivelyTraverseMutationEffects
函数中,我们在删除一个 Fiber 的同时,需要解绑 ref,它调用了safelyDetachRef
函数来完成这个操作,如果它是一个函数类型的 ref,会执行 ref 函数,参数为null,这个函数类型的 ref 就是我们的 class 组件使用 createRef
创造的,执行这个函数函数可以将其内部的值置为空,如果它不是一个 function, 也就是一个带有 current 的 DOM ,我们就把它的 useRef
属性设置为 null,从而清空我们的挂载:
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {const ref = current.ref;if (ref !== null) {if (typeof ref === 'function') {let retVal;try {if (enableProfilerTimer &&enableProfilerCommitHooks &¤t.mode & ProfileMode) {try {startLayoutEffectTimer();retVal = ref(null);} finally {recordLayoutEffectDuration(current);}} else {retVal = ref(null);}} catch (error) {captureCommitPhaseError(current, nearestMountedAncestor, error);}} else {ref.current = null;}}
}
而在 commit
阶段的 commitLayoutEffect
函数中,这个阶段会操作我们的真实 DOM,在函数的最后有这样的操作,如果我们处理的 Fiber 的标记中有 Ref 标记,我们将进入 commitAttachRef
这个函数,它的作用就是把 ref 和我们的 useRef
绑定,在类组件中或者一些另外的组件中,我们会通过safelyAttachRef
这个函数来调用 commitAttachRef
function commitLayoutEffectOnFiber(finishedRoot: FiberRoot,current: Fiber | null,finishedWork: Fiber,committedLanes: Lanes,
): void {//.......省略// 确保能拿到我们的 domif (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {if (enableScopeAPI) {// 排除 ScopeComponent 是因为之前已经处理了if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {commitAttachRef(finishedWork);}} else {if (finishedWork.flags & Ref) {commitAttachRef(finishedWork);}}}
}function commitMutationEffectsOnFiber(finishedWork: Fiber,root: FiberRoot,lanes: Lanes,
) {switch (finishedWork.tag) {//....// 对于 ScopeComponent ,这里已经处理了 safelyDetachRef 和 safelyAttachRefcase ScopeComponent: {if (enableScopeAPI) {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);if (flags & Ref) {if (current !== null) {safelyDetachRef(finishedWork, finishedWork.return);}safelyAttachRef(finishedWork, finishedWork.return);}if (flags & Update) {const scopeInstance = finishedWork.stateNode;prepareScopeUpdate(scopeInstance, finishedWork);}}return;}}
}
这个函数通过当前 fiber 的 tag 来获取对应的实例:对于 HostComponent ,实例就是获取到的 DOM 节点,其他情况就是 fiber.stateNode,之后判断 ref 的类型,如果是函数类型,调用 ref 函数并将实例传过去;若不是,则将 ref.current 赋值为该实例
function commitAttachRef(finishedWork: Fiber) {const ref = finishedWork.ref;if (ref !== null) {const instance = finishedWork.stateNode;let instanceToUse;switch (finishedWork.tag) {case HostComponent:instanceToUse = getPublicInstance(instance);break;default:instanceToUse = instance;}if (enableScopeAPI && finishedWork.tag === ScopeComponent) {instanceToUse = instance;}if (typeof ref === 'function') {let retVal;if (enableProfilerTimer &&enableProfilerCommitHooks &&finishedWork.mode & ProfileMode) {try {startLayoutEffectTimer();retVal = ref(instanceToUse);} finally {recordLayoutEffectDuration(finishedWork);}} else {retVal = ref(instanceToUse);}} else {ref.current = instanceToUse;}}
}
关于这个 hooks 的详细解读和用法讲解可以看这篇:React 的源码与原理解读(十三):Hooks解读之二 useRef
useState
useState()
是我们最常见的几个 hooks 之一,它运行我们传入一个初始值来初始化一个 state,之后返回给我们这个 state 和改变它的方法 setState:
const [state, setState] = useState(initialstate)
useState
的源码是这样的:
- 首先是我们获取传入的初始值,如果是函数的话,我们执行它获取结果作为我们的初始值
- 之后我们将我们的初始化放到 hook 节点的
memoizedState
和baseState
属性上 - 之后我们创建一个更新队列,我们将他们放在 hook 的 queue 属性中
- 最后我们使用
dispatchSetState
生成一个更新函数,返回给用户
function mountState<S>(initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {const hook = mountWorkInProgressHook();// 如果传入的初始值是一个函数,直接执行获得结果if (typeof initialState === 'function') {initialState = initialState();}// 更新 hook 缓存的数据hook.memoizedState = hook.baseState = initialState;// 创建一个更新队列const queue: UpdateQueue<S, BasicStateAction<S>> = {pending: null, // 依然是环状链表,这个属性指向链表的最后一个节点interleaved: null,lanes: NoLanes, dispatch: null, // setStatelastRenderedReducer: basicStateReducer, // 上次render传入的操作lastRenderedState: (initialState: any), // 上次render后的state};hook.queue = queue;// 生成更新函数const dispatch: Dispatch<BasicStateAction<S>,> = (queue.dispatch = (dispatchSetState.bind(null,currentlyRenderingFiber,queue,): any));return [hook.memoizedState, dispatch];
}
dispatchSetState
这个函数将这个更新封装成一个 Update 节点,然后把它拼接到 queue 的 pending 上;之后判定这个更新是不是在渲染阶段发生:如果是则设定一个标识;否则计算出新的 state,然后与之前的 state 对比,若没有更新,则直接退出;如果有更新,我们调用 scheduleUpdateOnFiber
函数开始一个调度,这个函数在 Lane 这章节已经详细讲过了,可以回去看这个函数:
function dispatchSetState<S, A>(fiber: Fiber, queue: UpdateQueue<S, A>, action: A) {// 获取laneconst lane = requestUpdateLane(fiber);// 将 action 操作封装成一个 update节点,用于后续构建链表使用const update: Update<S, A> = {lane, // 优先级action, // setState 传入的内容,可能是操作或者数值hasEagerState: false, // 紧急状态eagerState: null, // 紧急状态下提前计算出的结果next: (null: any), // 指向到下一个节点的指针};// 在渲染阶段的更新,拼接到 queue.pending 的后面if (isRenderPhaseUpdate(fiber)) {enqueueRenderPhaseUpdate(queue, update);} else {// 把 update 更新到 queue.pending 指向的环形链表里enqueueUpdate(fiber, queue, update, lane);const alternate = fiber.alternate;// 如果当前节点没有更新任务if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {const lastRenderedReducer = queue.lastRenderedReducer; // 上次render后的reducer,在mount时即 basicStateReducerif (lastRenderedReducer !== null) {let prevDispatcher;const currentState: S = (queue.lastRenderedState: any); // 上次render后的state,mount时为传入的initialStateconst eagerState = lastRenderedReducer(currentState, action);update.hasEagerState = true; // 表示该节点的数据已计算过了update.eagerState = eagerState; // 存储计算出来后的数据if (is(eagerState, currentState)) {// 若这次得到的state与上次的一样,则不再重新渲染return;}}}// 有更新任务const eventTime = requestEventTime();const root = scheduleUpdateOnFiber(fiber, lane, eventTime);if (root !== null) {entangleTransitionUpdate(root, queue, lane);}}markUpdateInDevTools(fiber, lane, action);
}function enqueueRenderPhaseUpdate<S, A>(queue: UpdateQueue<S, A>,update: Update<S, A>,
) {// 标识render阶段的更新产生了didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;const pending = queue.pending;if (pending === null) {update.next = update;} else {update.next = pending.next;pending.next = update;}queue.pending = update;
}function enqueueUpdate<S, A>(fiber: Fiber,queue: UpdateQueue<S, A>,update: Update<S, A>,lane: Lane,
) {// 交错更新if (isInterleavedUpdate(fiber, lane)) {const interleaved = queue.interleaved;if (interleaved === null) {update.next = update;pushInterleavedQueue(queue);} else {update.next = interleaved.next;interleaved.next = update;}queue.interleaved = update;} else {const pending = queue.pending;if (pending === null) {update.next = update;} else {update.next = pending.next;pending.next = update;}queue.pending = update;}
}
在更新阶段我们则调用了函数updateReducer
,它首先获取当前的 pending 队列和上次遗留下来的队列,把他们合并到一起,之后遍历队列中所有的 hook 更新,根据当前的优先级判定它能不能执行,若符合当前优先级的,则执行该 update 节点的 action,计算出新的 state,它将作为我们的返回值,遍历完所有可以执行的任务后,得到一个新的 newState,然后判断与之前的 state 是否一样,若不一样,则标记该 fiber 节点需要更新,并返回新的 newState 和 dispatch 方法。
function updateReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: I => S,
): [S, Dispatch<A>] {const hook = updateWorkInProgressHook();const queue = hook.queue;queue.lastRenderedReducer = reducer;const current: Hook = (currentHook: any);//上次遗留下来的优先级不够的任务let baseQueue = current.baseQueue;// 获取 queue 队列const pendingQueue = queue.pending;// 拼接链表if (pendingQueue !== null) {if (baseQueue !== null) {const baseFirst = baseQueue.next;const pendingFirst = pendingQueue.next;baseQueue.next = pendingFirst;pendingQueue.next = baseFirst;}current.baseQueue = baseQueue = pendingQueue;queue.pending = null; }if (baseQueue !== null) {const first = baseQueue.next;let newState = current.baseState; // 上次的渲染的结果值,每次循环时都计算得到该值,然后供下次循环时使用// 新的 BaseState,用于下次渲染的let newBaseState = null;// 新的 basequeuelet newBaseQueueFirst = null;let newBaseQueueLast = null;let update = first;do {const updateLane = update.lane;if (!isSubsetOfLanes(renderLanes, updateLane)) {// 优先级不足,跳过此更新,放到暂存执行的队列中const clone: Update<S, A> = {lane: updateLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};// 如果第一次出现了不能执行的了,我们要把当前的计算结果保存下来,因为我们会将此节点到最后的所有节点都存储起来,所以此时我们的 newBaseState 就是当前得到的值,这样下次渲染的时候,我们获得的就是这个不能执行的节点前的执行结果if (newBaseQueueLast === null) {newBaseQueueFirst = newBaseQueueLast = clone;newBaseState = newState;} else {newBaseQueueLast = newBaseQueueLast.next = clone;}// 更新优先级currentlyRenderingFiber.lanes = mergeLanes(currentlyRenderingFiber.lanes,updateLane,);// 标识跳过的任务的优先级markSkippedUpdateLanes(updateLane);} else {// 之前的节点有不能执行的,那么后面的节点都要缓存if (newBaseQueueLast !== null) {const clone: Update<S, A> = {// 该update需要执行,所以我们永远不能跳过他,使用NoLane优先级,可以避免上面的判断会跳过该步骤lane: NoLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};newBaseQueueLast = newBaseQueueLast.next = clone;}// 已经执行过了(mount中)if (update.hasEagerState) {newState = ((update.eagerState: any): S);} else {// 计算出当前位置的新的state,注意,这个 newState 是作为这一步的返回结果的,不一定我们的 newBaseState,所以就算要被缓存的节点,只要能执行就需要处理const action = update.action;newState = reducer(newState, action);}}update = update.next;} while (update !== null && update !== first);if (newBaseQueueLast === null) {// 所有的update都执行了,那么没有下一次渲染了,所以下一次需要的 baseState就是计算结果newBaseState = newState;} else {// 有低优先级的update任务,则next指针指向到第1个,形成单向环形链表,newBaseQueueLast.next = (newBaseQueueFirst: any);}// 若newState和之前的state不一样,标记该fiber需要更新if (!is(newState, hook.memoizedState)) {markWorkInProgressReceivedUpdate();}hook.memoizedState = newState; // 整个update链表执行完,得到的newState,用于本次渲染时使用hook.baseState = newBaseState; // 下次执行链表时的初始值hook.baseQueue = newBaseQueueLast; // 新的update链表,可能为空queue.lastRenderedState = newState; // 将本次的state存储为上次rendered后的值}// 交错更新,省略.....const dispatch: Dispatch<A> = (queue.dispatch: any);return [hook.memoizedState, dispatch];
}
useReducer
useReducer
和 useState
类似,但是它接收一个形如 (state, action) => newState 的 reducer
,并返回当前的 state 以及与其配套的 dispatch
方法。
const [state, dispatch] = useReducer(reducer, initState,init);
因为 useReducer
其实就是一个简单的可以自定义更新方法的 useState
,那么他们的源码也应该极为相似,所以这里我们就直接给出源码和注释:
function mountReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: I => S,
): [S, Dispatch<A>] {const hook = mountWorkInProgressHook();let initialState;// 如果是懒创建的 initState,我们调用函数得到其值,否则直接获取其值if (init !== undefined) {initialState = init(initialArg);} else {initialState = ((initialArg: any): S);}// 初始化 memoizedState 和 baseStatehook.memoizedState = hook.baseState = initialState;// 初始化更新队列const queue: UpdateQueue<S, A> = {pending: null,interleaved: null,lanes: NoLanes,dispatch: null,lastRenderedReducer: reducer,lastRenderedState: (initialState: any),};hook.queue = queue;// 创建 dispatchconst dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(null,currentlyRenderingFiber,queue,): any));return [hook.memoizedState, dispatch];
}
关于这两个 hooks 的详细解读和用法讲解可以看这篇:React 的源码与原理解读(十四):Hooks解读之三 useState&useReducer
useEffect
useEffect
是我们最常见的几个 hooks 之一,给函数组件增加了操作副作用的能力。其第一个参数是一个副作用函数,React 会在每次渲染后调用副作用函数 ,副作用函数还可以通过返回一个函数来指定如何清除副作用。其第二个参数是一个依赖项数组,只有其中有一项发生变化的情况才会触发当前的 userEffect ,否则不触发,如果我们设置第二个参数为空,那么相当于我们会监听所有的数据变化。
useEffect(effect, deps?);
useLayoutEffect
useLayoutEffect
和 useEffect
的定义完全相同,都是其传入一个副作用函数和一个依赖项数组,他的区别在于:
useEffect
是异步的,useLayoutEffect
是同步的useEffect
的执行时机是浏览器完成渲染之后,而useLayoutEffect
的执行时机是浏览器把内容真正渲染到界面之前
这两个 hook 源码也基本一致,mount 都是调用了一个 mountEffectImpl
函数:他首先把我们的设定的 fiberFlags 设定到了当前的 Fiber 上,后续我们会通过这个标志判定要不要处理 effect。然后使用 pushEffect
,它会先创建一个 effect 数据结构,接着将 effect 添加到函数组件 fiber 的更新队列 updateQueue 之上,最后返回这个创建的 effect 作为 hook.memoizedState。
function mountEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {return mountEffectImpl(PassiveEffect | PassiveStaticEffect,HookPassive,create,deps,);
}function mountLayoutEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {let fiberFlags: Flags = UpdateEffect;if (enableSuspenseLayoutEffectSemantics) {fiberFlags |= LayoutStaticEffect;}return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {const hook = mountWorkInProgressHook();const nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber.flags |= fiberFlags;hook.memoizedState = pushEffect(HookHasEffect | hookFlags,create,undefined,nextDeps,);
}function pushEffect(tag, create, destroy, deps) {const effect: Effect = {tag,create,destroy,deps,next: (null: any),};// 获取更新队列let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);// 没有的话先创建更新队列if (componentUpdateQueue === null) {componentUpdateQueue = createFunctionComponentUpdateQueue();currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);componentUpdateQueue.lastEffect = effect.next = effect;} else {const lastEffect = componentUpdateQueue.lastEffect;if (lastEffect === null) {componentUpdateQueue.lastEffect = effect.next = effect;} else {const firstEffect = lastEffect.next;lastEffect.next = effect;effect.next = firstEffect;componentUpdateQueue.lastEffect = effect;}}return effect;
}
update 阶段和 mount 阶段大同小异,调用了 updateEffectImpl
这个函数,我们获取了上次的缓存的 effect,拿出本次的 deps 和这次的进行对比,如果两次的依赖项没有变化,或者依赖项是空,我们本次不会执行这个 hook,推入一个没有 HookHasEffect
标记的 effect ,并且直接退出函数;如果有变化,或者 nextDeps
不存在,那么我们先在 fiber 上打标记,然后推入一个包含 HookHasEffect
标记的 effect,说明这个 effect 包含副作用需要被执行。
上述操作只是把副作用加入到了我们 Fiber 的 updateQueue
属性上,其具体的运行则是在 commit
阶段中,我们之前已经简单的提到过,这里再总结一下。
对于 useLayoutEffect
:
- 在
commitMutationEffects
这个阶段,执行我们删除节点和更新节点的销毁逻辑,也就是 destroy 函数 - 在
commitLayoutEffects
这个阶段,执行我们节点的副作用函数 create 逻辑并且挂载我们的 destroy 函数
对于 useEffect
:
- 首先我们在
commit
阶段一开始就执行了一次flushPassiveEffects
函数,这个函数遍历了我们的 fiber 上的副作用列表,对每个标记的HookPassive
的副作用(useEffect
创建的)调用它的 destroy 逻辑,然后在调他们的 create 逻辑并且挂载 destroy 函数,这个逻辑中把我们的rootWithPendingPassiveEffects
变量设置为了 null,这一步是因为上一次的渲染可能因为我们开启的flushPassiveEffects
被高优先级的任务抢占或者其他情况而没执行,我们需要确保进入 commit 阶段前我们没有未执行的useEffect
副作用了,这个逻辑也需要通过 do… while… 逻辑保证我们能顺利执行完毕(执行过程中也可能被抢占) - 在进入
commitBeforeMutationEffects
之前,如果显示我们的节点上具有HookPassive
标记(在 render 过程中会做好记号),说明我们有需要处理的useEffect
副作用,我们把rootDoesHavePassiveEffects
这个全局变量设置为 true ,之后我们开启一个调度,给我们的flushPassiveEffects
一个低优先级的任务,然后开始我们的commit
阶段的逻辑 - 在
commit
阶段的三个子阶段执行完毕后,我们根据rootDoesHavePassiveEffects
判断是不是有需要处理的useEffect
副作用, 如果有,那么我们把 FiberRoot 给予我们的rootWithPendingPassiveEffects
变量,这个,之后随着commit
阶段的结束,我们注册的任务开始了调度,此时我们的rootWithPendingPassiveEffects
包含了 effectList,就可以正常执行我们的处理逻辑 - 如果遍历正常结束,那么我们的
rootWithPendingPassiveEffects
会被置为 null,此时我们下一轮调度就不会走开头的 do… while… 逻辑清空rootWithPendingPassiveEffects
,但是如果我们的flushPassiveEffects
运行中被中断了,下一次运行时就仍会存在rootWithPendingPassiveEffects
,此时就需要清空他们了,要注意,如果我们没有后续的需要调度任务了,那么我们的flushPassiveEffects
肯定也不会被打断,就能顺利执行完毕,所以不用担心出现有useEffect
没执行的情况
关于这两个 hooks 的详细解读和用法讲解可以看这篇:React 的源码与原理解读(十五):Hooks解读之四 useLayoutEffect&useEffect
context
Context 是 React 官方提供的一种让父组件可以为它下面的整个组件树提供数据的数据传递方式,useContext
是一个 React Hook,可以让你读取和订阅组件中的 Context。
在 React 中,Context 的使用步骤如下:
- 创建 一个 context。
- 在指定数据的组件中 提供 这个 context。
- 在子组件中 消费 这个 context
首先我们使用 createContext
这个 API 来创建一个 Context ,它传入一个初始值,返回一个 context
const Context = React.createContext('default-value')
我们可以通过 Provider 包裹组件来提供这个 context,其中的 value 就是给子组件的这个 Provider 的初始值,下面的相当于 Context.Provider 包裹的所有子组件都可以通过 Context 来获取相同的数据,这些数据的初始值是 new-value。
const Context = React.createContext('default-value')
function Parent() {return ( // 在内部的后代组件都能够通过相同的 Ract.createContext() 的实例访问到 context 数据<Context.Provider value="new-value"><Children><Context.Provider>)
}
而我们的子组件可以通过的来消费我们的 context ,这里我们需要从之前定义 Context 的位置将其引入,只有使用了同一个 Context 才能获取相同的数据
import Context from "xxxxxx"
<Context.Consumer>{ v => {// 内部通过函数访问祖先组件提供的 Context 的值return <div> {v} </div>}}
</Context.Consumer>
useContext
是一个 React Hook,可以让你读取和订阅组件中的 Context
const value = useContext(SomeContext)
import Context from "xxxxxx"
function Child() {const { ctx } = useContext(Context)return <div> {ctx} </div>
}
context 这个类是 React 中定义的一个数据结构,createContext 就是新建了这样一个数据结构,包括了数据、Consumer 和 Provider 来提供用户使用
export type ReactContext<T> = {$$typeof: Symbol | number,Consumer: ReactContext<T>, // 消费 context 的组件Provider: ReactProviderType<T>, // 提供 context 的组件// 保存 2 个 value 用于支持多个渲染器并发渲染_currentValue: T,_currentValue2: T,_threadCount: number, // 用来追踪 context 的并发渲染器数量// DEV only_currentRenderer?: Object | null,_currentRenderer2?: Object | null,displayName?: string, // 别名_defaultValue: T, _globalName: string,...
};export function createContext<T>(defaultValue: T): ReactContext<T> {const context: ReactContext<T> = {$$typeof: REACT_CONTEXT_TYPE, // 用 $$typeof 来标识这是一个 context_currentValue: defaultValue, // 给予初始值_currentValue2: defaultValue, // 给予初始值_threadCount: 0,Provider: (null: any),Consumer: (null: any),_defaultValue: (null: any),_globalName: (null: any),};// 添加 Provider ,并且 Provider 中的_context指向的是 context 对象context.Provider = {$$typeof: REACT_PROVIDER_TYPE, // 用 $$typeof 来标识这是一个 Provider 的 symbol_context: context,};let hasWarnedAboutUsingNestedContextConsumers = false;let hasWarnedAboutUsingConsumerProvider = false;let hasWarnedAboutDisplayNameOnConsumer = false;// 添加 Consumercontext.Consumer = context;return context;
}
建立 Fiber 的过程中,我们会根据 $$typeof 的值给我们的 Fiber 的 tag 添加了不同的值,上文中,在创建 context 时,Provider 给予了 REACT_PROVIDER_TYPE 类型,而 Consumer 指向 context 本身,所以就是 REACT_CONTEXT_TYPE 类型字段,因而,当我们在 jsx 中解析到这两个类型时,就会判定为对应的字段。
我们在 beginWork 这个函数中,我们会对不同 tag 的进行处理:
-
在 Provider 初始化的时候,beginWork 中我们会将我们 context 的值压入栈中
-
而在 Consumer 初始化的时候,一个 Fiber 上依赖的所有 context 会被放入一个
dependencies
链表中,因为 Consumer 指向ReactContext 本身,所以我们直接通过 _currentValue 就可以拿到需要的对象 -
当一个 context 更新后,Provider 会进行判定,如果值发生变化不可复用,会调用
propagateContextChange
递归遍历所有的孩子节点,节点中使用了这个 Provider 的会被标识为强制更新优先级,在之后过程中被更新 -
当一个 Provider 处理完毕,在 commit 阶段,入栈的数值会被 pop 出去,然后对应 context 的值也会更新为栈中上一个节点的内容,这样做是为了保证在多次嵌套的 context 中,用户获得的始终是离它最近的的 Provider 提供的值
-
useContext 作为一个钩子,它本身只是为了适配 function 组件,它做的就是调用 Consumer 逻辑中的 readContext 函数来获取 context 的值
这部分的详细代码解读可以查看这篇:React 的源码与原理解读(十六):Context 与 useContext
写在后面
以上就是这半年来,作者对于 React v18.1.0 版本的解读,如果后续版本 React 的机制有了一定的更新迭代,这个教程可能会缺乏时效性。本教程的资料和解读部分有来自掘金 ,CSDN,知乎和公开博客网站等各方的资料,也有作者自己的解读和理解,肯定有不正确和不到位的部分,请各位海涵,一下的参考过的资料各位可以自行查阅:
https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberBeginWork.js
https://zhuanlan.zhihu.com/p/386897467
https://juejin.cn/post/7171000978278187038
https://www.xiabingbao.com/post/react/reconcile-children-fiber-riezuz.html
https://segmentfault.com/a/1190000015552387
https://xiaochen1024.com/courseware/60b1b2f6cf10a4003b634718/60b1b581cf10a4003b63472c
https://www.xiabingbao.com/post/react/react-element-jsx-rfl0yh.html
https://react.jokcy.me/book/api/react-children.html
https://juejin.cn/post/6844903873077837831
https://www.yuque.com/weixiaofuyanxintong-2tkrh/wzur6d/bnan90#mdMtM
如果有发现教程中有任何问题或者有疑问的,都欢迎私信作者。
作者的 Git:https://github.com/aiai0603
后续作者也会更新一些其他的前端教程,欢迎大家关注!