React原理(暴力版)

JSX 的本质是什么?

JSX 就他妈是个语法糖!

狗屁原理就这:

  1. JSX 本质就是个 React.createElement()的替身 ,你写的 <div>🐴👴</div>最后全被 babel 这崽种转成 React.createElement('div', null, '🐴👴')
  2. 吹得花里胡哨的"HTML in JS"实际是扯犊子,本质就是 JS 对象(虚拟 DOM),浏览器根本不认这玩意,得靠 React 这老铁给你渲染成真 DOM。
  3. 劳资写 JSX 就是为了爽 ,省得手写一坨 createElement跟特么裹脚布似的,但真当老子不知道你背地里是函数调用?

总结:JSX 就是 React 给程序员戴的 虚拟涩涩 ,看着像 HTML 其实是 JS 函数套皮,不服你咬它? 😅

(注:解释仅为节目效果,JSX 本质是 JavaScript 的语法扩展,通过编译转换为 React 元素描述对象。)

如何理解 React Fiber 架构?

一、Fiber 之前:老架构的痛点

老版 React 的渲染是纯莽夫行为 :递归更新整个组件树,一旦开干就停不下来,跟尼玛打桩机似的突突突到底。16ms 渲染不完?主线程直接卡成 PPT,用户操作全被阻塞,界面跟死妈了一样没反应。

二、Fiber 核心思想:老子现在会分片打工

1. Fiber 节点 = 有记忆的打工仔

js 复制代码
// 以前:一口气干到底的愣头青
function render(component) { /* 不干完不休息 */ }

// Fiber:知道自己干到哪的聪明逼
const fiber = {
  type: Component,      // 要干啥活
  stateNode: instance,  // 干活的本体
  return: parentFiber,  // 爹是谁
  child: firstChild,    // 第一个儿子
  sibling: nextSibling, // 下一个兄弟
  alternate: alternateFiber, // 上回干的备份
  memoizedProps: {},    // 上次用的参数
  memoizedState: {},    // 上次的状态
  flags: Placement,     // 这次要干啥操作
};

2. 双缓存机制:玩得就是一手千层饼

  • Current Tree:当前屏幕上显示的,稳如老狗
  • WorkInProgress Tree:后台偷偷构建的新版本
  • 搞定了就 瞬间切换,用户只看到"卧槽怎么突然好了"

三、Fiber 的核心骚操作

1. 可中断的渲染过程

js 复制代码
// React 现在会看时间打工:
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    // 还有时间就继续干
    performUnitOfWork(workInProgress);
  }
  
  // 时间到了?行行行先让给用户交互
  requestIdleCallback(workLoopConcurrent);
}

2. 优先级调度:插队是门艺术

  • Immediate:紧急更新,比如输入响应
  • UserBlocking:用户交互,要点面子
  • Normal:普通更新,排队去
  • Low:不重要的更新,边上凉快去
  • Idle:闲得蛋疼时才处理

四、Fiber 的实际收益

1. 时间切片(Time Slicing)

  • 把渲染拆成 5ms 小任务
  • 每干完一片就看看:"兄弟,用户有操作吗?有就先让路"
  • 用户体验从"这破应用又卡了"变成"哎呦还挺流畅"

2. 并发模式(Concurrent Mode)

  • 多个更新可以同时准备
  • 高优先级更新可以插队
  • Suspense 等高级功能的基础

五、简单比喻

老版 React:一本 1000 页的书,必须一口气读完,憋尿也得读完。

Fiber React:同一本书,但:

  1. 每读 5 分钟就抬头问:"要喝水吗?要上厕所吗?"
  2. 突然有急事?马上夹书签,处理完回来接着读
  3. 重要的章节优先读,不重要的往后稍稍

六、Fiber 的代价

代码复杂度指数级上升:React 团队头发-10086

打包体积增加:为了这波高级功能,React 胖了 10kg

调试难度 up:现在你要理解链表、优先级、双缓冲...脑细胞阵亡警告


总结 :Fiber 就是让 React 从一根筋的铁憨憨 变成了会看脸色、懂轻重缓急的社会人 。虽然底层复杂到亲妈都不认识,但对用户和开发者来说,就是应用突然变流畅了 ,高级功能突然能实现了

(React 团队:你知道我们这几年是怎么过的吗?头发都快薅秃了!)

Fiber 结构和普通 VNode 区别

一、VNode:传统死脑筋

VNode 就他妈是个傻白甜

  • 脑子里只有"我现在长啥样"

  • 每次更新都全盘重算,CPU 烧了关我屁事

  • 用户点按钮?等老子算完再说,卡死你活该

  • 遍历起来像野猪冲锋,不撞南墙不回头

    // 传统 VNode 内心戏:
    "我是谁?我在哪?我要渲染啥?"
    "哦要更新?行,把整棵树重新撸一遍"
    "用户你在操作?憋着!等我搞完!"

二、Fiber:带脑子的社会人

Fiber 就是个职场老油条

1. 会看脸色(优先级调度)

复制代码
if (用户输入) {
  priority = '最高';  // 爸爸的操作,立刻舔
} else if (数据更新) {
  priority = '中等';  // 正常干活
} else {
  priority = '摸鱼';  // 有空再说
}

2. 懂得存档(可中断恢复)

  • 干到一半老板喊?马上ctrl+s
  • 5ms 时间到?立即保存进度
  • 回来接着干,毛都不忘

3. 双面间谍(双缓冲机制)

  • 明面:Current Tree - 给用户看的假脸
  • 暗地:WorkInProgress - 后台偷偷整容
  • 整完了?瞬间换脸,用户还以为是美颜相机

三、工作方式对比

VNode 式工作(愣头青程序员):

复制代码
老板:改个按钮颜色
VNode:好!我重构整个项目!
(3小时后...)
VNode:改好了,顺便优化了亿点细节
老板:用户都跑光了,我CNM!

Fiber 式工作(职场老狗):

复制代码
老板:改个按钮颜色
Fiber:收到,标记为"小改动"
用户:点了一下输入框
Fiber:卧槽金主爸爸!立即暂停改颜色
(5ms内响应用户)
用户爽了,继续改颜色
Fiber:改完了,顺便看看有没有更重要的事

四、数据结构本质区别

VNode 是张照片

  • 每次重拍
  • 不会动
  • 没记忆
  • "我就一图纸,你爱咋咋"

Fiber 是活体监控

  • 记得上次的样子
  • 知道要改成啥样
  • 清楚兄弟爹妈都是谁
  • 暂停能继续
  • 给任务分三六九等

五、举个现实例子

刷抖音时加载列表

VNode 方案:

复制代码
你:下滑刷新
App:开始渲染100条新视频
你:点暂停
App:**装死中...**
你:我他妈点暂停啊!
App:等我渲染完这100条哈
你:手机已砸

Fiber 方案:

复制代码
你:下滑刷新
Fiber:开始渲染,但每5ms看一眼
你:点暂停
Fiber:**立即停手**,先处理暂停
你:嗯,反应挺快
Fiber:继续偷偷渲染剩下的

六、为什么 Fiber 这么复杂?

因为 React 不想当个"只会渲染的傻框架"

  1. 要支持时间切片 - 别卡用户
  2. 要玩并发更新 - 多个任务一起搞
  3. 要实现 Suspense - 异步加载不闪屏
  4. 要搞离线渲染 - 后台偷偷准备

代价 :代码复杂度上天,React 源码现在像天书,但用户只管爽。

七、一句话总结

VNode 是条狗:你扔飞盘,它必须捡回来才能干别的。

Fiber 是人精:你让它拖地,中途你妈喊吃饭,它会:

  1. 立即放下拖把
  2. 记住拖到哪了
  3. 先去吃饭
  4. 吃完饭继续拖
  5. 拖地时还接了个电话
  6. 所有事都办了,你还觉得它很闲

最后暴论

  • 用 VNode 的框架:我是个渲染器,别的别找我
  • 用 Fiber 的 React:我要当操作系统,调度一切,掌控雷电
  • Fiber vs VNode:一个是智能机器人,一个是铁憨憨

(React 团队:我们头发掉光写出来的 Fiber,就是为了让你们这些喷子感受不到卡顿,结果你们只关心"为什么打包体积大了 10KB"?)

简述 React diff 算法过程

一、基本原则:能不动就他妈不动

React 的信条

  1. 类型变了?直接掀桌重来 <div><span>整个子树全删了重做,懒得多看一眼
  2. key 都不加?那你活该低效 列表没 key?老子就当你们全是废物,全量对比,CPU 烧了关我屁事

二、Diff 三板斧

第一板斧:同级对比,不跨级装逼

复制代码
// 老树:<A> <B/> <C/> </A>
// 新树:<A> <D/> <E/> </A>

// React:只对比 A 的直接儿子
// 傻逼想法:拿 B 和 D 对比,C 和 E 对比
// 实际情况:<B/> → <D/> 类型都变了,滚去重建

第二板斧:列表加 key,不加是傻逼

复制代码
// 没 key 的憨批写法:
[<li>a</li>, <li>b</li>, <li>c</li>]
→
[<li>d</li>, <li>a</li>, <li>b</li>, <li>c</li>]

// React 看到:卧槽第一个从 a 变 d,第二个从 b 变 a...
// 结果:全他娘的重建,老子不干了!

// 有 key 的聪明写法:
[<li key="1">a</li>, <li key="2">b</li>, <li key="3">c</li>]
→
[<li key="4">d</li>, <li key="1">a</li>, <li key="2">b</li>, <li key="3">c</li>]

// React 看到:key="1" 从第一位移到第二位,移动就行
// 只新建一个 d,其他三个移动位置,CPU 狂喜

第三板斧:组件别乱变类型

复制代码
// 今天:<Button>提交</Button>
// 明天:<div>提交</div>
// React:你他妈逗我?组件类型都变了,里面内容再好也全删了重做!
// 性能?吃了!

