Appearance
react18
什么是 React?
- React 是一个用于构建用户界面的 JavaScript 库
React 能干什么?
- 可以通过组件化的方式构建 构建快速响应的大型 Web 应用程序
组件化
- 组件化 把页面拆分为一个个组件,方便视图的拆分和复用,还可以做到高内聚和低耦合
diff 算法时间复杂度
传统 diff 算法:
- 通过
循环递归
对节点进行依次对比,算法复杂度达到O(n^3)
- 如果要展示 1000 个节点,得执行上亿次比较。。即便是 CPU 快能执行 30 亿条命令,也很难在一秒内计算出差异。
- 通过
React diff:
首先,进行
同级比较
,并非循环比较。这样比较次数就降为
一层一次
,时间复杂度直接降为O(n)
如果同级相同位置节点
不一样
,则直接删除替换
,简单粗暴。
什么是 JSX
TIP
在 React17 以前,babel 是调用的 React.createElement,并且页面需要引入
在 React17 之后,babel 是调用的(react/jsx-runtimejsx)函数,并且页面无需引入
- jsx 是一个语法糖,即可以写 html 也可以写 js
- 因为 jsx 在浏览器端是不能被识别的,所以代码在编译阶段会转换成 ReactElement
- ReactElement 就是我们所说的虚拟节点
createPortal
- 将组件渲染到对应的容器中
ReactDOM.createPortal(child, container)
虚拟的 dom 优缺点
优点
- 处理了浏览器兼容性问题,避免用户操作真实 DOM
- 内容经过了 XSS 处理,可以防范 XSS 攻击
- 容易实现跨平台开发 Android、iOS、VR 应用
- 更新的时候可以实现差异化更新,减少更新 DOM 的操作
缺点
- 不利用 SEO
- 首屏加载速度慢
函数组件和类组件的区别
特性 | 函数组件 | 类组件 |
---|---|---|
定义方式 | JavaScript 函数 | ES6 类继承 React.Component |
状态管理 | useState /useReducer Hooks | this.state + this.setState |
生命周期 | useEffect Hook | 生命周期方法(componentDidMount 等) |
this 绑定 | 无 this ,避免绑定问题 | 需要处理 this 绑定 |
代码复用 | 自定义 Hooks | 高阶组件(HOC)或 Render Props |
性能优化 | React.memo + useMemo /useCallback | PureComponent /shouldComponentUpdate |
错误边界 | ❌ 不支持(需配合类组件) | ✅ 支持(componentDidCatch ) |
实例引用 | useRef + useImperativeHandle | 直接通过 ref 获取实例 |
心智模型 | 函数式编程(无副作用) | 面向对象编程(实例化) |
状态管理机制
jsx
// 函数组件(Hooks)
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c+1)}>{count}</button>;
}
// 类组件
class Counter extends React.Component {
state = { count: 0 };
render() {
return <button onClick={() => this.setState({count: this.state.count+1})>
{this.state.count}
</button>
}
}
- 核心差异:函数组件状态是闭包环境中的独立值,类组件状态是实例的
this.state
属性
生命周期实现
jsx
// 函数组件(useEffect)
useEffect(() => {
// componentDidMount + componentDidUpdate
fetchData();
return () => { /* componentWillUnmount */ }
}, [dependencies]);
// 类组件
componentDidMount() { fetchData(); }
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) fetchData();
}
componentWillUnmount() { cleanup(); }
- 核心差异:函数组件通过声明式依赖数组控制副作用,类组件需要手动比较
prevProps
this
绑定问题
jsx
// 类组件典型问题
class Button extends React.Component {
handleClick() {
// this 可能为 undefined
console.log(this.props);
}
// 解决方案1: 构造函数绑定
constructor() {
this.handleClick = this.handleClick.bind(this);
}
// 解决方案2: 箭头函数
handleClick = () => { ... }
}
- 函数组件优势:天然规避
this
绑定问题,所有值通过闭包获取
逻辑复用方式
jsx
// 函数组件(自定义 Hook)
const useLogger = (value) => {
useEffect(() => {
console.log("Value changed:", value);
}, [value]);
};
// 类组件(HOC)
const withLogger = (Comp) => {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log("Props changed:", this.props);
}
render() {
return <Comp {...this.props} />;
}
};
};
受控组件&非受控组件
- 优先使用
受控组件
,符合 React 设计原则 - 必须操作 DOM 时,再使用非受控组件
受控组件
受控组件是指由
React组件的状态(state)
来控制其表单元素
的值或行为的组件。在受控组件中,表单元素的值(如 input、textarea、select 等)以及其他用户输入的操作,都通过React组件的状态来进行管理和更新
。受控组件的特点包括:
- 1.状态控制:表单元素的值被保存在 React 组件的状态中,通过设置状态来控制表单元素的值。
- 2.事件处理:通过监听表单元素的事件(如 onChange 事件),将用户输入的值更新到组件的状态中。
- 3.数据流一致性:React 组件的状态是单一数据源,用于更新和渲染表单元素的值。通过确保状态与表单元素的值保持一致,可以实现数据的一致性和同步更新。
非受控组件
非受控组件,将表单数据交给
DOM节点来处理
,可以使用Ref
来获取非受控组件
的数据,希望能够赋予表单一个初始值,但是不去控制后续的更新
。可以采用 defaultValue 指定一个默认值非受控组件使用场景,必须手动操作 DOM 元素,state 实现不了,文件上传
<input type=file ref={fileRef}/>
js
import { useState } from "react";
const Controlled: React.FC = () => {
const [state, setState] = useState < string > "hulei";
const getValue = () => {
console.log(state);
};
return (
<>
<div>受控组件</div>
<input
type="text"
value={state}
onChange={(e) => setState(e.target.value)}
/>
<br />
{state}
<br />
<button onClick={getValue}>获取值</button>
</>
);
};
js
import { useRef, useState } from "react";
const Uncontrolled: React.FC = () => {
const [state, setState] = useState < string > "hulei";
const inputRef = useRef < HTMLInputElement > null;
const getValue = () => {
// 通过DOM或者到值
console.log(inputRef.current?.value);
};
return (
<>
<div>非受控组件</div>
<input type="text" defaultValue={state} ref={inputRef} />
<br />
{state}
<br />
<button onClick={getValue}>获取值</button>
</>
);
};
React 时间分片的大概原理
- 浏览器
刷新频率为 60Hz
也就是一秒钟 60 帧,大概 (1000/60)16.6 毫秒渲染一次
,而JS 线程
和渲染线程
是互斥的
,所以如果 JS线程执行任务
时间超过 16.6ms
的话,就会导致掉帧,导致卡顿
, React利用空闲的时间
进行更新,不影响 UI 渲染进 - 把一个耗时任务切分成一个个小任务,
分布在每一帧里
的方式就叫时间切片
为什么会出现 fiber
React 每次更新都是从根节点开始
递归
调度并且无法中断,如果 dom 树的结构比较复杂对应的虚拟节点计算会消耗对应的时间
如果此时用户在进行表单填写操作,会感觉的卡顿,因为
js渲染
和UI渲染
是互斥fiber 架构利用
浏览器空闲时间
(requestIdleCallback)执行任务,拒绝长时间占用主线程,放弃递归
,只采用循环
,因为循环可以被中断,任务拆分,将任务拆分成一个个小任务
fiber 数据结构
- fiber 是一个链表的数据结构,他的 child 指向第一个子节点,sibling 指向最近弟弟,return 指向父 fiber,即使是执行被打断,也很容易恢复
优先级
scheduler 优先级
- scheduler 优先级有 5 种,每一个优先级对应一个
过期时间
,过期时间越短,对应优先级越高
js
// 无优先级
export const NoPriority = 0;
// 立刻执行优先级(等待时间-1ms)
export const ImmediatePriority = 1;
//用户阻塞操作优先级 用户点击 ,用户输入(等待时间250ms)
export const UserBlockingPriority = 2;
// 正常优先级(等待时间5000ms)
export const NormalPriority = 3;
// 低优先级(等待时间10000ms)
export const LowPriority = 4;
// 空闲优先级(没有过期时间)
export const IdlePriority = 5;
Lane
- Lane 一共有 31 条车道,对应 5 种事件优先级
js
/**
* 一共有31条车道
*/
const TotalLanes = 31;
//没有车道,所有位都为0
const NoLanes = 0b0000000000000000000000000000000;
const NoLane = 0b0000000000000000000000000000000;
//同步车道,优先级最高
const SyncLane = 0b0000000000000000000000000000001;
const SyncBatchedLane = 0b0000000000000000000000000000010;
//离散用户交互车道 click
const InputDiscreteHydrationLane = 0b0000000000000000000000000000100;
const InputDiscreteLanes = 0b0000000000000000000000000011000;
//连续交互车道 mouseMove
const InputContinuousHydrationLane = 0b0000000000000000000000000100000;
const InputContinuousLanes = 0b0000000000000000000000011000000;
//默认车道
const DefaultHydrationLane = 0b0000000000000000000000100000000;
const DefaultLanes = 0b0000000000000000000111000000000;
//渐变车道
const TransitionHydrationLane = 0b0000000000000000001000000000000;
const TransitionLanes = 0b0000000001111111110000000000000;
//重试车道
const RetryLanes = 0b0000011110000000000000000000000;
const SomeRetryLane = 0b0000010000000000000000000000000;
//选择性水合车道
const SelectiveHydrationLane = 0b0000100000000000000000000000000;
//非空闲车道
const NonIdleLanes = 0b0000111111111111111111111111111;
const IdleHydrationLane = 0b0001000000000000000000000000000;
//空闲车道
const IdleLanes = 0b0110000000000000000000000000000;
//离屏车道
const OffscreenLane = 0b1000000000000000000000000000000;
事件优先级
js
//离散事件优先级 click onchange
export const DiscreteEventPriority = SyncLane; //1
//连续事件的优先级 mousemove
export const ContinuousEventPriority = InputContinuousLane; //4
//默认事件车道
export const DefaultEventPriority = DefaultLane; //16
//空闲事件优先级
export const IdleEventPriority = IdleLane;
React 渲染流程(精简版)
React 的渲染流程分为Scheduler 调度(协调 Reconciler)和渲染两大阶段,核心是优先级驱动和可中断渲染:
一、调度阶段(准备更新)
分配优先级
- 组件状态变化时,通过
requestUpdateLane
确定更新的优先级(Lane 机制) - 示例:点击事件 → 高优(SyncLane),数据请求 → 低优(DefaultLane)
- 组件状态变化时,通过
标记更新路径
- 从触发更新的 Fiber 向上遍历,合并 Lane 到根节点的
pendingLanes
(markUpdateLaneFromFiberToRoot
)
- 从触发更新的 Fiber 向上遍历,合并 Lane 到根节点的
调度决策
javascriptconst nextLane = getHighestPriorityLane(root.pendingLanes); if (nextLane === NoLane) return; // 无更新则取消 if (nextLane !== prevLane) { cancelCallback(prevTask); // 优先级变化时取消旧任务 }
- 高优先级任务会中断低优先级任务
二、渲染阶段(执行更新)
任务调度策略
更新类型 调度方式 是否可中断 场景 同步更新 微任务调度 ❌ 不可中断 点击、输入等交互 并发更新 Scheduler 调度器 ✅ 可中断 数据加载、懒加载组件 构建 Fiber 树(Render Phase)
- 执行
performConcurrentWorkOnRoot
- 可中断条件:
- 时间片耗尽(默认 5ms)
- 更高优先级任务插入
- 中断后返回恢复函数,等待重新调度
- 执行
提交更新(Commit Phase)
javascriptfunction commitRoot() { // 1. 执行 DOM 操作(增删改) // 2. 调用生命周期/useEffect // 3. 清空 pendingLanes }
- 同步一次性完成(不可中断)
- 更新后触发浏览器重绘
三、关键设计思想
- 优先级驱动
- 高优任务(如用户输入)优先响应
- 时间切片
- 将渲染拆分为 5ms 任务块,避免阻塞主线程
- 双阶段分离
- Render Phase(可中断/重试):计算变更
- Commit Phase(不可中断):应用变更
- 中断恢复机制
- 通过 Fiber 节点保存进度,精准恢复中断任务
示例场景:
当用户输入打断数据加载时:
- 高优输入任务中断低优数据渲染
- 优先处理输入并更新 DOM
- 空闲时恢复数据渲染
Scheduler 调度器
一、定位与作用
Scheduler 是 React 的独立调度引擎(核心包:scheduler
),主要解决两个核心问题:
- 任务优先级调度:对任务进行分级处理(5 级优先级)
- 时间切片:将长任务拆分为 5ms 的微任务块
💡 设计目标:让高优先级任务(如用户交互)能即时响应,低优先级任务(如数据加载)不阻塞主线程
二、核心机制
1. 优先级体系(5 级)
优先级常量 | 值 | 对应场景 |
---|---|---|
ImmediatePriority | 1 | 紧急任务(如动画) |
UserBlockingPriority | 2 | 用户交互(点击/输入) |
NormalPriority | 3 | 默认优先级(数据更新) |
LowPriority | 4 | 低优先级(通知类更新) |
IdlePriority | 5 | 空闲任务(日志上报等) |
javascript
// React 内部将 Lane 优先级映射到 Scheduler 优先级
const schedulerPriority = lanePriorityToSchedulerPriority(nextLane);
2. 时间切片原理
javascript
function workLoop(hasTimeRemaining, initialTime) {
while (currentTask !== null && !shouldYield()) {
// 每次执行 5ms 的任务块
if (
currentTask.expirationTime > initialTime &&
performance.now() - initialTime > 5
) {
break; // 时间片耗尽,中断执行
}
currentTask = flushTask(currentTask); // 执行任务
}
}
3. 中断恢复流程
三、关键 API(React 内部使用)
1. 任务调度
javascript
// 调度新任务
const taskId = unstable_scheduleCallback(
schedulerPriority,
performConcurrentWorkOnRoot,
{ timeout: expirationTime } // 超时控制
);
// 取消任务
unstable_cancelCallback(taskId);
2. 执行控制
javascript
// 检查是否需要中断(时间片耗尽/高优任务)
if (unstable_shouldYield()) {
return performConcurrentWorkOnRoot; // 返回恢复函数
}
// 获取当前时间片剩余时间
const remainingTime = unstable_getCurrentPriorityLevel();
四、与 React 渲染流程的协作
任务注册
React 通过scheduleCallback(NormalPriority, performConcurrentWorkOnRoot)
注册渲染任务执行控制
Fiber 构建过程中持续检查shouldYield()
:javascriptfunction workLoopConcurrent() { while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); // 构建 Fiber 节点 } }
中断点保存
中断时通过 Fiber 节点的return
和child
指针保存进度恢复执行
Scheduler 在浏览器空闲时重新调度未完成的任务
五、设计亮点
基于 MessageChannel 的调度
使用port.postMessage
实现微任务调度,比setTimeout
更高效饥饿问题处理
低优先级任务持续被中断时,会临时提升优先级防止饿死与浏览器渲染对齐
通过requestPaint
在重绘前完成任务提交超时强制执行
设置timeout
防止低优先级任务无限等待(如Offscreen
隐藏内容)
示例:当用户滚动时,Scheduler 会暂停正在执行的组件渲染,优先处理滚动事件,确保 60fps 流畅体验。
六、总结
"React Scheduler 是独立于 React 的调度引擎,核心解决 任务优先级管理 和 时间切片 问题。它实现了:
- 五级优先级体系,将 React 的 Lane 优先级映射到调度优先级
- 5ms 时间切片机制,通过
shouldYield()
控制任务中断 - 基于 MessageChannel 的调度,高效利用浏览器空闲时间
- 中断恢复机制,配合 Fiber 架构保存任务进度
例如用户输入时,Scheduler 会中断正在执行的渲染任务,优先处理交互事件,确保页面响应速度。这种设计是 React 并发模式流畅体验的基础支撑。"
Fiber 树构建流程
1. 初始化阶段
javascript
// 创建双缓存结构
const root = createFiberRoot(container);
// current: 当前显示的树
// workInProgress: 正在构建的树
root.current.alternate = workInProgress;
workInProgress.alternate = root.current;
2. 开始构建(Render Phase)
javascript
function performConcurrentWorkOnRoot(root) {
// 构建 workInProgress 树
renderRootSync(root, lanes);
// 检查中断条件
if (shouldYield()) {
return performConcurrentWorkOnRoot; // 返回恢复函数
}
// 进入提交阶段
commitRoot(root);
}
3. 深度优先遍历流程
4. beginWork 阶段(向下遍历)
javascript
function beginWork(current, workInProgress, renderLanes) {
// 复用逻辑:检查是否可以复用 current 树节点
if (current !== null && workInProgress.type === current.type) {
// 复用 Fiber 节点(双缓存关键)
cloneChildFibers(current, workInProgress);
}
// 根据组件类型处理
switch (workInProgress.tag) {
case FunctionComponent:
return updateFunctionComponent(current, workInProgress, ...);
case ClassComponent:
return updateClassComponent(current, workInProgress, ...);
case HostComponent:
return updateHostComponent(current, workInProgress, ...);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, ...);
}
}
5. completeWork 阶段(向上回溯)
javascript
function completeWork(current, workInProgress) {
// 创建 DOM 节点(HostComponent 类型)
if (workInProgress.tag === HostComponent) {
const instance = createInstance(
workInProgress.type,
workInProgress.pendingProps
);
// 挂载到 workInProgress 树
workInProgress.stateNode = instance;
// 处理属性更新
diffProperties(instance, workInProgress.pendingProps);
}
// 收集副作用(effect list)
if (workInProgress.flags > PerformedWork) {
// 添加到父节点的 effectList
appendEffectToParent(workInProgress);
}
}
6. 中断恢复机制
javascript
// 时间片检查(每5ms)
function shouldYield() {
return performance.now() - startTime > 5 || Scheduler_shouldYield();
}
// 恢复时从断点继续
function resumeWork() {
let node = workInProgress; // 保存的指针
while (node !== null && !shouldYield()) {
// 继续处理当前节点
node = performUnitOfWork(node);
}
}
7. 双缓存切换(Commit Phase)
javascript
function commitRoot(root) {
// 步骤1:切换树指针
root.current = root.finishedWork; // workInProgress → current
// 步骤2:执行 DOM 操作
commitMutationEffects(root);
// 步骤3:同步更新 alternate 指针
const current = root.current;
const workInProgress = current.alternate;
workInProgress.alternate = current;
current.alternate = workInProgress;
}
React 18 新特性影响
并发渲染优化:
- 使用
startTransition
标记低优先级更新 - 自动批处理:合并多个状态更新
- 使用
Offscreen API:
jsx<Offscreen mode="hidden"> <ExpensiveComponent /> </Offscreen>
- 隐藏的组件保持双缓存状态
- 复用已有的 Fiber 节点
Suspense 增强:
- 流式渲染时暂停/恢复组件树
- 部分子树可独立提交
总结要点
React 18 的 Fiber 树构建核心是双缓存架构:
双树结构:
current
树:当前显示的 UIworkInProgress
树:内存中构建的新树- 通过
alternate
指针相互连接
构建过程:
- 深度优先遍历(DFS)
- beginWork:向下处理组件渲染/Diff(可复用节点)
- completeWork:向上回溯创建 DOM/收集副作用
中断恢复:
- 每 5ms 检查
shouldYield()
- 保存当前
workInProgress
指针 - 时间片机制确保不阻塞主线程
- 每 5ms 检查
提交阶段:
- 原子操作切换双缓存树指针
- 同步更新
alternate
关系 - 一次性提交 DOM 变更
React 18 增强:
- 并发渲染:优先级驱动构建过程
- Offscreen:维持隐藏组件的双缓存
- Suspense:子树级的中断恢复
例如更新一个组件时:
- 从
current.alternate
克隆新树 - 深度优先构建
workInProgress
树 - 时间片耗尽时保存进度
- 恢复时从中断节点继续
- 完成构建后切换双缓存指针
React 的 Diff 原理
React 的 Diff 算法是虚拟 DOM 的核心优化策略,通过高效的差异比较最小化 DOM 操作,提高性能。React 的 Diff 算法主要分为以下几部分:
1、同层比较:仅比较同一层级的节点,不跨层级移动 3、复用优先:当 key 和 type 相同时复用老 Fiber 节点,否则创建新的 Fiber 节点 4、多场景优化:针对单节点和多节点不同处理策略
一、单节点 Diff 流程
- 1、循环老的 Fiber 节点,如果新的虚拟节点的
类型(type)
和key
和老 Fiber 一致,复用老的 Fiber 节点,同时还需要把剩下的 Fiber 兄弟节点标记为删除 - 2、如果新的虚拟节点的
类型(type)
和key
和老 Fiber 的不一致,需要创建新的 Fiber 节点,同时还需要把剩下的 Fiber 兄弟节点标记为删除 - 3、如果 Fiber 循环完毕后,未能找到能够复用的 Fiber 节点,需要根据新的虚拟 DOM 创建新的 Fiber 节点
js
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
let child = currentFirstChild;
let deleted = false;
while (child !== null) {
// 1. 检查key和type是否匹配
if (child.key === element.key && child.type === element.type) {
// 1.1 复用节点
const existing = useFiber(child, element.props);
existing.return = returnFiber;
// 1.2 删除后续兄弟节点
let nextChild = child.sibling;
while (nextChild) {
deleteChild(returnFiber, nextChild);
nextChild = nextChild.sibling;
}
return existing;
}
// 2. 不匹配则标记删除
deleteChild(returnFiber, child);
deleted = true;
child = child.sibling;
}
// 3. 未找到匹配节点
const created = createFiberFromElement(element);
created.return = returnFiber;
return created;
}
二、多节点 Diff 流程(多节点分为三种情况)
- 1、老的 Fiber 全部能被复用,新的节点还没有遍历完成(新的节点比老的多)
- 2、老的 Fiber 复用了部分,新的节点已经遍历完了(新的节点比老的少)
- 3、老的 Fiber 和新的虚拟 DOM 都没有循环完毕(根据老的 Fiber 生成 map 对象,遍历还未完成的虚拟 DOM,最大程度的复用)
- 第一轮比较 A 和 A,相同可以复用,更新,然后比较 B 和 C,key 不同直接跳出第一个循环
- 把剩下 oldFiber 的放入 existingChildren 这个 map 中
- 然后声明一个 lastPlacedIndex 变量,表示不需要移动的老节点的索引,默认为 0
- 继续循环剩下的虚拟 DOM 节点,从 C 开始
- 如果能在 map 中找到相同 key 相同 type 的节点则可以复用老 fiber,并把此老 fiber 从 map 中删除
- 如果能在 map 中找不到相同 key 相同 type 的节点则创建新的 fiber 节点
- 如果是复用老的 fiber,则判断老 fiber 的索引是否小于 lastPlacedIndex
- 如果小于 lastPlacedIndex 则需要移动老 fiber,lastPlacedIndex 不变
- 如果大于 lastPlacedIndex 则不需要移动老 fiber,更新 lastPlacedIndex 为老 fiber 的 index
- 虚拟 DOM 循环结束后把 map 中所有的剩下的 fiber 全部标记为删除
循环链表
实现思路:
- 1、当 fiber 更新队列为空时,让新的队列
queue1.next
指向头节点,让头节点指向queue1
- 2、当
queue2
进入队列时,让queue2.next
指向头节点的next
(queue1),让queue1.next
指向queue2
(形成循环链表结构),让头节点在指向 queue2 - 3、当
queue3
进入队列时,让queue3.next
指向头节点(queue2)的next
(queue1),让头节点(queue2.next)指向queue3
,让头节点在指向 queue3
- 1、当 fiber 更新队列为空时,让新的队列
目的:即可以保证链表的顺序也可以减少放入队列的循环次数,在更新状态的时候,需要从第一个进入的队列开始遍历,这里在添加队列的时候只需要改变 2 个指针,在遍历的时候找最后一个队列的下一个就是头节点,并且让最后一个 next 指向 null
合成事件
合成事件(SyntheticEvent)是 React 的核心特性之一,它创建了跨浏览器兼容的事件系统,解决了浏览器事件处理的兼容性问题,同时优化了性能。
- 1、跨浏览器一致性:统一不同浏览器的事件处理接口
- 2、性能优化:通过事件委托减少内存消耗
- 3、便捷开发:提供更符合 React 理念的事件处理方式
- 4、安全控制:防止事件滥用导致的 XSS 攻击
合成事件的实现原理
1. 事件注册
- 在 React 应用初始化时,React 会基于
ReactDOM.render
的容器节点(通常是 root)来监听所有支持的事件类型(如click
、change
等)。 - 实际上,React 并不是在一开始就注册所有事件,而是采用惰性注册:当第一次在组件中声明某个事件(比如
onClick
)时,React 才会在 root 上注册该事件类型。
2. 事件存储
- React 会维护一个映射,将事件类型与组件的事件处理函数关联起来。这个映射存储在组件的 fiber 节点上(具体是在 fiber 的
memoizedProps
和pendingProps
中)。
3. 事件触发
当事件在 DOM 上触发时(如用户点击),事件会冒泡到 root 节点。
- root 节点上绑定的事件监听器(由 React 注册)会被触发。
- React 通过事件对象(原生事件)的
event.target
找到实际触发事件的 DOM 节点。 - 然后,React 从这个 DOM 节点向上遍历,收集所有与事件类型相关的处理函数(例如,在
onClick
事件中,收集所有节点的onClick
处理函数)。这个遍历过程会考虑事件是否被阻止冒泡(stopPropagation
)。 - 接着,React 构造一个合成事件对象(
SyntheticEvent
),并依次调用收集到的事件处理函数
4. 事件对象池
- 为了提高性能,React 使用事件对象池。合成事件对象会被重用,在事件回调执行后,其属性会被置空。因此,如果需要在异步环境中使用事件对象(比如在 setTimeout 中),需要调用
event.persist()
来移除事件对象池中的引用,以便保留事件属性。
合成事件与原生事件的区别:
- 合成事件的命名采用小驼峰(camelCase),而不是纯小写。
- 在合成事件中,返回
false
不会阻止默认行为,必须显式调用preventDefault
。 - 由于事件委托,在组件卸载后,React 会自动移除事件处理,避免内存泄漏
js
// 支持的事件类型
const allNativeEvents = ["click"];
const elementEventPropsKey = "__props";
const listeningMarker = `_reactListening` + Math.random().toString(36).slice(2);
// 事件映射
function getEventCallbackNameFromtEventType(eventType) {
return {
click: ["onClickCapture", "onClick"],
}[eventType];
}
// 收集从目标元素到HostRoot之间所有目标回调函数
const collectPaths = (targetElement, container, eventType) => {
const paths = {
capture: [],
bubble: [],
};
// 收集事件回调是冒泡的顺序
while (targetElement && targetElement !== container) {
// div.__props = {onClickCapture:fn,onClick:fn....}
const eventProps = targetElement[elementEventPropsKey];
if (eventProps) {
const callbackNameList = getEventCallbackNameFromtEventType(eventType);
// 取出映射的事件
if (callbackNameList) {
callbackNameList.forEach((callbackName, i) => {
// react事件
const eventCallback = eventProps[callbackName];
if (eventCallback) {
if (i === 0) {
// 由于捕获的执行顺序是从上往下,所以是反向收集
// 反向插入捕获阶段的事件回调
paths.capture.unshift(eventCallback);
} else {
// 由于冒泡的执行顺序是从下往上,所以是正向收集
// 正向插入冒泡阶段的事件回调
paths.bubble.push(eventCallback);
}
}
});
}
}
targetElement = targetElement.parentNode;
}
return paths;
};
const dispatchEvent = (rootContainerElement, eventType, e) => {
const targetElement = e.target; // 事件源
if (targetElement === null) {
console.error("事件不存在target", e);
return;
}
// 从当前的事件源向上查找所有的,扑捉、冒泡事件,用于模拟原生的事件
const { capture, bubble } = collectPaths(
targetElement,
rootContainerElement,
eventType
);
// 合成原生事件
const se = createSyntheticEvent(e);
triggerEventFlow(capture, se);
// 如果事件被阻止不需要在执行
if (!se.__stopPropagation) {
triggerEventFlow(bubble, se);
}
};
export function listenToAllSupportedEvents(rootContainerElement) {
//监听根容器,也就是div#root只监听一次
if (!rootContainerElement[listeningMarker]) {
rootContainerElement[listeningMarker] = true;
allNativeEvents.forEach((eventType) => {
// 注册代理事件
rootContainerElement.addEventListener(eventType, (e) => {
dispatchEvent(rootContainerElement, eventType, e);
});
});
}
}
function createSyntheticEvent(e) {
const syntheticEvent = e;
syntheticEvent.__stopPropagation = false;
// 阻止事件传递
const originStopPropagation = e.stopPropagation;
syntheticEvent.stopPropagation = () => {
syntheticEvent.__stopPropagation = true;
if (originStopPropagation) {
originStopPropagation();
}
};
return syntheticEvent;
}
// 执行react事件
const triggerEventFlow = (paths, se) => {
for (let i = 0; i < paths.length; i++) {
const callback = paths[i];
callback.call(null, se);
if (se.__stopPropagation) {
break;
}
}
};
// 将支持的事件回调保存在DOM中
export const updateEventProps = (node, props) => {
node[elementEventPropsKey] = node[elementEventPropsKey] || {};
allNativeEvents.forEach((eventType) => {
// 获取支持的事件类型
const callbackNameList = getEventCallbackNameFromtEventType(eventType);
if (!callbackNameList) {
return;
}
// 事件映射
callbackNameList.forEach((callbackName) => {
if (Object.hasOwnProperty.call(props, callbackName)) {
node[elementEventPropsKey][callbackName] = props[callbackName];
}
});
});
return node;
};
react16 版本事件 bug
比如需要一个事件来控制一个元素的展示,在当前组件注册一个原生的 document 冒泡事件,在处理合成事件让元素展示的时候,如果原生事件处理的是隐藏,其结果就是元素无法展示
出现的原因是 react 冒泡事件先注册,注册在 document,所以 react 冒泡事件先执行,后执行原生的 document 冒泡事件,所以无法展示,通过阻止默认行为(冒泡),无效,因为是同级节点,只能阻止上级
js
// react16版本
function App() {
let [visibility, setVisibility] = useState(false);
// 注册冒泡事件
document.addEventListener("click", () => {
setVisibility(false);
});
let handlerClick = (e: any) => {
setVisibility(true);
// 阻止默认行为(冒泡),只能阻止上级,不能阻止同级
e.stopPropagation();
// 阻止所有
e.nativeEvent.stopImmediatePropagation();
};
return (
<div onClick={handlerClick}>
{visibility && "visibility"}
点击
</div>
);
}
context 用作原理
用作:数据共享,可以避免 props 一层成传递,可以实现跨组件数据共享
原理
- 1、在创建 createContext 时,会返回一个 context 对象,包含内容提供者(Provider)和消费者(Consumer),他们都有一个_context 属性,都指向同一个引用地址
- 2、react 在渲染时,如果是 Provider 组件,会给组件
_context._currentValue
进行赋值,在渲染当前的 children 组件 - 3、react 在渲染时,如果是 Consumer 组件,会取出
_context._currentValue
的值,传递给子组件(因为指向同一个引用地址,所以值是一样的) - 4、如果是类组件,有 contextType 属性,会对类上的 context 进行赋值
js
// 渲染provider
function mountProviderComponent(vdom) {
let { type, props } = vdom; // {$$typeof: REACT_PROVIDER,_context: context,};
// 引用赋值
type._context._currentValue = props.value;
// 用于下次新老做对比(渲染的其实是子元素)
vdom.oldRenderVdom = props.children;
if (!vdom.oldRenderVdom) return null;
return createDOM(props.children);
}
js
// 处理函数类件
function mountClassComponent(vdom) {
let { type: ClassComponent, props, ref } = vdom;
let classInstance = new ClassComponent(props);
vdom.classInstance = classInstance;
// 如果有contextType,把context绑定在实例上
if (ClassComponent.contextType) {
classInstance.context = ClassComponent.contextType._currentValue;
}
// .....
return dom;
}
js
function createContext() {
let context = { $$typeof: REACT_CONTEXT };
// 提供者
context.Provider = {
$$typeof: REACT_PROVIDER,
_context: context,
};
// 消费者
context.Consumer = {
$$typeof: REACT_CONTEXT,
_context: context,
};
return context;
}
this.state = {
color: "red",
};
const Context = React.createContext();
<Context.Provider value={{red: this.state.color}}>
<div>{this.state.color}</div>
</Context.Provider>
<Context.Consumer>
{(props) => {
return (
<div>
<span style={{ color: props.red }}>main</span>
</div>
);
}}
</Context.Consumer>
React 优化方案( shouldComponentUpdate,PureComponent,memo,useMemo,useCallback)
- 类组件
- shouldComponentUpdate 组件在更新之前会触发该生命周期,会把上次的 props、state 传递过来,可以根据自己的需求进行对比,如果返回 true,执行更新,如果 false 不更新组件
- PureComponent 内部实现了 shouldComponentUpdate
- 函数组件
- memo 函数组件在更新之前,对比之前的 props,如果未发生变化不执行更新操作
- 如果未提供自定义对比函数,走 memo 默认的 arePropsEqual,React 将对 props 进行浅层的对比。这意味着如果父组件重新渲染并传递相同的 props 对象引用,则子组件不会重新渲染
- useMemo 缓存值
- useCallback 缓存函数
- memo 函数组件在更新之前,对比之前的 props,如果未发生变化不执行更新操作
React.memo 默认比较策略
- React.memo 默认比较函数是浅比较,如果 props 是对象,比较的是引用地址,如果引用地址相同,则不更新组件,如果引用地址不同,遍历对象的第一层,如果 key,value 都相同,则不更新组件,如果不同,则更新组件
js
function shallowEqual(objA, objB) {
// 1. 相同引用检查
if (objA === objB) return true;
// 2. 如果有一个是 null,则不相等(重新渲染)
if (
typeof objA !== "object" ||
objA === null ||
typeof objB !== "object" ||
objB === null
) {
return false;
}
// 3. 键数量检查
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) return false;
// 4. 键值对检查(仅第一层)
for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
// 检查键是否存在
if (!Object.prototype.hasOwnProperty.call(objB, key)) {
return false;
}
// 比较属性值(如果对象有嵌套,一定不会相等)
// {a:10:b:{}}
// {a:10:b:{}}
if (objA[key] !== objB[key]) {
return false;
}
}
return true;
}
setState
- 在 React 事件中调用 setState,会进行批量更新策略,多个 setState 会进行合并
- 在原生事件和异步任务中调度 setState 不会走批处理,相等于同步执行,里面能获取到新的值
js
// 初始状态:count = 0
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
// 最终结果:count = 1 (不是3!)
js
// 使用函数形式确保基于最新状态
this.setState((prevState) => ({ count: prevState.count + 1 }));
this.setState((prevState) => ({ count: prevState.count + 1 }));
this.setState((prevState) => ({ count: prevState.count + 1 }));
// 最终结果:count = 3 ✅
js
handleClick = () => {
this.setState({ count: 42 });
console.log(this.state.count); // 输出旧值,不是42!
// 使用回调获取更新后状态
this.setState({ count: 43 }, () => {
console.log(this.state.count); // 输出43 ✅
});
};
为什么会出现 hooks
- 使得函数组件可以拥有类组件的特性,如状态管理、生命周期方法等,使得函数组件更加灵活和强大。
- 使得函数组件可以更加简洁和易读,避免了类组件中繁琐的生命周期方法和 this 绑定问题。
- 使得函数组件可以更加方便地复用状态逻辑,避免了类组件中繁琐的 mixin 和高阶组件等模式。
- 使得函数组件可以更加方便地测试,避免了类组件中繁琐的 this
hooks 原理
TIP
类组件中的 setState 是状态合并策略 hooks 队列中有多个时,状态不会进行合并,是直接覆盖更新,后者覆盖前者
- 1、React 在构建 Fiber 时,如果是函数组件首先会调用
renderWithHooks
进行初始化 - 2、构建全局 hooks
ReactCurrentDispatcher
派发器,如果是首次渲染挂载 MountHooksDispatcherOnMount
、如果是更新挂载 UpdateHooksDispatcherOnUpdate
(派发器就是提供给组件调用的如:useState...) - 3、当函数组件在被调用时,如果是初次渲染,会根据调用的情况,构建 hooks 链表
- 4、每一个 hook 都会创建一个更新的队列和一个 dispatchSetState 函数
并且已经绑定 fiber, hook队列
,并且 next 指向下一个 hook - 5、当 hook 被提交时,其实是调用的 hooks.dispatchSetState 函数,此时会将提交的 新的 State 存储在 hook 队列中,并且触发组件重新渲染
- 6、当组件重新渲染时,React 会依据 hooks 链表,按照 hooks 的顺序依次执行对应的函数,从而实现对应的特性。
hooks 注意事项
- 因为函数组件所有的 hook,以链表的形式存储的 fiber 中,当函数组件在更新的时候,会按照链表 next 一个个进行查找
- 如果在条件中使用 hooks、或者在循环中使用 hooks,会导致无法获取到正确的 hook 链表,从而出现 bug
useMemo 和 useCallback 区别
应用场景如需要缓存的函数,因为函数式组件每次任何一个 state 发生变化,会触发整个组件更新,一些函数是没有必要更新的,此时就应该缓存起来,提高性能,减少对资源的浪费;另外还需要注意的是,useCallback 应该和 React.memo 配套使用,缺了一个都可能导致性能不升反而下降。
相同点
- 1、useMemo 和 useCallback 都是 reactHook 提供的两个 API,用于缓存数据,优化性能;
- 2、两者接收的参数都是一样的,第一个参数表示一个回调函数,第二个表示依赖的数据。
- 3、在依赖数据发生变化的时候,才会调用传进去的回调函数去重新计算结果,起到一个缓存的作用
两者的区别
- 1、useMemo 缓存的结果是回调函数中 return 回来的值,主要用于缓存计算结果的值,应用场景如需要计算的状态
- 2、useCallback 缓存的结果是函数,主要用于缓存函数
js
function useMemo(factory, deps) {
let hook =
workInProgressFiber.alternate &&
workInProgressFiber.alternate.hooks &&
workInProgressFiber.alternate.hooks[hookIndex];
let currentMemo = null;
if (hook) {
let [lastMemo, lastDeps] = hook;
// 检测依赖的属性是否发生变化
let same = deps.every((item, index) => item === lastDeps[index]);
if (same) {
// 依赖的属性未发生变化返回之前的值
currentMemo = lastMemo;
} else {
// 返回新的值
currentMemo = factory();
}
} else {
// 创建新的
currentMemo = factory();
}
workInProgressFiber.hooks[hookIndex++] = [currentMemo, deps];
return currentMemo;
}
js
function useCallback(callback, deps) {
let hook =
workInProgressFiber.alternate &&
workInProgressFiber.alternate.hooks &&
workInProgressFiber.alternate.hooks[hookIndex];
let currentCallback = null;
if (hook) {
let [lastCallback, lastDeps] = hook;
let same = deps.every((item, index) => item === lastDeps[index]);
if (same) {
currentCallback = lastCallback;
} else {
currentCallback = callback;
}
} else {
currentCallback = callback;
}
workInProgressFiber.hooks[hookIndex++] = [currentCallback, deps];
return currentCallback;
}
useEffect 和 useLayoutEffect 区别
相同点
- 1、useEffect 和 useLayoutEffect 都是 reactHook 提供的两个 API,可以模拟类组件的部分生命周期
- 2、两者接收的参数都是一样的,第一个参数表示一个回调函数(函数可以返回一个函数),第二个表示依赖的数据。
- 3、在依赖数据发生变化的时候,才会调用传进去的回调函数,如果上次之前的函数有返回函数,会在执行回调函数之前执行
两者的区别(执行回调的时机不用)
- 1、 useEffect 是在 dom 更新后异步执行
- 2、 useLayoutEffect 是在 dom 更新前同步执行(有可能会阻塞 dom 渲染),执行完再更新 dom(useLayoutEffect 可以解决更新 dom 时候屏幕闪烁的问题)
js
function useEffect(callback, deps) {
let hook =
workInProgressFiber.alternate &&
workInProgressFiber.alternate.hooks &&
workInProgressFiber.alternate.hooks[hookIndex];
if (hook) {
let [lastCallback, lastDeps] = hook;
let same = deps && deps.every((item, index) => item === lastDeps[index]);
// 如果关联的deps未发生变化不做任何操作
if (same) {
workInProgressFiber.hooks[hookIndex++] = [lastCallback, deps];
} else {
// 如果发生变化,执行销毁的回调
lastCallback && lastCallback();
setTimeout(() => {
// 存储新的
workInProgressFiber.hooks[hookIndex++] = [callback(), deps];
});
}
} else {
setTimeout(() => {
// callback 返回的函数,在下次渲染前执行
workInProgressFiber.hooks[hookIndex++] = [callback(), deps];
});
}
}
js
function useLayoutEffect(callback, dependencies) {
let hook =
workInProgressFiber.alternate &&
workInProgressFiber.alternate.hooks &&
workInProgressFiber.alternate.hooks[hookIndex];
if (hook) {
let [lastCallback, lastDeps] = hook;
let same =
dependencies &&
dependencies.every((item, index) => item === lastDeps[index]);
if (same) {
workInProgressFiber.hooks[hookIndex++] = [lastCallback, dependencies];
} else {
lastCallback && lastCallback();
// DOM 更新完成后,浏览器绘制之前
queueMicrotask(() => {
lastCallback = callback();
workInProgressFiber.hooks[hookIndex++] = [lastCallback, dependencies];
});
}
} else {
// DOM 更新完成后,浏览器绘制之前
queueMicrotask(() => {
workInProgressFiber.hooks[hookIndex++] = [callback(), dependencies];
});
}
}
useRef&createRef 区别
- 1、都可以在组件中创建
ref对象
,createRef
在类
组件函数
组件都可以使用,useRef
只能在函数
组件中使用 - 2、在函数组件使用
createRef
时,每次组件更新都会重新去初始化(指向不同的引用地址) - 3、
useRef
存储在fiber
链表中,每次组件在更新不会重新去初始化(指向相同的引用地址)
js
// mount 阶段
function mountRef<T>(initialValue: T): {| current: T |} {
// 获取 hook 对象
const hook = mountWorkInProgressHook();
const ref = { current: initialValue };
if (__DEV__) {
Object.seal(ref);
}
// 存储
hook.memoizedState = ref;
return ref;
}
// update 阶段
function updateRef<T>(initialValue: T): {| current: T |} {
const hook = updateWorkInProgressHook();
// 返回之前的引用
return hook.memoizedState;
}
useRef&useState 区别
- useState 在更新的时候
会
导致组件重新渲染 - useRef 在更新的时候
不会
导致组件重新渲染
useRef 可以解决闭包陷阱问题
js
// 1、先点击第一个按钮6次
// 2、在点击第二个按钮1次
// 3、立马在点击第一个按钮4次
import { useState } from "react";
const App = () => {
// setState更新会导致组件更新
const [state, setState] = useState(0);
function handleClick() {
setTimeout(() => {
// 4、由于闭包的特性,当前的值是6,所以打印6
console.log(state);
}, 3000);
}
return (
<>
{/* setState更新会导致组件更新所以是10 */}
<button onClick={() => setState(state + 1)}>{state}</button>
<button onClick={handleClick}>print</button>
</>
);
};
export default App;
js
// 1、先点击第一个按钮6次
// 2、在点击第二个按钮1次
// 3、立马在点击第一个按钮4次
import { useRef } from "react";
const App = () => {
const state = useRef(0);
function handleClick() {
setTimeout(() => {
// 4、由于都是指向同一个引用地址所以是10
console.log(state.current);
}, 3000);
}
return (
<>
<button
onClick={() => {
state.current++;
}}
>
{/* useRef值的改变不会导致组件更新,所以一直是0 */}
{state.current}
</button>
<button onClick={handleClick}>print</button>
</>
);
};
export default App;
如何拿到 useState 更新的值
- 1、可通过
useEffect
,将更新值作为依赖条件 - 2、在更新 useState 之前,先计算值在更新 useState
forwardRef(组件转发)
WARNING
- 如果对函数组件,使用
ref
会抛出如下警告,因为函数组件
和类型组件
不同,没有实例所以对函数组件直接使用ref
是毫无意义的 - Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
- forwardRef 可以让父组件的
ref
获取到子组件的 DOM,同时也可以配合useImperativeHandle
,将子组件的函数暴露给父组件ref
js
import React, { useEffect, useRef, forwardRef } from "react";
const InputText = (props: any, ref: any) => {
return (
<>
<div>
<input ref={ref} />
</div>
</>
);
};
const ForwardInput = forwardRef(InputText);
const App: React.FC = () => {
const inputRef = useRef < HTMLInputElement > null;
useEffect(() => {
if (inputRef.current) {
// 获取到子组件的input
inputRef.current.focus();
}
});
return (
<>
<ForwardInput ref={inputRef}></ForwardInput>
</>
);
};
export default App;
- 1、forwardRef 是一个函数组件,它接收一个组件,返回一个对象,包含
$$typeof
类型,和render
(render 是外部传递的组件), - 3、当组件在渲染的时候,内部会保留父组件传递的
ref的引用
,如果是$$typeof
类型是REACT_FORWARD_REF
,调用render
渲染组件,并且把 props,ref 传递给对应的组件
js
function forwardRef(render) {
return {
$$typeof: REACT_FORWARD_REF,
render,
};
}
if (type && type.$$typeof === REACT_FORWARD_REF) {
return mountForwardComponent(vdom);
}
function mountForwardComponent(vdom) {
const { type, props, ref } = vdom;
const renderVdom = type.render(props, ref);
if (!renderVdom) return null;
return createDOM(renderVdom);
}
useImperativeHandle
- 1、useImperativeHandle 其实就是一个普通的函数,接受
2个参数
一个是父组件的ref
,另外是一个函数返回一个对象 - 2、当 useImperativeHandle 在被调用时,内部会调用第二个参数将返回的对象,存储在父组件
ref.current
- 3、因为是同一个引用地址,所以通过父组件的
ref.current
能够调用到子组件的定义的函数
js
import React, {
useEffect,
useRef,
forwardRef,
useImperativeHandle,
} from "react";
interface IInputText {
onClick: () => void;
}
const InputText = (props: any, ref: any) => {
useImperativeHandle(ref, () => {
return {
onClick: () => {
console.log("子组件的函数");
},
};
});
return (
<>
<div></div>
</>
);
};
const ForwardInput = forwardRef(InputText);
const App: React.FC = () => {
const inputRef = useRef < IInputText > null;
useEffect(() => {
if (inputRef.current) {
inputRef.current.onClick();
}
});
return (
<>
<ForwardInput ref={inputRef}></ForwardInput>
</>
);
};
export default App;
js
// 实现
export function useImperativeHandle(ref, handler) {
ref.current = handler();
}
react 新增了什么生命周和删除了什么生命周期,为什么要删除
- 1、componentWillMount、2、componentWillUpdate 3、componentWillReceiveProps
- React 废弃的这三个生命周期函数,大都是因为新版本的异步渲染,在调用 render 生成虚拟 DOM 阶段,由于更高级的操作到来而被打断,导致 render 之前的操作都会重来。使得之前的逻辑可能会被重复调用而弃用。
Vue vs React
Vue 使用模版拥抱 html
Vue 是真正意义上的做到了组件级更新,每一个组件就对应一个渲染的 effect,让响应式数据去收集 effect
Vue 采用的是递归的方式来渲染页面不可中断
React 使用的 JSX 拥抱 JS
React 更新都是从跟节点开始调度,会讲一个大的任务拆分成多个小的任务单元
React 更新策略是循环的方式,有任务优先级的概念,可中断执行
React 错误处理componentDidCatch
、useErrorBoundary
js
import React, { Component } from "react";
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
this.state = { hasError: true };
}
render() {
if (this.state.hasError) {
// 显示备用 UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
function App() {
return (
<div>
<ErrorBoundary>
<h1>Hello, world!</h1>
</ErrorBoundary>
</div>
);
}
js
function ErrorBoundary({ children }: { children: ReactNode }) {
const [errorMsg, updateError] = (useState < Error) | (null > null);
useErrorBoundary((e: Error) => {
updateError(e);
});
return <div>{errorMsg ? "报错:" + errorMsg.toString() : children}</div>;
}