React 核心包概览
October 12, 2022
react 是用于构建用户界面的 JavaScript 库
结构
React 仓库中与 web 开发相关的核心包只有四个
- react: 在编写 React 应用时,引用的函数基本都来自这个包,提供了编写 React 组件必备的函数
- scheduler: 调度机制的核心实现,负责调度不同优先级的任务
- react-reconciler: 接受 react 包提供的函数调用的输入,生成任务由 scheduler 调度,最终产物交给 react-dom
- react-dom: react 渲染器之一,负责将 react-reconciler 的产物输出到 web 界面中
React
React 中包含所有全局 React
API,比如:React.createElement
我们日常编写的 JSX 在编译时就会转化为 React.createElement
方法,这也是为什么在每个 JSX 的文件中你必须显示声明
import React from 'react'
React.createELement
最终会调用 ReactElement
方法返回一个包含组件数据的对象,也就是 React Element
对象
export function createElement(type, config, children) {
let propName;
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
// 将 config 处理后赋值给 props
// ...省略
}
const childrenLength = arguments.length - 2;
// 处理 children,会被赋值给props.children
// ...省略
// 处理 defaultProps
// ...省略
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// 标记这是个 React Element
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
return element;
};
React 核心只包含定义组件必要的 API。它不包含 Reconciler 算法或者其他平台特定的代码。它同时适用于 React DOM 和 React Native 组件
React Dom
服务于 Dom 的渲染器,作用就是将 React 组件渲染成 Dom
ReactDOM.render(<App />, document.getElementById('root'))
这行代码的意思就是将名为 App
的 React 组件渲染到 id
为 root
的根 dom 节点中去,此时 React DOM 拿到所有 ReactElements
就会去交给 reconciler ,最后拿到 reconciler 的输出再负责最后的渲染,因此 React Dom 在此起到了一个连接点的作用。
Fiber 树的渲染可以划分为三阶段,即渲染前,渲染,渲染后。想知道 Fiber 树如何构建的可以先移步下面的 React Reconciler
渲染前
这一阶段主要是为真正的渲染做一些准备
- 处理 blur 和 focus 相关逻辑
- 设置全局状态
- 重置全局变量
- 调用该阶段生命周期方法,例如
getSnapshotBeforeUpdate
- 调度
useEffect
(注意是调度,不是调用)
渲染
该阶段将发生真正的 Dom 操作
- 根据
effectList
对 Dom 进行增删改 - 对 fiber 进行删除时,解绑 ref,类组件调用
componetWillUnmount
方法 - 函数式组件调用
useLayoutEffect
的销毁函数 - 同步调用
componentDidMount
和useLayoutEffect
等,此时浏览器还没开始绘制
渲染后
浏览器渲染完毕后,浏览器通知 React 自己处于空闲状态。此阶段已经可以访问真实的 Dom
- 调用
useEffect
- 数据清除操作
React Reconciler
reconciler 运作流程有四个步骤,目的就是产出一种稳定结构供渲染器渲染
- 输入函数
scheduleUpdateOnFiber
,只要涉及到需要改变 fiber 的操作(无论是首次渲染 或后续更新操作), 最后都会间接调用scheduleUpdateOnFiber
- 与 scheduler 交互,注册调度任务(其中涉及优先级管理、时间切片等复杂逻辑
- 执行任务回调,在内存中构造 Fiber 树
- 输出结果提供给渲染器进行渲染
Fiber 树构建
Fiber 树的构建分为两种情况,初次构建与对比更新
初次构建
入口:ReactDom.render
→ updateContainer
→scheduleUpdateOnFiber
对比更新
入口:
Class
组件中调用setState
Function
组件中调用hook
对象暴露出的dispatchAction
- 在
container
节点上重复调用render
(官网示例)
当发生更新时,创建新 Fiber 之前需要和旧 Fiber 进行对比。最后构造的 Fiber 树有可能是全新的, 也可能是部分更新的。
reconciler 的主要内容就是 fiber 树的构建,而 Fiber Reconciler 是从 Stack Reconciler 重构而来,重点的优化就是实现了可中断的的递归遍历
// performSyncWorkOnRoot 会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot 会调用该方法
function workLoopConcurrent() {
// shouldYield 可暂停循环
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
在 Fiber Reconciler 中,performUnitOfWork
分为两个阶段,探寻阶段 beginWork
与回溯阶段 completeWork
,以上两个阶段是每个 Fiber 节点都会经历,也正是这两个阶段完成了 Fiber 树的构建
beginWork
从根节点开始深度优先遍历,对遍历到的 Fiber 节点执行 beginWork,整体流程大致如下图所示
beginWork
会根据 ReactElement
对象创建所有的 Fiber 节点,最终构造出 Fiber 树的整体结构(通过设置 return
与 sibling
),在 Update 阶段时会确立好要更新的 Fiber 节点的方式存储到 effectTag
中,便于 render 阶段时处理。
你可以从这里 (opens new window)看到effectTag
对应的DOM
操作,比如:
// DOM需要插入到页面中
export const Placement = /* */ 0b00000000000010;
// DOM需要更新
export const Update = /* */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /* */ 0b00000000000110;
// DOM需要删除
export const Deletion = /* */ 0b00000000001000;
completeWork
从叶子结点开始向上回溯,对 Fiber 节点执行 completeWork
方法。beginWork
会创建完 Fiber 节点并且将对该节点的改动添加到该节点的 effectTag
中,但在渲染阶段我们是要将其转化为 Dom 节点并执行 Dom 操作的,所以在 completeWork
阶段将会为渲染层准备好这些数据
- 生成当前节点的 Dom 节点,将子孙的 Dom 节点插入到当前节点,设置
Fiber.stateNode
指向当前的 Dom 实例 - 为 Dom 节点设置属性,绑定事件
- 将
beginWork
设置的effectTag
设置到父节点的effectList
中
通过上述几个步骤,要生成的 Dom 树与要执行的副作用变都被回溯到了根节点上,commit 阶段时便不需要再深度优先遍历去确认
至此,fiber 树构建完成,只需要将根节点 FiberRootNode
交给 commit 阶段的方法即可。也就是 React-dom 中的渲染
reconciler 本身不与 DOM 绑定。挂载的确切结果(在源代码中有时叫做 “挂载映像”)取决于 renderer,可以是一个 DOM 节点(React DOM),一个字符串(React DOM Server),或是一个表示原生视图的数字(React Native)。
Scheduler
React 内部对于优先级的管理体系。
可中断渲染、时间切片、异步渲染等特性,都依赖 Scheduler
调度内核
Scheduler 需要满足以下两个功能
- 暂停 Js 执行,将主线程还给浏览器,让浏览器有机会更新页面
- 在未来某个时刻继续调度任务,执行上次还未执行完的任务
微任务会在一次事件循环的最后执行,并不能让出主线程给浏览器,因此只能用宏任务去实现,这里的 MessageChannel
的作用就是生成宏任务( setTimeout
也可以生成宏任务,因为 setTimeout
并不稳定,嵌套调用时存在最小延迟 4ms
任务队列
调度的目的是消费任务,scheduler 中维护了一个 taskQueue
,任务队列的管理便是围绕这个 taskQueue
来展开
// Tasks are stored on a min heap
var taskQueue = []
创建任务
将任务创建完成后添加到任务队列中,然后触发调度申请
// 省略部分无关代码
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 1. 获取当前时间
var currentTime = getCurrentTime();
var startTime;
if (typeof options === 'object' && options !== null) {
// 从函数调用关系来看, 在v17.0.2中,所有调用 unstable_scheduleCallback 都未传入options
// 所以省略延时任务相关的代码
} else {
startTime = currentTime;
}
// 2. 根据传入的优先级, 设置任务的过期时间 expirationTime
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
var expirationTime = startTime + timeout;
// 3. 创建新任务
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (startTime > currentTime) {
// 省略无关代码 v17.0.2中不会使用
} else {
newTask.sortIndex = expirationTime;
// 4. 加入任务队列
push(taskQueue, newTask);
// 5. 请求调度
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}
消费任务
收到调度申请后开始循环处理任务队列中的任务
// 省略无关代码
function flushWork(hasTimeRemaining, initialTime) {
// 1. 做好全局标记, 表示现在已经进入调度阶段
isHostCallbackScheduled = false;
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
// 2. 循环消费队列
return workLoop(hasTimeRemaining, initialTime);
} finally {
// 3. 还原全局标记
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
}
}
主要逻辑都在 workLoop 中
// 省略部分无关代码
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime; // 保存当前时间, 用于判断任务是否过期
currentTask = peek(taskQueue); // 获取队列中的第一个任务
while (currentTask !== null) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// 虽然currentTask没有过期, 但是执行时间超过了限制(毕竟只有5ms, shouldYieldToHost()返回true). 停止继续执行, 让出主线程
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// 执行回调
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
// 回调完成, 判断是否还有连续(派生)回调
if (typeof continuationCallback === 'function') {
// 产生了连续回调(如fiber树太大, 出现了中断渲染), 保留currentTask
currentTask.callback = continuationCallback;
} else {
// 把currentTask移出队列
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
} else {
// 如果任务被取消(这时currentTask.callback = null), 将其移出队列
pop(taskQueue);
}
// 更新currentTask
currentTask = peek(taskQueue);
}
if (currentTask !== null) {
return true; // 如果task队列没有清空, 返回true. 等待调度中心下一次回调
} else {
return false; // task队列已经清空, 返回false.
}
}
每一次 while 循环的退出就是一个时间切片,退出条件有两个
- 没有更多任务了
- 在消费任务队列时,执行下一个 task 的 callback 之前,都会进行检测,所以检测是以 task 为单位