三、Diff 具体怎么跑的

第一步:看标签名

复制代码
if (老节点.type !== 新节点.type) {
  // 类型不同?滚去重建整个子树
  unmount(老节点);
  mount(新节点);
  return; // 后面不看了,浪费时间
}
// 类型一样?行,接着看你肚子里货变了没

第二步:看属性(DOM 元素)

复制代码
// 老属性:{ className: 'old', id: 'app' }
// 新属性:{ className: 'new', id: 'app' }

// React 做法:
1. 遍历新属性,设置 className 为 'new'
2. 遍历老属性,发现 id 还在,不管
3. 发现 style 没了?删!
// 只改动的部分,没变的绝不碰

第三步:看儿子们(最头疼的)

情况1:文本儿子(简单得像弱智)
复制代码
// 老:<div>hello</div>
// 新:<div>world</div>
// 操作:直接替换文本,收工
情况2:数组儿子(考验 key 的时候到了)
复制代码
// 情况A:没 key(React 默认用 index 当 key)
[<div>A</div>, <div>B</div>]
→
[<div>C</div>, <div>A</div>, <div>B</div>]

// React 的猪脑子想法:
// 位置0:A → C,类型一样但内容变,更新
// 位置1:B → A,更新
// 位置2:undefined → B,新增
// 结果:更新2次,新增1次 → 傻逼效率

// 情况B:有 key
[<div key="a">A</div>, <div key="b">B</div>]
→
[<div key="c">C</div>, <div key="a">A</div>, <div key="b">B</div>]

// React 聪明了:
// key="c":新增
// key="a":从0移到1
// key="b":从1移到2
// 结果:移动2次,新增1次 → 这才是人干的事

四、Fiber 时代的 Diff 优化

Fiber 之前:递归 diff,不 diff 完不睡觉

Fiber 之后

复制代码
// 开始 diff
function beginWork(fiber) {
  // diff 5ms
  if (时间到了) {
    // 保存当前进度
    // 把控制权还给浏览器
    // 用户:哎呦不卡了
  }
  // 有时间了?接着 diff
}

五、给菜鸡的忠告

  1. key 用稳定 ID,别他妈用 index
  2. 组件类型别乱变,今天 Button 明天 div
  3. 该拆组件就拆,别一坨屎全堆一起
  4. shouldComponentUpdate/PureComponent/memo 用起来,别每次都重新拉屎

六、一句话总结

React Diff 就是个势利眼

  • 类型一样?行,看看你变了多少
  • 类型不同?滚,重建!
  • 有 key?好,尽量移动
  • 没 key?废物,全量对比,卡死你活该
  • React Diff 算法:老子打补丁比你妈缝衣服还讲究

最后 :React 团队费老大劲搞优化,结果你一个 key={index}全给干废了,你是对面派来的吧?

React 和 Vue diff 算法的区别

一、核心区别:React 是理想主义憨批,Vue 是现实主义鸡贼

React Diff:链表遍历 + 穷举对比

复制代码
// React 思路:
1. 看类型,不一样?**全删了重做**,老子不废话
2. 一样?**深度递归**,每个节点都看看
3. 列表?有 key 就尽量移动,没 key 就当废物全量对比
4. 完事了?不,还要搞个**副作用链表**,最后统一提交

// 实际表现:
- 第一轮:类型对比 → 不一样就掀桌
- 第二轮:属性对比 → 只改变的部分
- 第三轮:儿子对比 → 最耗CPU的地方
- 结果:**理论上最优,实际上憨批**(除非你严格按规矩来)

Vue Diff:双端对比 + 能复用就复用

复制代码
// Vue 思路:
1. 老子不管类型变不变,先看看能不能**就地复用**
2. 列表?**头头、尾尾、头尾、尾头**四种姿势先试一遍
3. 还不行?搞个**key映射表**,能复用的绝不新建
4. 实在没救了?**最小编辑距离**,能少动就少动

// 实际表现:
- 第一步:新老头节点一样?**直接复用**,指针后移
- 第二步:新老尾节点一样?**直接复用**,指针前移
- 第三步:老头 vs 新尾?**移动节点**到后面
- 第四步:老尾 vs 新头?**移动节点**到前面
- 第五步:都不行?**建个Map**找能复用的
- 结果:**能偷懒就偷懒,能复用就复用**

二、性能特点对比

React Diff:严格但傻快

复制代码
// 优点:
- 类型判断快,不一样直接砍,不bb
- Fiber 可中断,不卡用户
- 适合大型、稳定结构

// 缺点:
- 没 key 的列表就是灾难
- 组件类型一变,里面再优化也白给
- 开发者要懂规矩,不然性能吃屎

// 适用场景:
你团队全是高手,代码规范如军规

Vue Diff:灵活但狡猾

复制代码
// 优点:
- 能复用的绝不新建,省内存
- 双端对比,简单场景快如狗
- 开发者随便写,Vue 给你擦屁股

// 缺点:
- 极端情况可能不是最优
- 要维护映射表,额外内存
- 太"聪明"有时反被聪明误

// 适用场景:
你团队水平参差不齐,有人写屎山代码

三、举例说明

场景1:列表头插入

复制代码
// 老数组:[A, B, C]
// 新数组:[D, A, B, C]

// React(没 key):
A → D,更新
B → A,更新  
C → B,更新
null → C,新增
结果:3次更新 + 1次新增 → 憨批操作

// Vue:
1. 头头对比:A != D,不匹配
2. 尾尾对比:C == C,复用,指针移动
3. 尾尾对比:B == B,复用,指针移动
4. 尾尾对比:A == A,复用,指针移动
5. 剩个 D,新增到头部
结果:1次新增,3次移动 → 聪明多了

场景2:组件类型突变

复制代码
// React:
<Button>提交</Button> → <div>提交</div>
// React:类型变了!整个 Button 子树全删,div 新建
// 里面就算有 memo 也救不了,**斩立决**

// Vue:
<button>提交</button> → <div>提交</div>
// Vue:标签不同,但看看内容能不能复用?
// 文本节点"提交"一样,复用文本节点
// 只是换个标签,**留校察看**

四、设计哲学差异

React:我是你爹,你得听我的

复制代码
React:加 key!
菜鸡:我不,我就用 index
React:卡死你活该
菜鸡:React 好垃圾
React:???

React:别乱变组件类型!
菜鸡:我就要 div 和 button 换来换去
React:性能炸了
菜鸡:React 垃圾
React:我他妈...

Vue:我是你妈,屎我也能吃

复制代码
菜鸡:我写屎山代码
Vue:没事,妈给你优化
菜鸡:我不加 key
Vue:妈尽量帮你复用它
菜鸡:我乱改标签
Vue:妈看看能不能复用点东西
菜鸡:Vue 真贴心
Vue:(擦着汗)下次别这样了...

五、更新策略差异

React:两阶段提交(渲染 + 提交)

复制代码
// 阶段1:Reconciliation(协调)
// 偷偷摸摸 diff,生成副作用链表
// 可中断,用户优先

// 阶段2:Commit(提交)  
// 一次性更新 DOM
// 不可中断,必须完成
// 优点:DOM 更新集中,减少重排

Vue:边 diff 边更新

复制代码
// Watcher 触发更新
// 立即执行 patch
// 一边 diff 一边改 DOM
// 同步执行,不可中断
// 优点:响应快,简单直接

六、谁更好?

看场景

  • 你要极致性能 + 团队牛逼 → React(规矩多但上限高)
  • 你要开发爽 + 团队有菜鸡 → Vue(擦屁股能力强)

看脾气

  • 喜欢严格约束,不听话就死 → React
  • 喜欢灵活宽容,屎山也能跑 → Vue

七、一句话总结

React Diff:军校教官,规矩多,不守规矩就枪毙,但守规矩就能打胜仗。

Vue Diff:老油条保姆,能帮你收拾烂摊子,但你太烂它也救不了。

最后暴论

  • React 团队:老子搞最优算法,你们别拖后腿!
  • Vue 团队:你们随便写,屎我尽量吃,别噎死就行。
  • 开发者:我不管,我就要写得爽,性能是框架的事!
  • React vs Vue Diff:一个用枪一个用刀,但都能弄死你

React JSX 循环为何使用 key

JSX 循环加 key:你当 React 是你肚子里的蛔虫?

一、不加 key 会发生什么?

想象一下:你妈让你从洗衣机里拿衣服晾,但所有袜子都长得一样。

复制代码
// 你写了这坨屎:
{items.map((item, index) => (
  <li>{item.name}</li>  // 没加 key,React 要疯了
))}

// React 看到的数据变化:
// 之前:[<li>袜子1</li>, <li>袜子2</li>, <li>袜子3</li>]
// 之后:[<li>新袜子</li>, <li>袜子1</li>, <li>袜子2</li>, <li>袜子3</li>]

// React 的内心戏:
"第一个从袜子1变成新袜子?**重渲染**!"
"第二个从袜子2变成袜子1?**重渲染**!"
"第三个从袜子3变成袜子2?**重渲染**!"
"第四个从空气变成袜子3?**新增**!"

// 实际你只想:加个新袜子,其他袜子位置下移
// React 以为:三条袜子全他妈变了,还得加条新的
// 结果:CPU 烧了,性能炸了,用户卡了

二、加了 key 后 React 的智商

复制代码
// 正常人写法:
{items.map(item => (
  <li key={item.id}>{item.name}</li>  // 给了身份证
))}

// React 现在能看懂了:
"哦,key="1" 从位置0移到位置1,**移动一下**"
"key="2" 从位置1移到位置2,**移动一下**"
"key="3" 从位置2移到位置3,**移动一下**"
"key="4" 新的,**新增一个**"

// 结果:3次移动 + 1次新增
// 性能:原地起飞
// 用户:哎呦挺流畅

