入口: const root = ReactDOM.createRoot(document.getElementById('root')) root.render(类组件) ⬇️ 类组件内部 render() { return ( <div>12</div> )} ⬇️ (经过babel-preset-react-app 把jsx语法,编译为h函数形式) React.createElement('div', null, '12') ⬇️ ( React.createElement返回值是一个普通 JavaScript 对象,即 React 元素) 该元素是对UI结构以及属性的描述。 { type: 'div', props: { children: '123' }, key: null, ref: null, _owner: null, _store: {} } ⬇️ 然后render 将 react元素 生成为虚拟DOM 再由react内部将虚拟dom构建为树,进行diff计算等,再创建真实dom ⬇️ componentDidMount生命周期钩子 组件挂载完成
代码定义 → JSX转换 → 创建元素 → Fiber树构建 → 协调(Diff) → DOM更新 → 生命周期回调
class vote extends React.Component{
render() {
return (<div>
....
</div>
}
}
- 使用vote组件
import vote
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<Vote {title = 'xxx'}/>)
- ❗️‼️注意: react18中,setState 本身并不是传统意义上的“异步操作”(如 Promise 或 setTimeout),而是
批量更新(Batching)机制
的表现。React 内部维护一个更新队列
,在 同一渲染周期内 的多个 setState 会被合并处理,多个状态更新合并(批量处理)后再统一计算新状态和触发渲染,这可能导致状态更新看起来“延迟生效”。
所以本文中讲的异步操作都理解为批量更新(Batching)机制
- 🆘 react16 的setState 在
合成事件
又有区别
handleVue() {
console.log(this)
}
handleVue2 = (x, y) => {
console.log(x, y, this)
}
render() {
console.log('渲染完成')
return (
<div className="vote">
<button onClick={this.handleVue}>vue</button> {/* this指向undefined */}
<button onClick={() => this.handleVue()}>vue</button> {/* this指向组件实例 */}
<button onClick={this.handleVue.bind(this)}>vue</button> {/* this指向组件实例 */}
<button onClick={this.handleVue2.bind(1, 2)}>vue</button> {/* this指向组件实例,且预传参 */}
</div>
)
}
- 合成事件的事件委托机制
合成事件都会委托给入口文件的挂载结点 #root上,合成事件与原生事件能在同一节点并行,到那时合成事件的绑定要早于原生事件,因为合成事件是react内部绑定的。
‼️ 注意:
事件传播分离:合成事件与原生事件的传播路径独立。阻止合成事件冒泡不会影响原生事件,反之亦然。
- purecomponent 是基于堆内存地址的浅比较,一个对象的某个深层属性更改,它判定为地址不变,不会触发页面更新 ❌,这是错误的。所以它不适合深层次的属性的 state 或 props 的组件。
💯Hooks组件
💝 this: 因为类组件
需要 react内部new 来实例化,所以要处理this绑定的问题,而函数组件
是 纯函数,每次渲染都会重新执行该函数组件,产生一个私有上下文,所以不涉及this的处理。
❤️🔥 想了解hooks组件的更新机制,先理解一下Fiber
Fiber 是 React 维护的一种数据结构,包含组件相关的所有信息。
每个React (函数 ,类) 组件都对应 一个Fiber 节点,所有的组件组成了 完整的Fiber树。
- React 维护两棵 Fiber 树:Current Tree(当前屏幕显示)和 WorkInProgress Tree(正在构建的更新)。
- 状态变更时,两棵树变更的节点 进行对比,复用大部分属性(如 type、key),仅更新变化的 props 或 state。
fiber = {
type: ComponentFunction, 组件类型(函数组件)
memoizedState: hook1, **指向 Hook 链表的头部**
stateNode: {}, 组件实例(类组件)或 DOM 节点
// 其他调度相关属性(如优先级、副作用链表等)
}
❤️🔥 再了解一下Hook
Hook(钩子),eg:useState
为一个hook函数,它的每一次调用,都会生成一个React内部维护的hook对象
const [count, setCount] = useState(0)
hook对象 = {
memoizedState: 0, 当前页面展现的值
baseState: 0, 所有更新(queue)处理后的新基准.
当所有已处理的更新(queue 中的更新)被消费后,
baseState 会被更新为最新的 memoizedState
**但是** 渲染被中断(如高优先级任务插队),
React 会回退到 baseState 重新计算状态。
queue: null, 更新队列(存储 setCount 触发的更新动作)
next: null, 指向下一个 Hook 的指针
};
hook对象的queue队列作用如下:
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(1);
setCount(2);
};
// handleClick()后的hook对象
hook = {
memoizedState: 0, // 当前状态值
baseState: 0, // 基础状态(初始值)
queue: {
setCount(1),
setCount(2);
}, // 更新队列
next: null, // 指针
}
每次调用hook的副作用函数后,就会加入到对应hook对象的更新队列中,等待react 内部更新。
每一个hook函数自上而下产生的hook对象 ,都会以链表的形式维护在 fiber
的memoizedState
中
function Component() {
const [name, setName] = useState("Alice"); hook对象1
const [age, setAge] = useState(30); hook对象2
const [location, setLocation] = useState("Nowhere"); hook对象3
}
首次渲染(Mount)链表指向
Fiber.memoizedState → Hook对象1 → Hook对象2 → Hook对象3
后续渲染(Update)时,复用hook对象
Fiber.memoizedState → Hook对象1 → Hook对象2 → Hook对象3
React 再次执行组件函数,按相同顺序调用 useState
但不会重新创建 Hook 对象,而是按顺序遍历 Fiber链表
💘 setState对象形式 通过「闭包」链接hook对象
为什么要闭包?
闭包的作用是确保同一组件实例在不同渲染周期中的状态和上下文被正确隔离
是实例状态持久化的唯一手段。
每一个useState 执行产生的hook对象,hook中变量对外暴露,外部引用了该变量, 为一个闭包的产生。
const [count, setCount] = useState(0) 来自React内部存储的count,而不是组件函数的局部变量。
const handleClick = () => {
setCount(count + 1) 引用了count,闭包产生
}
useState 简要更新逻辑
let hook = [
memoizedState: any, // 当前状态值(对于 useState,保存的是 state)
baseState: any, // 基础状态(用于计算更新)
baseQueue: Update<any> | null, // 待处理的更新队列
queue: UpdateQueue<any> | null, // 状态更新队列(保存 setState 触发的更新)
next: Hook | null,
];
function useState(newValue) {
// 定义 setState 函数
const setState = (newValue) => {
// 收集更新阶段:将更新加入当前 Hook 的队列
// 加入队列
hooks.queue.push(newValue);
------------------------------------------上下为两个不同的阶段
// 处理更新阶段
for (const update of hooks.queue) {
currentState = typeof update === 'function'
? update.action(currentState) // 函数式更新,传入当前状态
: newState = update.action; // 对象式更新,直接替换值
}
hooks.memoizedState = currentState; // 更新最终状态
hooks.queue = [];
// 触发重新渲染(模拟 React 的更新机制)
scheduleRender();
};
// 返回值 和 副作用函数 setState
return [hook.memoizedState, setState];
}
// 模拟重新渲染
function scheduleRender() {
currentHookIndex = 0; // 重置索引,准备下一次渲染
render();
}
✅:Fiber有了,hook对象有了,闭包有了,可以来梳理 hook 是如何让函数组件动态起来了。
1. 组件首次渲染(Mount):
- Fiber 创建 → 初始化 Hook 链表(useState) → setState 闭包绑定 Fiber 和 Hook。
2. 用户触发 setState:
- setState 通过闭包找到对应的 Hook → 将更新加入 Hook.queue。
→ 调度更新(scheduleUpdateOnFiber)。
3. React 调度更新:
- 进入渲染阶段 → 从 Fiber 读取 Hook 链表 → 按顺序处理每个 Hook 的队列。
→ 计算新状态 → 更新 Hook.memoizedState。
4. 组件函数重新执行-渲染(Update):
- 使用最新的状态生成新 VDOM → 协调(Reconciliation) → 提交(Commit)到 DOM。
‼️ useState 返回的变量, 是函数组件外, React内部状态在该次渲染中的状态值快照
当组件执行,并引用了这个变量时,闭包就会形成,
所以调用栈的同步代码,或是将要进入任务队列的回调,都会提前绑定好该闭包的引用
当任务队列的事件推到调用栈时,直接拿到前面已经绑定的闭包值来使用。
useState 在每次组件渲染时都会被调用,但不会重新初始化状态(除非组件重新挂载)。它的作用是返回当前状态值,而不是重新执行初始化逻辑
‼️ 注意
执行到 setstate() 这一行时,会把它注册到 hook.queue 对象中,且不会立马执行 (该hook由react维护,不进入堆栈,任务队列)
接着执行后续代码,同步的直接执行,异步的进入队列
当调用栈清空
此时hook.queue 会先于任务队列执行,
当hook.queue中的更新动作全部消费完,就会触发组件重新调用渲染,
然后再执行刚刚加入任务队列的事件。
💝所以,可以理解为一轮事件周期内,queue中的setstate() 的消费,要快于任务队列。
❤️🔥 看代码理解
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
setTimeout(() => {
console.log(count, 'count')
})
}
- handleclick 入调用栈
- setcount调用,进入hook.queue 等待批量更新,引用了count ,在堆中产生count的闭包
- settimeout 的回调绑定count的闭包引用(定义时绑定),再加入任务队列
- 消费hook.quene ,触发Fiber调度,更新count,渲染页面
- 拉取任务队列的定时器回调,并执行。
🧪 setState 函数式更新
与对象式不同的唯一一点就是变量的获取
- 对象式直接是绑定闭包引用来进行计算
- 而函数式, 回调函数在链表中执行时,永远上一个的执行后的pedding状态,是下一个回调任务的参数。称为链式状态传递。
⏰ 简单的 setState 更新模型。
flushsync
flushSync会绕过 React 的自动批处理,强制同步处理状态更新,但仅在新旧状态值实际变化时触发组件渲染。
⚠️ useState 自带性能优化
短路优化:当新状态与旧状态相同(Object.is 比较)时,React 会跳过渲染。
更新队列同一批中,多个函数式的,会基于前一个pending的状态更改(不论是否函数式), 将最新值穿给下一个,最后触发一次更新渲染
例如,假设当前count是0: 调用setCount(0 +1) → 计划更新到1 接着调用setCount(prev => prev +10),这里的prev是前一个pending计划的状态,即1,所以结果为11。
更新队列同一批中,多个对象形式的,后面的覆盖前面的,仅采取最后一次action触发一次更新渲染
export default function Counter() {
console.log('render')
const [count, setCount] = useState(0)
const handleClick = () => {
for (let index = 0; index < 10; index++) {
setCount(count + 5)
}
}
1:点击按钮,render执行几次?count最终为?
export default function Counter() {
console.log('render')
const [count, setCount] = useState(0)
const handleClick = () => {
for (let index = 0; index < 10; index++) {
setCount(count + index)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>按钮</button>
</div>
)
}
2:点击按钮,render执行几次?count最终为?
export default function Counter() {
console.log('render')
const [count, setCount] = useState(0)
const handleClick = () => {
for (let index = 0; index < 10; index++) {
setCount(prev => prev + 10)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>按钮</button>
</div>
)
}
3:点击按钮,render执行几次?count最终为?
export default function Counter() {
console.log('render')
const [count, setCount] = useState(0)
const handleClick = () => {
for (let index = 0; index < 10; index++) {
setCount(prev => prev + index)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>按钮</button>
</div>
)
}
4:点击按钮,render执行几次?count最终为?
export default function Counter() {
console.log('render')
const [count, setCount] = useState(0)
const handleClick = () => {
for (let index = 0; index < 10; index++) {
flushSync(() => {
setCount(count + 1)
})
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>按钮</button>
</div>
)
}
5:点击按钮,render执行几次?count最终为?
export default function Counter() {
console.log('render')
const [count, setCount] = useState(0)
const handleClick = () => {
for (let index = 0; index < 10; index++) {
flushSync(() => {
setCount(prev => prev + index)
})
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>按钮</button>
</div>
)
}
6:点击按钮,render执行几次?count最终为?
export default function Counter() {
console.log('render')
const [count, setCount] = useState(0)
const handleClick = () => {
for (let index = 0; index < 10; index++) {
setCount(count + 1)
setCount(prev => prev + 10)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>按钮</button>
</div>
)
}
6:点击按钮,render执行几次?count最终为?
export default function Counter() {
console.log('render')
const [count, setCount] = useState(0)
const handleClick = () => {
for (let index = 0; index < 10; index++) {
flushSync(() => {
setCount(count + index)
})
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>按钮</button>
</div>
)
}
7:点击按钮,render执行几次?count最终为?