本篇我们来了解下react中用到的一些基础知识和概念,作为后续我们学习react三大模块的时候的前置知识储备。
react源码目录解析
react项目的源码是用monorepo的工程化架构组织的,如下核心代码都在packages包下面,分了很多个子包
bash
根目录
├── fixtures # 包含一些给贡献者准备的小型 React 测试项目
├── packages # 包含元数据(比如 package.json)和 React 仓库中所有 package 的源码(子目录 src)
├── scripts # 各种工具链的脚本,比如git、jest、eslint等
packages 目录
下面来看下包含核心源码的 packages
目录:
java
packages/
├── react/
├── react-dom/
├── react-reconciler/
├── scheduler/
├── shared/
├── react-server/
├── react-native-renderer/
├── react-devtools/
└── ...
- react/
提供核心API如createElement
、memo
、Context
和Hooks实现,所有通过import React from 'react'
引入的API均在此定义。 - scheduler/
任务调度器,负责优先级排序和时间切片,确保高优先级任务(如用户交互)优先执行。 - react-reconciler/
协调器实现,包含Fiber架构的diff算法和副作用标记逻辑。 - shared/
公共工具方法和常量定义,被多个模块复用。 - react-dom/
浏览器环境渲染器,处理DOM操作和事件系统,是ReactDOM.render()
的底层实现。
上面几个包是比较核心的包,react所有的流程都在这些包里面实现。调度层Scheduler (状态更新触发调度器分配任务优先级) → 协调层Reconciler (生成Fiber副作用链表) → 渲染层Renderer (提交变更到界面)。如果想调试源码建议从packages/react-dom/src/client/ReactDOMRoot.js
入口文件切入,逐步分析渲染链路。
jsx和babel
react使用jsx来表示UI声明式,react/jsx-runtime
是在React 17版本中引入的新JSX转换方式。在此之前(React 16及更早版本),JSX会通过Babel转换为React.createElement
调用,而从React 17开始,官方推荐使用react/jsx-runtime
提供的jsx
函数进行转换。这一变更使得开发者无需在每个文件中显式引入React即可使用JSX语法。本质上没有变化,都是把jsx编译成渲染函数,y运行时渲染函数去生成虚拟DOM。
Babel 的作用
- 将
let/const
转换为var
- 将箭头函数
() => {}
转换为function(){}
- 将
class
类语法转换为构造函数 - 支持 JSX 转换为
React.createElement()
、_jsx
- 支持异步语法
async/await
转换为Promise
js
import React from 'react'; // 手动引入 react
function App() {
return <h1>Hello World</h1>;
}
// 转换结果
// React 17之前,JSX 转换结果
import React from 'react';
function App() {
return React.createElement('div', null, 'Hello world!');
}
// React 17之后,JSX 转换结果
import { jsx as _jsx } from 'react/jsx-runtime'; // 由编译器引入(禁止自己引入!)
function App() {
return _jsx('div', { children: 'Hello world!' });
}
JSX 转换的过程大致分为两步:
- 编译时:由 Babel 编译实现,Babel 会将 JSX 语法转换为标准的 JavaScript API;
- 运行时:由 React 实现,
jsx
方法 和React.createElement
方法;
上面对于渲染函数的引用分为了两种方式
_jsx
解构引用React.createElement
全量引用
直接引用 React
会将非 createElement
相关的 API
(比如上面例子中就没有用到 useState
)一并被打包进编译文件中,而使用解构,可以通过 tree-sharking
来排除掉项目中未使用到的 React API
,这就是为什么 JSX runtime
可以减小打包体积
看到这里你应该明白了,其实 _jsx
和 createElement
本质上相同,都是一个创建虚拟DOM的函数。我们看下react内部源码实现
js
// packages/react/src/jsx.ts
import { REACT_ELEMENT_TYPE } from 'shared/ReactSymbols';
import {
Type,
Ref,
Key,
Props,
ReactElementType,
ElementType
} from 'shared/ReactTypes';
const ReactElement = function (
type: Type,
key: Key,
ref: Ref,
props: Props
): ReactElementType {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
ref,
props,
__mark: 'erxiao'
};
return element;
};
// packages/react/src/jsx.ts
// ...之前的代码
export const jsx = (type: ElementType, config: any, ...children: any) => {
let key: Key = null;
let ref: Ref = null;
const props: Props = {};
for (const prop in config) {
const val = config[prop];
if (prop === 'key') {
if (val !== undefined) {
key = '' + val;
}
continue;
}
if (prop === 'ref') {
if (val !== undefined) {
ref = val;
}
continue;
}
if ({}.hasOwnProperty.call(config, prop)) {
props[prop] = val;
}
}
const childrenLength = children.length;
if (childrenLength) {
if (childrenLength === 1) {
props.children = children[0];
} else {
props.children = children;
}
}
return ReactElement(type, key, ref, props);
};
// packages/react/index.ts
import { jsx } from './src/jsx';
export default {
version: '1.0.0',
createElement: jsx
};
fiber数据结构
-
Fiber 节点结构:每个节点包含类型、属性、DOM 节点引用以及形成链表的指针(parent、child、sibling)
-
双缓存机制:通过 alternate 属性实现当前树与工作树的切换
-
工作循环:
- 调度阶段:决定下一个要执行的工作单元
- 执行阶段:处理每个工作单元,创建 / 更新 Fiber 节点
- 提交阶段:将变更应用到实际 DOM
-
增量渲染:利用时间切片5ms时间执行工作,超时则让出主线程
-
优先级调度:虽然简化版没有完整实现优先级,但通过 timeRemaining () 检查实现了基本的可中断性
-
可恢复性:通过链表结构记录工作进度,下次可以从 nextUnitOfWork 继续
-
双缓存机制:通过 current (当前树) 和 workInProgress (工作树) 实现渲染准备与实际 DOM 更新分离
-
两阶段处理:
- 调度阶段 (可中断):计算变更、确定 effect
- 提交阶段 (不可中断):执行 DOM 操作
ini
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// 用于连接其他Fiber节点形成Fiber树
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
// 作为动态的工作单元的属性
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
这种设计使 React 能够在处理复杂 UI 时不会阻塞主线程,保证了良好的用户交互响应性。
微任务和宏任务
react18中用了微任务(Microtasks)和宏任务(Macrotasks)来实现高效任务调度的核心机制,它们的区别主要体现在执行时机、优先级和适用场景上。React 利用这种区别来优化渲染性能,确保用户交互等关键操作优先响应。
- 微任务 :
queueMicrotask
或Promise.then
调度为微任务,确保立即响应,避免用户操作卡顿。 - 宏任务 :
MessageChannel
实现可中断调度为宏任务,不可用时降级为setTimeout
,延迟执行以避免阻塞关键操作。
主要实现以下:
-
异步处理任务,可以实现异步任务编排,批处理功能;
-
让出主线程,实现任务分片,把长任务分成多个小任务,在每一帧在去执行;
-
实现更细粒度的优先级调度,紧急打断非紧急任务。
js
// 用于高优先级任务的微任务队列
const microtaskQueue = [];
let isMicrotaskFlushing = false;
// 处理微任务队列,可以批处理
function flushMicrotasks() {
isMicrotaskFlushing = true;
try {
// 执行所有微任务
while (microtaskQueue.length > 0) {
const callback = microtaskQueue.shift();
callback();
}
} finally {
isMicrotaskFlushing = false;
}
}
if (typeof queueMicrotask === 'function') {
queueMicrotask(flushMicrotasks);
} else {
// 降级方案:使用 Promise
Promise.resolve().then(flushMicrotasks).catch(error => {
console.error('Microtask error:', error);
});
}
// 处理宏任务队列(通过 MessageChannel)
// 用于低优先级任务的 MessageChannel
const channel = new MessageChannel();
const port1 = channel.port1;
const port2 = channel.port2;
// 执行回调任务
port1.onmessage = function() {
......
};
// 调度宏任务
function scheduleMacrotask() {
port2.postMessage(null);
}
};
任务优先级
我们已经知道优先级
意味着任务的过期时间。设想一个大型React
项目,在某一刻,存在很多不同优先级
的任务
,对应不同的过期时间。
同时,又因为任务可以被延迟,所以我们可以将这些任务按是否被延迟分为:
- timerQueue:保存未就绪任务
- taskQueue:保存已就绪任务
每当有新的未就绪的任务被注册,我们将其插入timerQueue
并根据开始时间重新排列timerQueue
中任务的顺序。
当timerQueue
中有任务就绪,即startTime <= currentTime
,我们将其取出并加入taskQueue
。
取出taskQueue
中最早过期的任务并执行他。
为了能在复杂度找到两个队列中时间最早的那个任务,Scheduler
使用小顶堆实现了优先级队列
。