三、为什么必须用 key?

原因1:React 不是神仙,分不清谁是谁

复制代码
// 你眼中:
[张三, 李四, 王五] → [赵六, 张三, 李四, 王五]

// 没 key 时 React 眼中:
[人类, 人类, 人类] → [人类, 人类, 人类, 人类]
"我操,都长一样,哪个是哪个?"

原因2:状态和 DOM 会乱套

复制代码
// 恐怖故事:用 index 当 key
{items.map((item, index) => (
  <TodoItem 
    key={index}  // 卧槽你是傻逼吗?
    item={item}
  />
))}

// 删除第一个item后:
// 之前:key=0 → 张三,key=1 → 李四
// 之后:key=0 → 李四(原来 key=1 的!)
// 结果:李四继承了张三的 DOM 状态
// 输入框内容、勾选状态全乱了
// 用户:我勾选的怎么变了???

四、key 的注意事项

1. 绝对不要用 index(重要的事说三遍)

复制代码
// 傻逼写法:
key={index}  // 列表一变,key 全乱套

// 普通写法:
key={item.id}  // 数据库给的 ID,稳

// 实在没 ID:
key={`${item.name}-${item.createdAt}`}  // 组合唯一值

2. key 要稳定,别瞎鸡巴变

复制代码
// 傻逼操作:
key={Math.random()}  // 每次渲染 key 都变
// React:我他妈...每个节点都当新的
// 性能:直接归零
// 内存:爆炸

// 正确做法:
key={item.id}  // 这辈子不变

3. 兄弟节点中唯一就行,不用全局唯一

复制代码
// 这个可以:
<ul>
  <li key="a">A</li>
  <li key="b">B</li>
</ul>
<div>
  <span key="a">又用 a 了</span>  // 可以,不是兄弟
</div>

五、不加 key 的控制台警告

复制代码
Warning: Each child in a list should have a unique "key" prop.

翻译:列表里的每个儿子都要有唯一的 key,不然老子不知道谁是谁!
补充:你再不加 key,React 就当你是个傻逼,然后默默用 index 当 key。
结果:列表一变,状态全乱,性能全无,用户开骂。

六、Vue 的对比

Vue:不加 key 我也尽量优化,但加了 key 我更聪明

React:不加 key?我当你是个智障,性能炸了别怪我

七、现实比喻

没 key

幼儿园老师让 10 个穿同样校服的小朋友排队,然后中间插进来一个新小朋友。

老师:我操,你们谁是谁?点名册对不上,全给我重排!

有 key

每个小朋友胸口贴了名牌(key)。

新小朋友来了,老师一看名牌就知道该插哪。

其他小朋友:不用动,就他一个人调整位置。

八、一句话总结

key 就是 React 的眼镜

不加 key → React 是 1000 度近视,看所有组件都一个样

加了 key → React 戴上眼镜,能分清谁是谁,该移动的移动,该复用的复用

最后暴论

写 React 不加 key 就像开车不系安全带,平时没事,一出事就怪车不行。React 团队费老大劲搞虚拟 DOM 和 diff 算法,你一个 key 都不加,相当于把法拉利当拖拉机开,然后骂法拉利垃圾。要点脸行吗?

React 事件和 DOM 事件区别

一、DOM 事件:原始人裸奔

DOM 事件就是原始社会的群P

复制代码
// 原始 DOM 事件写法
button.addEventListener('click', function() {
  console.log('点击了');
});

// 问题1:事件绑满身
button.addEventListener('click', handler1);
button.addEventListener('click', handler2);
button.addEventListener('click', handler3);
// 按钮:我他妈浑身都是监听器

// 问题2:移除麻烦
button.removeEventListener('click', handler2); // 得记住函数引用
// 忘移除?内存泄露安排

// 问题3:事件传播像粪坑炸了
<div onclick="父级触发">
  <button onclick="子级触发">
    点我
  </button>
</div>
// 点击按钮:子级 → 父级 → 爷爷 → window
// 一路冒泡,谁都能插一脚

二、React 事件:戴套的文明人

React 事件是统一管理的性冷淡

1. 事件池:不浪费一滴精(内存)

复制代码
// React 把所有事件绑在 document/root 上
// 你写的:
<button onClick={handleClick}>点我</button>

// React 实际干的:
document.addEventListener('click', react代理函数);
// 点击时:找到对应组件,调用你的 handleClick

// 优点:1000个按钮 = 1个监听器
// DOM 原版:1000个按钮 = 1000个监听器
// 内存:React 赢麻了

2. 合成事件:统一处理,防止发疯

复制代码
// 你的 handler 收到的是 SyntheticEvent
function handleClick(e) {
  // e 是 React 包装过的
  e.nativeEvent; // 这才是原始事件
  e.stopPropagation(); // React 特供版
  e.preventDefault(); // 也是特供版
}

// React 帮你做了:
// 1. 事件对象复用(用完就回收)
// 2. 兼容性处理(IE?狗都不用)
// 3. 统一 API(不管 Chrome 还是 Safari)

三、核心区别对比

方面 DOM 事件 React 事件
绑定位置 每个元素单独绑 顶层一个代理
内存占用 监听器满天飞,内存爆炸 一个代理管全部,省内存
事件对象 每次新建,用完 GC 回收 事件池复用,减少垃圾回收
兼容性 自己处理 IE 的狗屎 React 擦好屁股了
阻止冒泡 e.stopPropagation() e.stopPropagation()(但其实是 React 版的)
异步访问 同步可用 异步可能被回收,得 e.persist()

四、React 事件的骚操作

1. 自动清理:拔屌无情

复制代码
function Component() {
  useEffect(() => {
    // 不用手动清理
    return () => {
      // React 自动帮你解绑事件
      // 妈妈再也不用担心内存泄露
    };
  }, []);
}

2. 事件委托:一个爹管所有儿子

复制代码
// React 16:绑在 document
// React 17+:绑在 root
// 原理:
点击按钮 → document 捕获事件 → React 找到对应组件 → 调用 handler
// 像快递站:所有快递都送这,再分发给各家

3. 合成事件的坑

复制代码
function handleClick(e) {
  // 同步用,没问题
  console.log(e.type);
  
  // 异步用?出事了!
  setTimeout(() => {
    console.log(e.type); // null!事件对象被回收了!
  }, 0);
  
  // 解决方法:持久化
  e.persist(); // 说:这对象我还要用,别回收
  setTimeout(() => {
    console.log(e.type); // 现在有了
  }, 0);
}

五、冒泡捕获的区别

DOM:自然传播

复制代码
// 捕获:爷爷 → 爸爸 → 儿子
// 冒泡:儿子 → 爸爸 → 爷爷
// 想听哪个阶段?自己选
div.addEventListener('click', handler, true); // 捕获
div.addEventListener('click', handler, false); // 冒泡

React:合成事件体系

复制代码
// 写法一样,但其实是 React 模拟的
<div onClick={handleBubble}>冒泡</div>
<div onClickCapture={handleCapture}>捕获</div>

// React 16:先触发子组件捕获 → 子组件冒泡 → 父组件捕获 → ...
// 其实是 React 自己调度,不是真的事件流

六、性能对比

DOM 事件

复制代码
1000个按钮点击监听:
内存:1000个函数引用,1000个监听器
GC压力:事件对象用完就扔
性能:页面卡成狗

React 事件

复制代码
1000个按钮点击监听:
内存:1个代理函数 + 1000个回调引用
GC压力:事件对象复用,几乎不产生垃圾
性能:丝滑如德芙

七、开发体验

DOM 事件开发:

复制代码
// 手动绑定
button.addEventListener('click', handler);
// 记得移除
button.removeEventListener('click', handler);
// 兼容 IE
if (element.attachEvent) {
  element.attachEvent('onclick', handler);
} else {
  element.addEventListener('click', handler);
}
// 开发者:我他妈是框架还是你是框架?

React 事件开发:

复制代码
// 声明式,像写配置
<button onClick={handleClick}>
  点我
</button>
// 移除?不用管,React 处理
// 兼容性?不用管,React 处理
// 开发者:爽!

八、常见坑爹场景

1. 混用 React 和 DOM 事件

复制代码
// 在 React 组件里
useEffect(() => {
  document.addEventListener('click', () => {
    console.log('DOM 事件');
  });
}, []);

const handleClick = () => {
  console.log('React 事件');
};

// 点击按钮:
// 1. React 事件触发
// 2. 冒泡到 document
// 3. DOM 事件触发
// 结果:执行两次,顺序看 React 版本
// 解决:e.nativeEvent.stopImmediatePropagation()

2. 异步访问事件对象

复制代码
// 菜鸡写法:
const handleClick = (e) => {
  setTimeout(() => {
    console.log(e.target); // null!
  }, 1000);
};

// 老司机写法:
const handleClick = (e) => {
  const target = e.target; // 先保存
  e.persist(); // 或者持久化
  setTimeout(() => {
    console.log(target);
  }, 1000);
};

九、一句话总结

DOM 事件:裸奔,爽但容易得病(内存泄露、兼容性问题)

React 事件:戴套,麻烦点但安全卫生(内存优化、兼容性好)

React 心里话:你们这群菜鸡,连事件兼容性都搞不定,还整天抱怨性能。老子全给你包了,你们就负责写业务,别他妈瞎搞原生事件了行不行?

简述 React batchUpdate 机制

  • React batchUpdate:一坨屎攒着一起拉

一、没 batchUpdate 的憨批场景

想象一下:你妈让你去小卖部买东西:

复制代码
// 没 batchUpdate 时(React 16 前)
setState({ 可乐: 1 });  // 跑一趟小卖部
setState({ 薯片: 1 });  // 又跑一趟小卖部  
setState({ 冰棍: 1 });  // 再跑一趟小卖部
// 结果:跑三趟,累成狗,效率低成屎

