从原生事件到 React 组件化:用状态驱动模型加载进度 UI
在浏览器端运行 AI 模型时,页面通常会经历"点击加载 → 显示加载提示 → 展示多个文件进度 → 成功或报错"的过程。
这个小型 WebGPU Demo 目前还没有接入真正的模型下载和推理,但已经完成了一个很典型的 React UI 骨架:用状态管理界面、用 map 渲染进度列表、把每一行进度抽成独立组件。
本文基于当前代码梳理,运行未验证。
1. 原生事件:onclick 与 addEventListener
原生 HTML 可直接在标签上写事件:
html
<button onclick="console.log('点击了')">按钮</button>
这种方式叫内联事件。问题是 HTML 和 JavaScript 耦合在一起,逻辑复杂后难维护。
另一种是 DOM API:
js
const button = document.getElementById('btn')
button.addEventListener('click', () => {
console.log('点击了')
})
addEventListener 支持同一元素、同一事件绑定多个监听器,也方便拆分逻辑,因此原生开发中更常用。
2. React 为什么不用手动找 DOM
React 更常见的写法是:
tsx
<button onClick={() => setStatus('loading')}>
Load Model
</button>
这里不需要 getElementById,也不用手动操作 DOM。点击后只修改状态:
tsx
setStatus('loading')
React 会根据新状态重新计算页面应该长什么样。
可以把两种思路对比为:
text
原生 DOM:点击 → 找元素 → 手动修改页面
React: 点击 → 修改状态 → React 更新页面
React 的 onClick 使用的是 React 的合成事件机制。日常开发只需要按照 JSX 事件属性的方式使用即可。
3. 用状态表达模型加载过程
组件中定义了多类状态:
tsx
const [status, setStatus] = useState(null)
const [error, setError] = useState(null)
const [loadingMessage, setLoadingMessage] = useState('开始加载')
它们分别表示:
status:当前页面阶段,例如未开始、加载中、完成;error:发生异常时的错误内容;loadingMessage:显示给用户的加载提示。
当状态变为 loading 时才显示进度区域:
tsx
{status === 'loading' && (
<div>
<p>{loadingMessage}</p>
</div>
)}
&& 是 JavaScript 的短路逻辑:左侧为真,才返回右侧 JSX;左侧为假,React 不渲染该内容。
这是 React 条件渲染的常见写法。
4. 禁用按钮,避免重复加载
当前按钮使用状态控制 disabled:
tsx
<button
disabled={status !== null || error !== null}
onClick={() => setStatus('loading')}
>
Load Model
</button>
含义是:一旦开始加载,或已经发生错误,就不能再点"加载模型"。
这不是单纯的样式控制,而是浏览器按钮的真实禁用能力。Tailwind 还能为禁用状态添加样式:
tsx
className="disabled:cursor-not-allowed"
5. 用 map 把进度数据变成页面
进度不是单个值,而是一组文件:
tsx
const [progressItem] = useState([
{ text: 'model.onnx', percentage: 0, total: 123456 },
{ text: 'model2.onnx', percentage: 10, total: 123456 },
])
使用 map 可以将每项数据转换成一个组件:
tsx
{progressItem.map(({ text, percentage, total }) => (
<Progress
key={text}
text={text}
percentage={percentage}
total={total}
/>
))}
核心过程:
text
进度数据数组 → map 加工 → Progress 组件数组 → React 渲染列表
其中 key={text} 用于标识每一项。React 依赖稳定且唯一的 key 来判断列表更新时"哪一项对应哪一项"。
6. 为什么要抽取 Progress 组件
单个文件进度的展示被拆到 Progress:
tsx
const Progress = ({ text, percentage, total }) => {
return (
<div>
<p>{text}</p>
<p>{percentage}%</p>
<p>{total}</p>
</div>
)
}
父组件负责:
- 保存状态;
- 响应用户点击;
- 遍历进度数据。
Progress 组件负责:
- 接收
text、percentage、total; - 展示一条文件进度。
这就是组件化的价值:父组件管理业务和数据,子组件负责独立 UI 。未来想把文本换成真正的进度条,只需要修改 Progress 组件。
7. 当前 Demo 还差哪些关键步骤
当前代码已经能表达加载 UI 状态,但真正的端侧模型加载尚未接入。后续可以逐步补充:
- 点击后下载或初始化模型;
- 下载事件中更新
progressItem; - 加载成功后将
status改为ready; - 失败时调用
setError(...); - 为 Props 声明 TypeScript 类型。
此外,列表渲染应保留稳定 key,并将 mx-w-[510px] 修正为 max-w-[510px]。
总结
这个页面体现了 React 的核心开发模式:不直接操作 DOM,而是维护状态,再让 UI 根据状态自动变化。
- 原生事件推荐
addEventListener;React 中使用onClick; useState负责保存加载状态;&&实现条件渲染;map把数组转换为 JSX 列表;- Props 将数据从父组件传给可复用的
Progress子组件。