摘要:从DOM事件演进到React合成事件,onClick触发模型下载,setStatus驱动loading态,map遍历文件生成进度条列表,Progress子组件接收props展示每个文件的下载进度。一步步让页面活起来。
目录
- [从原生事件到 React 合成事件](#从原生事件到 React 合成事件 "#%E4%B8%80%E4%BB%8E%E5%8E%9F%E7%94%9F%E4%BA%8B%E4%BB%B6%E5%88%B0-react-%E5%90%88%E6%88%90%E4%BA%8B%E4%BB%B6")
- [Load Model 按钮:onClick 触发状态切换](#Load Model 按钮:onClick 触发状态切换 "#%E4%BA%8Cload-model-%E6%8C%89%E9%92%AEonclick-%E8%A7%A6%E5%8F%91%E7%8A%B6%E6%80%81%E5%88%87%E6%8D%A2")
- [map 列表渲染:让 Vue 开发者秒懂的 React 哲学](#map 列表渲染:让 Vue 开发者秒懂的 React 哲学 "#%E4%B8%89map-%E5%88%97%E8%A1%A8%E6%B8%B2%E6%9F%93%E8%AE%A9-vue-%E5%BC%80%E5%8F%91%E8%80%85%E7%A7%92%E6%87%82%E7%9A%84-react-%E5%93%B2%E5%AD%A6")
- [Progress 组件:抽离、传参、复用](#Progress 组件:抽离、传参、复用 "#%E5%9B%9Bprogress-%E7%BB%84%E4%BB%B6%E6%8A%BD%E7%A6%BB%E4%BC%A0%E5%8F%82%E5%A4%8D%E7%94%A8")
- 总结
一、从原生事件到 React 合成事件
第一篇搭好了静态页面骨架------标题、描述文字、模型介绍、空白的进度条区域。这一篇要让页面动起来:点击按钮 → 触发下载 → 进度条实时更新。
这一切的起点是事件处理 。在 React 里,按钮的点击事件写作 onClick,这背后有一段 DOM 事件标准的演进史。
DOM 事件的三代演化
html
<!-- 第 0 代:内联属性 onclick -->
<button id="btn" onclick="console.log('点击')">按钮</button>
DOM 0 级事件直接写在 HTML 属性里,JS 与 HTML 耦合在一起,不符合模块化分离原则。
javascript
// 第 2 代:addEventListener(DOM 2 级标准)
document.getElementById('btn')
.addEventListener('click', function() {
console.log('点击了按钮');
});
addEventListener 将 JS 逻辑从 HTML 中分离出来,而且支持同一个元素多次监听同一个事件------这是 DOM 0 做不到的。
React 的取舍:不发明新概念
Vue 用 @click、v-on:click 这类自定义语法绑定事件。React 选择了另一条路------直接用 JS 里已有的概念 。onClick 就是 HTML 的 onclick 的驼峰写法,对有 JS 基础的开发者没有额外的学习成本。
但 React 的 onClick 不是原生 DOM 事件 。它是合成事件(Synthetic Event)------React 在底层将原生事件包装了一层:
arduino
用户点击 → 原生 click 事件 → React 事件系统(事件委托) → 合成事件对象 → 你的 onClick 回调
合成事件做了三件事:跨浏览器兼容(抹平不同浏览器的差异)、事件委托(不在每个元素上单独绑定,统一在根节点管理)、自动清理(组件卸载时无需手动 removeEventListener)。
二、Load Model 按钮:onClick 触发状态切换
理解了事件机制,来看 App 组件的 Load Model 按钮------整个页面的"启动键":
tsx
<button
className="border px-4 py-2 rounded-lg bg-blue-400
text-white hover:bg-blue-500 disabled:cursor-not-allowed
select-none"
disabled={status !== null || error !== null}
onClick={() => {
setStatus("loading");
}}
>
Load Model
</button>
这一个按钮包含了交互设计的三个关键细节:
disabled 状态控制
tsx
disabled={status !== null || error !== null}
| 场景 | status | error | disabled | 效果 |
|---|---|---|---|---|
| 初始状态 | null |
null |
false |
按钮可点击 |
| 下载中 | "loading" |
null |
true |
按钮灰色,不可点击 |
| 下载完成 | "ready" |
null |
true |
按钮灰色,模型已就绪 |
| 下载失败 | null |
"出错" |
true |
按钮灰色,先处理错误 |
disabled:cursor-not-allowed 原子类在按钮禁用时将鼠标变为禁止样式------视觉上明确告知用户"现在不能点"。防止用户在模型下载过程中重复点击引发多个并发的下载请求。
onClick 的状态切换
tsx
onClick={() => {
setStatus("loading");
}}
整个页面的状态流转由这一行启动:
第一篇讲过 React 的"数据驱动视图"------
setStatus("loading")就是一个完美的例子。你不需要写document.getElementById("progress").style.display = "block"这样的 DOM 操作。你只需要修改一个状态变量,React 自动算出哪些 UI 需要更新。
三、map 列表渲染:让 Vue 开发者秒懂的 React 哲学
模型文件不是一个,而是一组------多个 .onnx 权重文件需要逐个下载。每个文件都需要独立的进度条。这种"数组 → 多个 UI 元素"的场景,Vue 用 v-for,React 用什么?
答案是:不用任何自定义指令。直接用 JS 原生方法 map。
tsx
{
status === "loading" && (
<div className="w-full max-w-[500px] text-left mx-auto p-4">
<p className="text-center mb-1">{loadingMessage}</p>
{progressItems.map(({ text, percentage, total }) => (
<Progress
text={text}
percentage={percentage}
total={total}
/>
))}
</div>
)
}
progressItems 是一个对象数组:
typescript
const [progressItems, setProgressItems] = useState([
{ text: 'model.onnx', percentage: 0, total: 34353543453 },
{ text: 'model2.onnx', percentage: 10, total: 14353543453 },
]);
map 遍历这个数组,每一项都变成一个 <Progress/> 组件------JSON 数组直接被"映射"为 JSX 数组。 Vue 的 v-for 是模板语法,React 的 map 是纯 JS 逻辑,不需要学习额外的指令。
双层条件渲染
这里出现了第一篇提过的 && 短路表达式,而且是嵌套条件:
ini
status === "loading" ← 外层条件:只有 loading 状态才渲染进度区域
↓
progressItems.map(...) ← 内层循环:每个文件一个 Progress 组件
两层组合起来的效果:点击 Load Model → status 变为 loading → 进度条区域出现 → progressItems 数组动态更新 → map 自动重新渲染每个 Progress 组件。不需要手动增删 DOM 节点。
四、Progress 组件:抽离、传参、复用
进度条不是 App 的内部逻辑------它是可复用的独立业务模块。把它从 App 中抽离为一个独立组件,符合"组件作为开发最小单元"的前端发展趋势。
子组件定义
tsx
const Progress = ({ text, percentage, total }) => {
percentage ??= 0;
return (
<div>
<p>{text}</p>
<p>{percentage}%</p>
<p>{total}</p>
</div>
);
};
export default Progress;
三个关键语法:
| 语法 | 含义 | 说明 |
|---|---|---|
{ text, percentage, total } |
解构 props | 从父组件传入的属性对象中一次性提取三个字段 |
percentage ??= 0 |
空值合并赋值 | 如果 percentage 是 null 或 undefined,赋默认值 0 |
| 返回 JSX | 组件 = 函数 | 函数接收 props,返回 UI 描述 |
父子组件数据流
父组件 (App) 拥有数据源 progressItems,通过 props 将每条数据注入子组件。子组件 (Progress) 只负责根据 props 渲染 UI,不关心数据从哪来------这就是数据单向流动。
组件树:从 DOM 树到组件树
传统页面的结构靠 DOM 树表达。React 页面的结构靠的是组件树:
scss
App
├── h1 (标题)
├── h2 (副标题)
├── p (模型介绍)
├── button (Load Model)
├── error 条件渲染
└── loading 条件渲染
├── Progress (model.onnx)
└── Progress (model2.onnx)
组件树一眼能看出页面的构成:组件化程度、嵌套层级、数据传递路径。页面越复杂,组件树的价值越大------团队协作靠组件拆分,代码维护靠组件边界。
五、总结
从一篇的静态骨架到这一篇的动态交互,三个核心机制让页面"活"了起来:
- React 合成事件 :
onClick不是原生事件,是跨浏览器兼容、事件委托、自动清理的包装层。React 不发明新概念,onClick就是onclick的驼峰版。 - onClick → setStatus :一个点击 → 状态变更 → 重渲染 → UI 自动更新。
disabled防止重复点击,loading状态驱动进度条区域显示,不需要任何document.getElementById。 - map 列表渲染 :Vue 用
v-for,React 直接用 JS 原生map。JSON 数组 → JSX 数组,不引入额外指令。 - Progress 组件抽离 :函数接收 props 返回 JSX = 组件。
??=做默认值兜底。父组件持有数据,子组件接收渲染,数据单向流动。 - 组件树 > DOM 树:页面结构通过组件层级表达,一眼看懂数据流向和嵌套关系。组件是开发的最小单元------团队好协作、好复用、好维护。
第一篇把模型介绍和下载链路搭起来,这一篇把点击、进度、状态切换装上去。下一篇,LLM 该开始真正"说话"了。
------ 事件触发状态,状态驱动渲染,组件树承载一切。