React 看到

  1. 可乐状态更新 → 重新渲染一次
  2. 薯片状态更新 → 再渲染一次
  3. 冰棍状态更新 → 又渲染一次
  4. 用户:这界面怎么他妈闪了三次?

二、batchUpdate:聪明人的做法

batchUpdate 就是憋着

复制代码
// 有 batchUpdate 时
// React:等等,看看还有没有别的更新
setState({ 可乐: 1 });  // 先记下来
setState({ 薯片: 1 });  // 再记下来
setState({ 冰棍: 1 });  // 还记下来
// React:好了,攒够了,**一次全买回来**
// 结果:跑一趟,买三样,效率起飞

三、谁在管这个"憋屎"?

1. React 能自动憋的

复制代码
// 场景1:React 事件处理函数
const handleClick = () => {
  setCount(1);     // 第一次更新
  setName('张三');  // 第二次更新
  setAge(18);      // 第三次更新
  // React:都在我 event handler 里,**一起处理**!
  // 最终:只渲染一次
};

// 场景2:生命周期函数
componentDidMount() {
  fetchData().then(data => {
    setData(data.items);    // 批处理
    setLoading(false);      // 批处理
  });
}

2. React 憋不住的

复制代码
// 场景1:setTimeout / Promise(异步操作)
setTimeout(() => {
  setCount(1);     // 第一次更新,立即渲染
  setName('张三');  // 第二次更新,立即渲染
  // React:这他妈是异步,我管不了!
  // 结果:渲染两次
}, 1000);

// 场景2:原生事件
button.addEventListener('click', () => {
  setCount(1);  // 立即渲染
  setName('李四'); // 立即渲染
  // React:这不是我的地盘,管不了!
});

四、React 18 的终极憋尿术

React 17 以前:只能在 React 事件里憋,异步就憋不住

React 18 以后自动批处理所有更新,随时随地都能憋

复制代码
// React 18:老子现在啥都能憋
setTimeout(() => {
  setCount(1);
  setName('张三');
  setAge(18);
  // React 18:异步?照憋不误!
  // 结果:只更新一次
}, 1000);

// 用 flushSync 强行拉屎:
import { flushSync } from 'react-dom';

flushSync(() => {
  setCount(1);  // 立即更新
});
setName('张三');  // 下一次更新
// 结果:渲染两次(一次 flushSync,一次正常)

五、batchUpdate 的好处

1. 性能提升

复制代码
// 没批处理:更新 N 次,渲染 N 次
// 有批处理:更新 N 次,渲染 1 次
// 性能差距:N 倍

2. 避免中间状态闪现

复制代码
// 没批处理:
setCount(1);  // 渲染:count=1, name=''
setName('张三'); // 渲染:count=1, name='张三'
// 用户可能看到:count=1 但 name 还是空的状态

// 有批处理:
setCount(1);
setName('张三');
// 一次渲染:count=1, name='张三' 同时出现

六、特殊情况处理

1. 我想立即更新怎么办?

复制代码
// 用 flushSync 插队
import { flushSync } from 'react-dom';

const handleClick = () => {
  // 正常批处理
  setCount(1);
  setName('张三');
  
  // 这个立即更新!
  flushSync(() => {
    setSpecialFlag(true);
  });
  
  // 继续批处理
  setAge(18);
  // 结果:渲染两次(flushSync 一次,其他的批处理一次)
};

2. 连续 setState 依赖前值

复制代码
// 错误做法:
setCount(count + 1);
setCount(count + 1);  // 拿到的还是旧的 count
// 结果:只加了 1

// 正确做法:
setCount(prev => prev + 1);
setCount(prev => prev + 1);  // 拿到更新后的值
// 结果:加了 2

七、底层原理(简单版)

复制代码
// React 内部有个"是否在批处理"的标记
let isBatchingUpdates = false;

// 开始批处理
function batchedUpdates(fn) {
  isBatchingUpdates = true;
  try {
    fn();
  } finally {
    isBatchingUpdates = false;
    // 处理所有攒着的更新
    flushUpdates();
  }
}

// setState 时
function setState(newState) {
  if (isBatchingUpdates) {
    // 在批处理中:先存着
    enqueueUpdate(this, newState);
  } else {
    // 不在批处理:立即更新
    updateComponent(this, newState);
  }
}

八、一句话总结

batchUpdate 就是 React 的憋尿术

  • 能憋就憋,攒够了一起尿
  • React 17 前:只在自家厕所(事件处理)能憋
  • React 18 后:在哪都能憋,除非你非要当场拉(flushSync)

好处:减少不必要的渲染,性能提升,避免中间状态

代价:有时候你想立即更新得用特殊手段

最后暴论:没有 batchUpdate 的 React 就像膀胱不好的老头,有点尿意就要跑厕所,一天跑 800 趟,累不死也烦死了。有了 batchUpdate,就是正常人的膀胱,攒一攒一次解决,高效又体面。

简述 React 事务机制

  • React 事务机制:一套组合拳的仪式感

一、事务是什么鬼?

事务就是 React 的"仪式感"

复制代码
// 想象你要打人:
// 普通人:上去就是一拳
// React 事务:
1. initialize()   // 先喊:我要打你了!
2. 执行你的代码      // 实际打人
3. close()        // 打完收工:下次还敢?

// React 16 前到处是这玩意儿,现在基本被Hooks干废了

二、事务的三段式装逼

复制代码
// 伪代码感受一下:
transaction.perform(() => {
  // 你的 setState
});

// 实际执行顺序:
1. transaction.initialize()  // 所有 wrapper 的初始化
2. 你的代码被执行
3. transaction.close()       // 所有 wrapper 的收尾

三、现实例子:更新组件

没有事务(原始人)

复制代码
component.setState(newState);  // 直接改
// 问题:改了一半崩了怎么办?前后要做事怎么办?

有事务(文明人)

复制代码
// React 内部
RESET_BATCHED_UPDATES.initialize();  // 标记:开始批量更新
SELECTION_RESTORATION.initialize();  // 保存光标位置
// ... 其他一堆 initialize

component.setState(newState);  // 你的代码

SELECTION_RESTORATION.close();  // 恢复光标位置
RESET_BATCHED_UPDATES.close();  // 执行批量更新
// ... 其他一堆 close

四、为什么搞这么复杂?

1. 保证一致性(要么全做,要么全不做)

复制代码
// 改 state 前:保存光标位置
// 改 state 中:你的代码
// 改 state 后:恢复光标位置
// 就算你代码报错,光标也得恢复

2. 批量更新(攒着一坨一起拉)

复制代码
// initialize: 开始憋屎模式
// 你的代码: 连续 setState
// close: 憋够了,一次性拉出来

3. 错误边界(出事了也能收拾)

复制代码
// 以前没有 Error Boundary
// 事务就是简陋的错误处理:
try {
  transaction.initialize();
  yourCode();
} finally {
  transaction.close();  // 无论如何都要执行
}

五、事务的 wrapper 例子

复制代码
// React 内置的一些 wrapper:
const TRANSACTION_WRAPPERS = [
  SELECTION_RESTORATION,      // 光标恢复
  RESET_BATCHED_UPDATES,      // 批量更新
  FLUSH_BATCHED_UPDATES,      // 刷新更新
  // ... 还有其他一堆
];

// 执行时像洋葱:
// SELECTION_RESTORATION.initialize()
// RESET_BATCHED_UPDATES.initialize()
// FLUSH_BATCHED_UPDATES.initialize()
// --- 你的代码 ---
// FLUSH_BATCHED_UPDATES.close()
// RESET_BATCHED_UPDATES.close()
// SELECTION_RESTORATION.close()

六、事务的缺陷(为什么被弃用)

1. 代码晦涩

复制代码
// 看 React 15 源码:
ReactUpdates.batchedUpdates(() => {
  // 你的代码
}, transaction);
// 新手:这他妈是啥?魔法吗?

2. 性能开销

复制代码
// 每个更新都包一层事务
// wrapper 多了就慢
// 像穿 10 层雨衣出门

3. 调试困难

复制代码
// 报错栈:
at Transaction.perform
at wrapper.close
at anotherWrapper.initialize
at YourComponent.setState
// 调试:我他妈到底在哪层?

七、Hooks 时代的替代方案

事务机制被 Hook 和 Fiber 干掉了

复制代码
// React 15:用事务
transaction.perform(() => {
  this.setState({ count: 1 });
});

// React 16+:用 useEffect
useEffect(() => {
  // 类似 close 阶段
  return () => {
    // 类似 initialize 阶段
  };
}, [deps]);

// 或者用 useLayoutEffect
useLayoutEffect(() => {
  // 在 DOM 更新后,浏览器绘制前执行
  // 类似事务的 close 阶段
});

八、一句话总结

React 事务

  • 就是给代码穿三层雨衣(initialize → 执行 → close)
  • 为了保证安全(要么全做,要么全不做)
  • 为了批量处理(攒着一起搞)
  • 太复杂,现在被 Hooks 和 Fiber 干掉了

类比

就像你拉屎要先脱裤子(initialize),拉屎(你的代码),擦屁股冲水(close)。

React 16 前必须按这个流程,现在你可以穿开裆裤直接拉了。

最后暴论

事务机制就是 React 早期的过度设计,像老太太的裹脚布又臭又长。

好在 React 团队知道自己傻逼,用 Fiber 和 Hooks 把它干掉了。

现在只有老项目里还能看到这坨历史遗产,新手不用学,知道有这么个玩意儿存在过就行。

理解 React concurrency 并发机制

  • React 并发:从铁憨憨到时间管理大师

一、并发前:React 是个一根筋的傻逼

React 16 以前

