JSX 的本质是什么?
JSX 就他妈是个语法糖!
狗屁原理就这:
- JSX 本质就是个
React.createElement()的替身 ,你写的<div>🐴👴</div>最后全被 babel 这崽种转成React.createElement('div', null, '🐴👴')。 - 吹得花里胡哨的"HTML in JS"实际是扯犊子,本质就是 JS 对象(虚拟 DOM),浏览器根本不认这玩意,得靠 React 这老铁给你渲染成真 DOM。
- 劳资写 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:同一本书,但:
- 每读 5 分钟就抬头问:"要喝水吗?要上厕所吗?"
- 突然有急事?马上夹书签,处理完回来接着读
- 重要的章节优先读,不重要的往后稍稍
六、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 不想当个"只会渲染的傻框架":
- 要支持时间切片 - 别卡用户
- 要玩并发更新 - 多个任务一起搞
- 要实现 Suspense - 异步加载不闪屏
- 要搞离线渲染 - 后台偷偷准备
代价 :代码复杂度上天,React 源码现在像天书,但用户只管爽。
七、一句话总结
VNode 是条狗:你扔飞盘,它必须捡回来才能干别的。
Fiber 是人精:你让它拖地,中途你妈喊吃饭,它会:
- 立即放下拖把
- 记住拖到哪了
- 先去吃饭
- 吃完饭继续拖
- 拖地时还接了个电话
- 所有事都办了,你还觉得它很闲
最后暴论:
- 用 VNode 的框架:我是个渲染器,别的别找我
- 用 Fiber 的 React:我要当操作系统,调度一切,掌控雷电
- Fiber vs VNode:一个是智能机器人,一个是铁憨憨
(React 团队:我们头发掉光写出来的 Fiber,就是为了让你们这些喷子感受不到卡顿,结果你们只关心"为什么打包体积大了 10KB"?)
简述 React diff 算法过程
一、基本原则:能不动就他妈不动
React 的信条:
- 类型变了?直接掀桌重来
<div>变<span>?整个子树全删了重做,懒得多看一眼 - 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
}
五、给菜鸡的忠告
- key 用稳定 ID,别他妈用 index
- 组件类型别乱变,今天 Button 明天 div
- 该拆组件就拆,别一坨屎全堆一起
- 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 看到:
- 可乐状态更新 → 重新渲染一次
- 薯片状态更新 → 再渲染一次
- 冰棍状态更新 → 又渲染一次
- 用户:这界面怎么他妈闪了三次?
二、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 的工作流程:
- 协调阶段:在内存里 YY 要怎么改(可中断,不卡用户)
- 提交阶段:真的动手改 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 就是个定时任务调度器:
- 先记下来要干啥(渲染时)
- 等正事干完(DOM 更新完)
- 抽空执行(下一帧)
- 记得收拾(清理函数)
像什么:
- 渲染是上班干活
- useEffect 是下班后的应酬
- 你可以不应酬([]),偶尔应酬([dep]),天天应酬(没依赖数组)
- 应酬前先收拾昨晚的烂摊子(清理函数)