从原生事件到 React 组件化:用状态驱动模型加载进度 UI

从原生事件到 React 组件化:用状态驱动模型加载进度 UI

在浏览器端运行 AI 模型时,页面通常会经历"点击加载 → 显示加载提示 → 展示多个文件进度 → 成功或报错"的过程。

这个小型 WebGPU Demo 目前还没有接入真正的模型下载和推理,但已经完成了一个很典型的 React UI 骨架:用状态管理界面、用 map 渲染进度列表、把每一行进度抽成独立组件。

本文基于当前代码梳理,运行未验证

1. 原生事件:onclickaddEventListener

原生 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 组件负责:

  • 接收 textpercentagetotal
  • 展示一条文件进度。

这就是组件化的价值:父组件管理业务和数据,子组件负责独立 UI 。未来想把文本换成真正的进度条,只需要修改 Progress 组件。

7. 当前 Demo 还差哪些关键步骤

当前代码已经能表达加载 UI 状态,但真正的端侧模型加载尚未接入。后续可以逐步补充:

  1. 点击后下载或初始化模型;
  2. 下载事件中更新 progressItem
  3. 加载成功后将 status 改为 ready
  4. 失败时调用 setError(...)
  5. 为 Props 声明 TypeScript 类型。

此外,列表渲染应保留稳定 key,并将 mx-w-[510px] 修正为 max-w-[510px]

总结

这个页面体现了 React 的核心开发模式:不直接操作 DOM,而是维护状态,再让 UI 根据状态自动变化。

  • 原生事件推荐 addEventListener;React 中使用 onClick
  • useState 负责保存加载状态;
  • && 实现条件渲染;
  • map 把数组转换为 JSX 列表;
  • Props 将数据从父组件传给可复用的 Progress 子组件。
相关推荐
程序员黑豆4 小时前
Java 注释详解:单行、多行与文档注释的完整指南
java·前端·ai编程
剪刀石头布啊4 小时前
antd中可编辑表格中巧用useWatch实现联动高性能效果,以及定制延伸学习
前端
油丶酸萝卜别吃5 小时前
前端转全栈学习路线
前端·学习
浮生望5 小时前
前端路由进阶:History API原理与手写HistoryRouter
前端
陆枫Larry5 小时前
JavaScript 中的竞态是什么,为啥会有竟态?
前端
上海安当技术7 小时前
半天接入:USBKey RESTful API + C 动态库,Web 和 C/S 两套集成路径实战
前端·后端·restful·集成·usbkey
DevUI团队8 小时前
从“即兴创作”到“规格先行”,华为云码道(CodeArts)代码智能体持续深耕企业级规范驱动开发能力
前端·人工智能·后端
weixin_431600449 小时前
NestJS 入门(3):Guard 如何挡住未登录请求?
前端·后端·学习·nest.js
kyriewen9 小时前
我用Claude Code两天干完了团队两周的排期——周报发出去那一刻我就后悔了
前端·javascript·ai编程
IT_陈寒10 小时前
JavaScript类型转换把我坑惨了,这破玩意真该早点搞明白
前端·人工智能·后端