复制代码
// 渲染流程:
开始渲染 → 突突突突 → 不渲染完不停 → 用户卡死 → 渲染完成
// 像他妈拉屎不让人催,非得拉完才能出厕所
// 用户:我点按钮啊!我滚动啊!我操!
// React:憋着!等老子渲染完!

二、并发模式:React 学会看脸色了

核心思想 :渲染可以被打断,用户操作最大

复制代码
// 以前:渲染是爷爷
用户点击 → 等渲染完 → 处理点击
// 结果:卡顿,用户骂娘

// 现在:用户是爸爸
正在渲染 → 用户点击 → 立即暂停渲染 → 处理点击 → 继续渲染
// 结果:流畅,用户说牛逼

三、并发怎么实现的?

1. 时间切片(Time Slicing)

复制代码
// 以前渲染 100ms 的组件:
████████████████████████████████████████
// 用户:卡了 100ms,啥也干不了

// 并发模式:
███ 5ms ███ 5ms ███ 5ms ███ 5ms ... // 每次只干 5ms
// 每次干完 5ms 就问:用户有操作吗?有就先让路
// 用户:哎呦,不卡了

2. 优先级调度

复制代码
// React 现在会分三六九等:
const priorities = {
  用户输入: '紧急',      // 键盘、点击,立即响应
  悬停动画: '高',        // 稍微等等
  数据获取: '中',        // 可以等会儿
  低优先级更新: '低',     // 闲得蛋疼再处理
  离屏内容: '最低',      // 滚出屏幕了?去死吧
};

// 高优先级可以插队:
低优先级渲染中 → 用户点击 → 立即中断低优先级 → 处理点击 → 回来继续

四、并发的骚操作

1. Suspense:优雅的 loading

复制代码
// 以前:
{loading ? <Spinner /> : <Content />}
// 问题:一闪而过的 Spinner,布局抖动

// 并发 + Suspense:
<Suspense fallback={<Skeleton />}>
  <SlowComponent />  // 慢慢加载
  <FastComponent />  // 先显示这个
</Suspense>
// 结果:先显示骨架屏,内容慢慢来,不阻塞

2. useTransition:告诉 React 我不急

复制代码
const [isPending, startTransition] = useTransition();

// 用户点标签页
const handleClick = () => {
  // 这个不急,可以慢慢来
  startTransition(() => {
    setTab(newTab);  // 渲染很重的组件
  });
};

return (
  <div>
    <button onClick={handleClick}>
      切换标签
    </button>
    {isPending && <Spinner />}  // 告诉用户:在切了,别急
    <HeavyTabContent />
  </div>
);
// 用户点击时,UI 立即响应,内容慢慢加载

3. useDeferredValue:延迟更新

复制代码
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);  // 这个值更新会慢半拍

return (
  <>
    <input value={text} onChange={e => setText(e.target.value)} />
    <SlowList query={deferredText} />  // 用延迟的值,不卡输入
  </>
);
// 输入框:立即响应
// 列表:慢慢更新,不卡输入

五、并发的代价

1. 代码复杂度上天

复制代码
// 以前:setState → 渲染 → 完成
// 现在:要考虑:
// - 这个更新优先级高不高?
// - 要不要用 startTransition?
// - Suspense 边界放哪?
// - 会不会有 tearing 问题?
// 开发者:我他妈写个 UI 还要懂操作系统调度?

2. 可能看到中间状态

复制代码
// 问题:高优先级更新插队
当前状态:A
低优先级更新:A → B(渲染中)
用户点击:高优先级更新 A → C
// 结果:用户先看到 C,然后可能又看到 B
// 这叫 tearing(撕裂),React 18 大部分解决了,但还有边缘情况

3. 打包体积变大

复制代码
// 为了并发,React 代码更复杂
// bundle 大了 10-20%
// 用户:我 4G 手机招谁惹谁了?

六、现实比喻

没并发时:

像傻逼收银员

  • 前面顾客买了 100 件商品
  • 后面顾客只买一瓶水
  • 收银员:等我把这 100 件扫完!
  • 后面顾客:我操你妈

有并发时:

像聪明收银员

  • 前面顾客买了 100 件商品
  • 收银员扫了 5 件,抬头看
  • 后面顾客:我就买瓶水
  • 收银员:马上给您结!
  • 结完水,继续扫剩下的 95 件
  • 大家都很开心

七、什么时候用并发?

复制代码
// 一定要用:
- 大型列表/表格渲染
- 图表/可视化
- 复杂动画
- 输入框实时搜索

// 可以考虑用:
- 标签页切换
- 路由切换
- 弹窗打开

// 别瞎鸡巴用:
- 静态页面
- 简单表单
- 为啥?杀鸡用牛刀,还容易切到手

八、并发的坑

1. 外部状态库可能不兼容

复制代码
// Redux、MobX 等:我他妈不知道你在并发渲染啊!
// 可能看到中间状态
// 解决方案:用 useSyncExternalStore

2. useEffect 可能执行多次

复制代码
// 低优先级更新可能被中断重试
// useEffect 可能执行多次
// 解决方案:用 useLayoutEffect 或注意幂等性

3. 测试变复杂

复制代码
// 以前测试:触发 → 等更新 → 断言
// 现在测试:要考虑并发渲染、优先级、中断
// 测试代码复杂度:* 10086

九、一句话总结

React 并发

  • 以前 React 是铁憨憨,渲染不完成不让路
  • 现在 React 是时间管理大师,会分片干活,会让路给用户
  • 代价是代码复杂 10 倍 ,但用户体验爽 100 倍

最后暴论

React 团队用 5 年时间,把 UI 渲染从"单线程傻跑"升级到"多任务操作系统"。

现在写 React 不像写前端,像写操作系统调度器。

好处是应用流畅得跟原生一样,坏处是门槛高得能当架构师。

菜鸡看了直摇头,高手看了直呼内行。

React reconciliation 协调的过程

  • React Reconciliation:React 的"找不同"玄学

一、协调是啥?虚拟 DOM 的"找不同"游戏

协调就是 React 的"大家来找茬"

复制代码
// 老虚拟 DOM:<div className="old">Hello</div>
// 新虚拟 DOM:<div className="new">World</div>

// React 协调过程:
1. 看看标签一样不?都是 div → 行,不用删
2. 看看属性一样不?className 从 old 变 new → 改!
3. 看看儿子一样不?Hello 变 World → 改!
4. 完了,真 DOM 就改两处,其他不动

二、协调的核心思想:能复用就复用,不能就干碎

1. 类型不同?直接枪毙

复制代码
// 老:<div>内容</div>
// 新:<span>内容</span>
// React:标签都不一样,重建!
// 结果:整个 div 子树全删,span 新建
// 性能:炸了

2. 类型相同?看看能不能接着用

复制代码
// 老:<div className="red">Hello</div>
// 新:<div className="blue">World</div>
// React:都是 div,能复用!
// 操作:改 className,改文本
// 性能:省了

三、协调的三板斧

第一斧:逐层比较,不跨级装逼

复制代码
// 老树:
<div>
  <Header />
  <Content>
    <List />
  </Content>
</div>

// 新树:
<div>
  <Header />
  <Content>
    <Table />  // List 变 Table
  </Content>
</div>

// React 比较:
1. 比较 div → 一样
2. 比较 Header → 一样
3. 比较 Content → 一样
4. 比较 Content 的儿子:List vs Table → 不一样!
5. 结果:只重建 List/Table 那部分,Header 复用
// 不会拿 List 和 Table 的孙子比较,跨级比较太傻逼

第二斧:列表比较,key 是亲爹

复制代码
// 没 key 的傻逼情况:
// 老数组:[A, B, C]
// 新数组:[D, A, B, C]

// React 没 key 时:
位置0:A → D,重建
位置1:B → A,重建  
位置2:C → B,重建
位置3:null → C,新建
// 结果:重建3个,新建1个 → 血妈亏

// 有 key 时:
key="a" 从位置0→1,移动
key="b" 从位置1→2,移动
key="c" 从位置2→3,移动
key="d" 新增到位置0
// 结果:移动3个,新建1个 → 血赚

第三斧:组件复用,props 和 state 决定

复制代码
// 组件更新,看 shouldComponentUpdate
class Component extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 返回 true:重建
    // 返回 false:复用旧的
    // 默认:浅比较 props 和 state
  }
}

// 函数组件用 React.memo
const MemoComp = React.memo(MyComp, (prevProps, nextProps) => {
  // 返回 true 不更新,false 更新
  // 跟 shouldComponentUpdate 相反,脑残设计
});

四、协调的详细过程(Fiber 版)

1. 构建 WorkInProgress 树

复制代码
// 当前屏幕:Current Fiber 树
// 后台构建:WorkInProgress Fiber 树
// 像他妈双显卡交火,一个显示,一个渲染

2. 深度优先遍历,但可中断

复制代码
// 以前(递归,不可中断):
function render(node) {
  render(node.child);  // 递归到底
  render(node.sibling);
}
// 问题:树太深就爆栈,不能停

// Fiber(循环,可中断):
while (nextFiber && 还有时间) {
  performUnitOfWork(nextFiber);  // 处理当前节点
  nextFiber = getNextFiber();    // 获取下一个
}
// 没时间了?保存进度,下次继续

3. 收集副作用(副作用清单)

复制代码
// Fiber 遍历时收集要改的 DOM:
const effectList = [];

function performUnitOfWork(fiber) {
  // 比较新旧,看要不要改 DOM
  if (需要更新) {
    fiber.flags = Update;  // 标记要更新
    effectList.push(fiber); // 加入副作用列表
  }
  
  // 处理子节点
  // 处理兄弟节点
}

4. 提交更新(Commit 阶段)

复制代码
// 所有比较完成,一次性改 DOM
function commitRoot() {
  // 不可中断,必须一口气完成
  commitBeforeMutationEffects();  // 更新前
  commitMutationEffects();        // 更新 DOM
  commitLayoutEffects();          // 更新后
  // 完事,切换 Current 和 WorkInProgress
}

五、协调的性能优化

1. PureComponent / memo

复制代码
// 类组件:
class MyComp extends React.PureComponent {
  // 自动浅比较 props
  // 不一样才更新
}

// 函数组件:
const MyComp = React.memo(function MyComp(props) {
  // 默认浅比较 props
}, arePropsEqual);  // 自定义比较函数

2. key 要用好

复制代码
// 傻逼:key={index}
// 列表一变,key 全乱,性能炸

// 正常:key={item.id}
// 稳如老狗

// 没 key:列表全量对比
// 有 key:React 用 key 建 Map,O(1) 查找

3. 避免突变

复制代码
// 傻逼:直接改
this.state.items.push(newItem);
this.forceUpdate();
// React:我他妈不知道你改了啥,全量对比吧

// 聪明:新建引用
this.setState({
  items: [...this.state.items, newItem]
});
// React:哦,数组引用变了,对比一下

六、协调的坑

1. index 当 key

复制代码
// 删除第一个元素:
// 老:key=0:A, key=1:B, key=2:C
// 新:key=0:B, key=1:C
// React 以为:A→B, B→C, C删了
// 实际:A删了,B、C前移
// 结果:B 组件复用 A 的实例,状态全乱了

2. 随机 key

复制代码
// 每次渲染 key 都变:
key={Math.random()}
// React:我操,每个 key 都是新的
// 结果:全部重建,性能归零
// 内存:爆炸

3. 跨层级移动

复制代码
// 老树:
<div>
  <A />
  <B />
</div>

// 新树:
<div>
  <B />  // 从第二个儿子变第一个
  <A />  // 从第一个儿子变第二个
</div>

// React 比较:
第一个儿子:A → B,类型不同,重建
第二个儿子:B → A,类型不同,重建
// 结果:两个都重建
// 即使有 key 也救不了,因为不同父节点

七、协调 vs 真实 DOM 操作

没有协调(jQuery 时代):

复制代码
$('.list').empty();
data.forEach(item => {
  $('.list').append(`<li>${item}</li>`);
});
// 每次全量删除、全量添加
// DOM 操作爆炸,性能吃屎

有协调(React):

复制代码
// 1. 虚拟 DOM 比较(内存中,快)
// 2. 生成最小 DOM 操作
// 3. 执行 DOM 操作
// 结果:DOM 操作最少,性能起飞

八、一句话总结

React Reconciliation

  • 就是个虚拟 DOM 的找不同算法
  • 能复用就复用,能不动 DOM 就不动
  • key 是亲爹,没 key 就瞎比对比
  • 跨层级移动是硬伤,会全量重建
  • 最终目标:用 JS 的计算时间换 DOM 的操作时间

最后暴论

协调算法就是 React 的智商:

  • 有 key 时智商 180,最小化 DOM 操作
  • 没 key 时智商 80,暴力对比
  • index 当 key 时智商 0,直接摆烂

React 团队费老大劲优化协调算法,结果你一个 key={index}全给干废了。

就像给你配了台顶配电脑,你非要用它玩扫雷,还抱怨电脑卡。

React 组件渲染和更新的全过程

  • React 组件渲染和更新:从生到死的一坨屎流程

一、组件出生(挂载)

1. 你写代码,Babel 转译

复制代码
// 你写的:
<Button color="red">点我</Button>

// Babel 转成:
React.createElement(Button, { color: "red" }, "点我")
// 你:我就写个标签,你转成函数调用?
// Babel:不然呢?浏览器认识 JSX?

2. 创建 Fiber 节点

复制代码
// React 创建 Fiber(打工仔档案):
const fiber = {
  tag: FunctionComponent,  // 组件类型
  stateNode: null,         // 组件实例(还没生)
  memoizedProps: { color: 'red' },  // 这次用的 props
  memoizedState: null,     // 还没 state
  return: parentFiber,     // 爹是谁
  child: null,             // 儿子(还没生)
  sibling: null,           // 兄弟(就你一个)
  flags: Placement,        // 要干啥:插到 DOM
  // ... 一堆其他字段
};
// React:先建档案,后面再招人

3. 递归构建 Fiber 树

复制代码
// 从根开始,深度优先:
function workLoop() {
  while (有活 && 有时间) {
    performUnitOfWork(当前Fiber);
  }
}

// 处理一个节点:
function performUnitOfWork(fiber) {
  // 1. 开始工作(beginWork)
  if (fiber.tag === FunctionComponent) {
    // 调用你的函数组件
    const children = YourComponent(fiber.pendingProps);
    // 创建子 Fiber
    reconcileChildren(fiber, children);
  }
  
  // 2. 有儿子?先处理儿子
  if (fiber.child) {
    return fiber.child;
  }
  
  // 3. 没儿子了?处理兄弟
  let nextFiber = fiber;
  while (nextFiber) {
    // 完成工作(completeWork)
    completeUnitOfWork(nextFiber);
    
    if (nextFiber.sibling) {
      return nextFiber.sibling;  // 处理兄弟
    }
    // 兄弟也没了?回爹那
    nextFiber = nextFiber.return;
  }
}

4. 生成 DOM 节点

复制代码
// 处理到原生组件(div、span):
function completeUnitOfWork(fiber) {
  if (fiber.tag === HostComponent) {  // 原生 DOM
    // 创建真实 DOM
    const dom = document.createElement(fiber.type);
    
    // 设置属性
    updateDOMProperties(dom, fiber.memoizedProps);
    
    // 关联 Fiber 和 DOM
    fiber.stateNode = dom;
    
    // 收集到父节点下
    if (fiber.return && fiber.return.stateNode) {
      fiber.return.stateNode.appendChild(dom);
    }
  }
}

5. 提交到 DOM(Commit 阶段)

复制代码
// Fiber 树构建完,一口气更新 DOM:
function commitRoot() {
  // 1. 更新前:getSnapshotBeforeUpdate
  commitBeforeMutationEffects();
  
  // 2. 更新 DOM:最重的活
  commitMutationEffects();
  // 这里才真的 appendChild、setAttribute
  // 之前全是在内存里 YY
  
  // 3. 更新后:componentDidMount、useLayoutEffect
  commitLayoutEffects();
  
  // 4. 切换树
  root.current = finishedWork;  // WorkInProgress 变 Current
}

二、组件更新(setState)

1. 你触发更新

复制代码
// 三种方式触发:
1. setState({ count: 1 });      // 类组件
2. dispatch(action);            // useState
3. forceUpdate();               // 强制更新(傻逼才用)

// React:又来活了,记下来
enqueueUpdate(fiber, update);
// 放到更新队列,排队

2. 调度更新

复制代码
// React 看看优先级:
if (是用户输入) {
  scheduleCallback(ImmediatePriority, performSyncWork);
} else {
  scheduleCallback(NormalPriority, performConcurrentWork);
}

// 高优先级?立即执行
// 低优先级?有时间再干

3. 协调(Reconciliation)

复制代码
// 比较新旧,生成副作用
function beginWork(current, workInProgress) {
  // 比较 props
  const nextProps = workInProgress.pendingProps;
  const prevProps = current !== null ? current.memoizedProps : null;
  
  // 调用组件,得到新 children
  const nextChildren = YourComponent(nextProps);
  
  // diff 新旧 children
  reconcileChildren(current, workInProgress, nextChildren);
  
  // 标记要干啥
  if (需要更新DOM) {
    workInProgress.flags |= Update;
  }
  if (要删除) {
    workInProgress.flags |= Deletion;
  }
}

4. diff 算法干活

复制代码
// 对子节点列表:
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  // 第一轮:从左到右找一样的
  // 第二轮:从右到左找一样的  
  // 第三轮:有 key 的用 Map 找
  // 第四轮:剩下的新建或删除
  
  // 结果:
  // 1. 能复用:移动
  // 2. 不能复用:重建
  // 3. 多了:新建
  // 4. 少了:删除
}

5. 收集副作用

复制代码
// 遍历时收集要改的节点
const deletions = [];  // 要删的
const updates = [];    // 要更新的
const placements = []; // 要新增的

// 标记在 Fiber.flags 上
fiber.flags = Update | Placement | Deletion;

6. 提交更新

复制代码
function commitMutationEffects() {
  // 1. 先删
  deletions.forEach(commitDeletion);
  
  // 2. 更新/新增
  updates.forEach(commitWork);
  placements.forEach(commitPlacement);
}

三、组件死亡(卸载)

1. 从树中移除

复制代码
// 父组件不渲染你了:
// 之前:<div><Child /></div>
// 之后:<div></div>

// React 标记为删除
childFiber.flags |= Deletion;
parentFiber.deletions.push(childFiber);

2. 清理阶段

复制代码
function commitDeletion(fiber) {
  // 1. 执行 componentWillUnmount
  // 2. 清理 ref
  // 3. 清理事件监听
  // 4. 清理 Effect(useEffect 返回的函数)
  // 5. 从 DOM 移除
  fiber.stateNode.remove();
  
  // 6. 断开 Fiber 引用,等 GC 回收
  fiber.stateNode = null;
  fiber.child = null;
  fiber.sibling = null;
  fiber.return = null;
}

四、生命周期/Effect 执行时机

类组件:

复制代码
// 挂载:
constructor → render → componentDidMount

// 更新:
shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate

// 卸载:
componentWillUnmount

函数组件:

复制代码
// 每次渲染:
1. 执行函数体
2. 清理上次的 Effect(cleanup)
3. 执行本次的 Effect
4. 执行 useLayoutEffect(同步)

// 依赖数组变化才重跑 Effect
useEffect(() => {
  // 渲染后才执行
  return () => { /* 清理 */ };
}, [依赖]);

五、性能优化点

1. 避免不必要的渲染

复制代码
// 傻逼:
<Child onClick={() => {}} />  // 每次都是新函数
// 结果:Child 每次都重新渲染

// 聪明:
const onClick = useCallback(() => {}, []);
<Child onClick={onClick} />  // 函数不变
// 结果:Child 用 memo 可以避免重渲染

2. 用好 key

复制代码
// 列表没 key:
items.map((item, index) => <Item key={index} />)
// React:位置变就重建,性能炸

// 有 key:
items.map(item => <Item key={item.id} />)
// React:用 id 追踪,能移动就移动

3. 合理拆分组件

复制代码
// 屎山组件:
<App>
  <Header />
  <Content />  // 里面 state 一变,整个重渲染
  <Footer />
</App>

// 聪明拆分:
<App>
  <Header />
  <Content />
  <Footer />
</App>
// Content 自己管自己的 state,不影响别人

六、一句话总结完整流程

挂载

复制代码
你写 JSX → Babel 转成 createElement → React 建 Fiber 树 → 递归处理 → 生成 DOM → 一次性插入

更新

复制代码
setState → 标记更新 → 调度(分优先级)→ 协调(diff 新旧)→ 收集副作用 → 提交到 DOM

卸载

复制代码
父组件不渲染你 → 标记删除 → 提交时清理 → 从 DOM 移除 → GC 回收

七、React 心里话

React 的工作流程

  1. 协调阶段:在内存里 YY 要怎么改(可中断,不卡用户)
  2. 提交阶段:真的动手改 DOM(不可中断,必须一口气干完)

像什么

  • 协调阶段:画施工图(可以慢慢画,画错重画)
  • 提交阶段:按图施工(必须一气呵成,不能干一半跑了)

最后暴论

React 渲染就像拉屎:

  • 协调阶段:脱裤子、酝酿、找感觉(可中断,可以先去接电话)
  • 提交阶段:一泻千里(不可中断,天王老子来了也得拉完)

每次 setState 就是一次完整的脱裤子拉屎流程,区别只是屎多屎少。

React 18 的并发就是学会在马桶上刷手机,拉一会儿歇一会儿,随时响应外界呼唤。

为何 Hooks 不能放在条件或循环之内?

  • Hooks 不能在条件/循环里:React 的老年痴呆症

一、Hooks 的本质:有记忆的傻逼

Hooks 就像个记性不好的老头

复制代码
// 第一次渲染:
useState(0)  // 老头:第一个 hook,记在小本本第一行
useEffect()  // 老头:第二个 hook,记在第二行
useMemo()    // 老头:第三个 hook,记在第三行

// 第二次渲染:
useState(0)  // 老头:翻开小本本,第一行是啥?哦是 useState
useEffect()  // 老头:第二行是啥?哦是 useEffect  
useMemo()    // 老头:第三行是啥?哦是 useMemo
// 一切正常,老头记性还行

二、加个条件判断试试

复制代码
// 第一次渲染(show 为 true):
if (show) {
  useState(0)  // 老头:第一行,记 useState
}
useEffect()   // 老头:第二行,记 useEffect

// 第二次渲染(show 为 false):
if (show) {  // false,不执行
  // useState 没了!
}
useEffect()   // 老头:翻开小本本...
              // 第一行应该是 useState,怎么变成 useEffect 了?
              // 我操,我记乱了!
              // React:Uncaught Error: Rendered fewer hooks than expected

三、React 怎么记的?

React 用链表记 Hooks

复制代码
// 组件里的 Hooks 链表:
let hook1 = { memoizedState: 0, next: hook2 };  // useState
let hook2 = { memoizedState: null, next: hook3 }; // useEffect
let hook3 = { memoizedState: null, next: null };  // useMemo

// React 渲染时顺序遍历:
let currentHook = hook1;  // 第一个 hook
// 调用你的 useState
currentHook = currentHook.next;  // 移到第二个

// 第二个 hook
// 调用你的 useEffect
currentHook = currentHook.next;  // 移到第三个

// 第三个 hook
// 调用你的 useMemo

四、条件判断如何破坏链表

情况1:第一次有,第二次没有

复制代码
// 第一次:
if (true) { useState(0) }  // hook1
useEffect()                // hook2
// 链表:hook1 → hook2

// 第二次:
if (false) { /* useState 没了! */ }
useEffect()  // React 以为这是 hook1,其实是 hook2
// 链表预期:hook1 → hook2
// 实际:hook2(孤零零一个)
// React:我操,hook1 呢?

情况2:第一次没有,第二次有

复制代码
// 第一次:
if (false) { /* 没执行 */ }
useEffect()  // hook1
// 链表:hook1

// 第二次:
if (true) { useState(0) }  // React 以为这是 hook1
useEffect()                // React 以为这是 hook2
// 链表预期:hook1 → hook2
// 实际:useState(当 hook1)→ useEffect(当 hook2)
// 但 useState 的值去哪了?全乱了!

五、循环也一样傻逼

复制代码
// 第一次:items = [1, 2, 3]
for (let i = 0; i < items.length; i++) {
  useState(items[i]);  // hook1, hook2, hook3
}
useEffect();  // hook4

// 第二次:items = [1, 2](少了一个)
for (let i = 0; i < items.length; i++) {
  useState(items[i]);  // hook1, hook2
  // hook3 呢?循环次数变了!
}
useEffect();  // React 以为这是 hook3,其实是 hook4
// 链表又乱了!

六、为什么 React 这么傻逼?

1. 设计决定

复制代码
// React 可以设计成:
useState(0, "count");  // 给个名字
// 但 React 团队觉得:
// 1. 太丑
// 2. 要起名字,麻烦
// 3. 我们相信开发者不写屎代码
// 结果:用顺序,简单但脆弱

2. 性能考虑

复制代码
// 用链表 O(1) 访问
// 如果用 Map 要 O(1) 但内存大
// React:为了性能,忍了

七、如何绕过这个限制?

1. 把条件提到外面

复制代码
// 傻逼:
if (isAdmin) {
  const [adminData, setAdminData] = useState(null);
}

// 聪明:
const [adminData, setAdminData] = useState(null);
if (!isAdmin) {
  // 不用 adminData
}

2. 用 useMemo/useCallback 包装条件逻辑

复制代码
const data = useMemo(() => {
  if (condition) {
    return computeExpensiveValue();
  }
  return null;
}, [condition]);  // 依赖数组控制重计算

3. 自定义 Hook 封装

复制代码
function useConditionalHook(condition) {
  const [state, setState] = useState(null);
  
  useEffect(() => {
    if (condition) {
      // 条件逻辑
    }
  }, [condition]);
  
  return state;
}
// 在组件里:useConditionalHook(someCondition)
// 顺序永远不变

八、React 的错误信息

复制代码
Hooks can only be called inside the body of a function component.

1. 你可能犯了以下错误:
- 在条件里调 Hook(你是傻逼)
- 在循环里调 Hook(你是傻逼)  
- 在嵌套函数里调 Hook(你也是傻逼)
- 在类组件里调 Hook(你是大傻逼)

2. 你可能想说:我他妈就想条件用 Hook!
3. React 说:不行,老子记性差,顺序乱了就疯
4. 解决方案:把条件逻辑放 Hook 里面,别放外面

九、类比解释

没条件的 Hooks(正常):

像军训报数

  • 第一个人:1!(useState)
  • 第二个人:2!(useEffect)
  • 第三个人:3!(useMemo)
  • 教官:好,都记住了

有条件的 Hooks(傻逼):

有人请假了还报数

  • 第一个人:1!(useState)
  • 第二个人:请假了
  • 第三个人:2!(React 以为你是第二个,其实是第三个)
  • 教官:我操,2号不是请假了吗?你是谁?

十、为什么其他框架没这问题?

Vue 3 Composition API:

复制代码
// Vue 用响应式系统,不依赖顺序
const count = ref(0);
const double = computed(() => count.value * 2);
// 放哪都行,Vue 追踪的是变量,不是顺序

Solid.js:

复制代码
// Solid 编译时分析,知道你在用啥
const [count, setCount] = createSignal(0);
// 编译成其他东西,不依赖运行时顺序

React:老子就这样,爱用不用

复制代码
// React 团队:我们 2018 年就定了这规则
// 现在几百万项目在用,改不了
// 你们适应一下,又不会死

十一、一句话总结

React Hooks 不能放条件/循环里因为

  • React 用链表顺序记 Hook
  • 每次渲染必须同样的 Hook,同样的顺序
  • 条件/循环会改变顺序 ,React 就记乱
  • 记乱的结果:状态错乱,直接报错

React 心里话

老子是个记性差的傻逼,就靠数数记事情。

你第一次给我 1、2、3,第二次也必须给我 1、2、3。

少一个不行,多一个不行,顺序变更不行。

你要条件判断?在 Hook 里面判断,别在外面搞我!

useEffect 的底层是如何实现的

  • useEffect 底层实现:React 的定时拉屎器

一、useEffect 是什么货色?

useEffect 就是 React 的"事后诸葛亮"

复制代码
// 你写的:
useEffect(() => {
  console.log('搞完了');
}, []);

// React 实际干的:
1. 组件渲染(拉屎中)
2. 渲染完(拉完了)
3. 浏览器画到屏幕(冲水)
4. 然后才执行你的 useEffect(擦屁股)
// 顺序:先拉屎,后擦屁股

二、useEffect 的底层存储

1. Fiber 节点里有个"Effect 链表"

复制代码
// 每个 Fiber 节点:
const fiber = {
  memoizedState: null,  // Hooks 链表
  updateQueue: null,    // 更新队列
  
  // Effect 相关:
  firstEffect: null,    // 第一个副作用
  lastEffect: null,     // 最后一个副作用
  nextEffect: null,     // 下一个副作用
  
  // 你的 useEffect 存在这:
  const hook = {
    memoizedState: {  // Effect 对象
      create: () => console.log('搞完了'),  // 你的函数
      destroy: null,  // 清理函数
      deps: [],       // 依赖数组
      next: null,     // 下一个 Effect
    },
    next: null,       // 下一个 Hook
  };
};

2. 多个 useEffect 串成链表

复制代码
// 你写:
useEffect(() => { console.log(1); });
useEffect(() => { console.log(2); });
useEffect(() => { console.log(3); });

// React 存成:
effect1 → effect2 → effect3
// 像他妈羊肉串,一串挂着

三、useEffect 的执行时机

1. 渲染阶段:只收集,不执行

复制代码
function updateFunctionComponent(fiber) {
  // 1. 执行你的组件函数
  const children = YourComponent();
  
  // 2. 处理 Hooks
  if (有useEffect) {
    // 只是创建 Effect 对象,挂到链表
    // 不执行!记住了,现在不执行!
    const effect = {
      tag: HasEffect,  // 要执行
      create: 你的函数,
      destroy: null,
      deps: 你的依赖,
    };
    fiber.updateQueue = effect;
  }
  
  // 3. 继续协调子节点
  reconcileChildren(fiber, children);
}

2. 提交阶段:分三个子阶段

阶段1:BeforeMutation(提交前)
复制代码
function commitBeforeMutationEffects() {
  // 执行 getSnapshotBeforeUpdate
  // useEffect?不,还没到
  // 这里主要是给类组件用的
}
阶段2:Mutation(改DOM)
复制代码
function commitMutationEffects() {
  // 这里才真的改 DOM:
  // 增删改 DOM 节点
  // 设置属性
  // useEffect?不,还没到!
  // React:DOM 没改完,不执行副作用
}
阶段3:Layout(布局后)
复制代码
function commitLayoutEffects() {
  // 1. 执行 useLayoutEffect
  // 注意:useLayoutEffect 在这执行!
  // 同步执行,阻塞浏览器绘制
  
  // 2. 执行类组件的 componentDidMount/Update
  
  // 3. useEffect?不,还没到!
  // React:useLayoutEffect 先,useEffect 后
}

3. 真正的 useEffect 执行时机

在 commit 阶段之后,下一个事件循环

复制代码
function commitRoot() {
  // 上面三个阶段走完...
  
  // 然后安排 useEffect:
  scheduleCallback(
    NormalPriority,
    () => {
      // 下一帧执行
      flushPassiveEffects();
    }
  );
}

function flushPassiveEffects() {
  // 这里才真的执行 useEffect
  let effect = fiber.firstEffect;
  while (effect) {
    if (effect.tag === HasEffect) {
      // 执行你的函数
      const destroy = effect.create();
      // 保存清理函数
      effect.destroy = destroy;
    }
    effect = effect.nextEffect;
  }
}

四、依赖数组怎么工作的?

1. 第一次渲染

复制代码
// 你写:
useEffect(() => {}, [a, b]);

// React:
const prevDeps = null;  // 上次依赖是 null
const nextDeps = [a, b]; // 这次依赖
const areEqual = areHookInputsEqual(nextDeps, prevDeps);
// 第一次:null 和 [a,b] 肯定不等
// 标记要执行
effect.tag = HasEffect;

2. 后续渲染

复制代码
// 第二次渲染:
const prevDeps = [a, b];  // 上次的
const nextDeps = [a, b];  // 这次的
const areEqual = areHookInputsEqual(nextDeps, prevDeps);
// 浅比较,一样
// 标记不执行
effect.tag = NoEffect;

// 依赖变了:
const prevDeps = [a, b];
const nextDeps = [a, c];  // b 变 c
// 浅比较,不一样
// 标记要执行
effect.tag = HasEffect;

3. 浅比较的实现

复制代码
function areHookInputsEqual(nextDeps, prevDeps) {
  if (prevDeps === null) return false;  // 第一次
  
  for (let i = 0; i < prevDeps.length; i++) {
    // 用 Object.is 比较
    if (!Object.is(nextDeps[i], prevDeps[i])) {
      return false;  // 有一个不一样就重执行
    }
  }
  return true;  // 全一样就不执行
}

五、清理函数怎么工作的?

1. 执行时机

复制代码
// 组件更新时:
1. 先执行上次的清理函数(如果有)
2. 再执行新的 effect

// 组件卸载时:
1. 执行清理函数
2. 然后组件滚蛋

// 代码:
function updateEffect(create, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps;
  
  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 依赖没变,跳过
        return;
      }
    }
    // 依赖变了,标记要清理旧的
    hook.memoizedState.tag |= HasEffect | HookHasEffect;
  }
}

2. 清理函数的保存

复制代码
// 执行 effect 时:
function commitPassiveEffect(fiber, effect) {
  // 先清理旧的
  if (effect.destroy !== undefined) {
    effect.destroy();  // 执行上次的清理
  }
  
  // 执行新的
  const destroy = effect.create();
  // 保存清理函数,下次用
  effect.destroy = destroy;
}

六、useEffect 和 useLayoutEffect 的区别

useLayoutEffect

复制代码
// 执行时机:commit 的 Layout 阶段
// 特点:同步,阻塞浏览器绘制
// 类比:拉完屎马上擦,不擦完不让冲水
// 用途:改 DOM 布局,需要同步

// 代码位置:
function commitLayoutEffects() {
  while (nextEffect !== null) {
    if (effect.tag === Update) {
      const instance = nextEffect.stateNode;
      if (是useLayoutEffect) {
        // 同步执行!
        instance.someLayoutEffect();
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}

useEffect

复制代码
// 执行时机:commit 之后,下一帧
// 特点:异步,不阻塞绘制
// 类比:拉完屎先冲水,过会儿再擦
// 用途:数据获取、订阅等副作用

// 代码位置:
scheduleCallback(NormalPriority, () => {
  flushPassiveEffects();  // 下一帧执行
});

七、React 18 的严格模式坑爹行为

复制代码
// 开发环境下,React 18 会:
1. 挂载组件
2. 执行 effect
3. 立即卸载组件(清理 effect)
4. 再挂载组件(再执行 effect)
5. 结果:effect 执行两次

// 为什么?为了暴露不正确的清理函数
// 你的 effect 应该能承受:执行 → 清理 → 再执行
// 如果你写了:setInterval 但不清理,这里就暴露了

// 生产环境:只执行一次
// 开发环境:执行两次,帮你找 bug

八、useEffect 的常见傻逼用法

1. 依赖数组漏项

复制代码
// 傻逼:
const [count, setCount] = useState(0);
useEffect(() => {
  console.log(count);
}, []);  // 漏了 count
// 永远打印 0,count 变了也不打印

// 聪明:
useEffect(() => {
  console.log(count);
}, [count]);  // 加上

2. 依赖数组放对象/函数

复制代码
// 傻逼:
const obj = { a: 1 };
useEffect(() => {
  console.log(obj);
}, [obj]);  // 对象每次都是新的
// 结果:每次渲染都执行

// 聪明:
const obj = useMemo(() => ({ a: 1 }), []);
useEffect(() => {
  console.log(obj);
}, [obj]);  // 现在稳定了

3. 无限循环

复制代码
// 傻逼:
const [count, setCount] = useState(0);
useEffect(() => {
  setCount(count + 1);  // 更新触发重渲染
}, [count]);  // count 变就执行
// 结果:无限循环,页面卡死

九、useEffect 底层数据流

复制代码
你调用 useEffect
  ↓
React 创建 Effect 对象
  ↓
挂到 Fiber.updateQueue
  ↓
渲染完成
  ↓
commit 阶段
  ↓
调度 flushPassiveEffects(下一帧)
  ↓
执行 Effect
  ↓
保存清理函数
  ↓
下次更新先执行清理

十、一句话总结

useEffect 底层

  • 就是个链表串着所有 effect
  • commit 完成后异步执行(不阻塞绘制)
  • 依赖数组用浅比较,一样就跳过
  • 清理函数在下次执行前调用
  • useLayoutEffect 同步执行,会阻塞绘制

React 心里话

useEffect 就是个定时任务调度器:

  1. 先记下来要干啥(渲染时)
  2. 等正事干完(DOM 更新完)
  3. 抽空执行(下一帧)
  4. 记得收拾(清理函数)

像什么

  • 渲染是上班干活
  • useEffect 是下班后的应酬
  • 你可以不应酬([]),偶尔应酬([dep]),天天应酬(没依赖数组)
  • 应酬前先收拾昨晚的烂摊子(清理函数)
相关推荐
shoa_top2 小时前
一文带你掌握 JSONP:从 Script 标签到手写实现
前端·面试
Crazy_Urus2 小时前
深入解析 React 史上最严重的 RCE 漏洞 CVE-2025-55182
前端·安全·react.js
八荒启_交互动画2 小时前
【基础篇007】GeoGebra工具系列_多边形(Polygon)
前端·javascript
清风扶我腰_直上青天三万里2 小时前
vue框架无痛开发浏览器插件,好用!!本人使用脚手架开发了一款浏览器tab主页加收藏网址弹窗,以后可以自己开发需要的插件了!!
前端
知其然亦知其所以然2 小时前
小米的奇幻编程之旅:当 JavaScript 语法变成了一座魔法城
前端·javascript·面试
webkubor2 小时前
一次 H5 表单事故:100vh 在 Android 上到底坑在哪
前端·javascript·vue.js
是一碗螺丝粉2 小时前
突破小程序5层限制:如何用“逻辑物理分离”思维实现无限跳转
前端·架构
神秘的猪头2 小时前
🎉 React 的 JSX 语法与组件思想:开启你的前端‘搭积木’之旅(深度对比 Vue 哲学)
前端·vue.js·react.js
三十_2 小时前
如何正确实现圆角渐变边框?为什么 border-radius 对 border-image 不生效?
前端·css