从原生事件到 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 子组件。
相关推荐
爱分享的程序猿-Clark1 小时前
【前端分享】vue开发项目,如何实现 从A页面跳转到B页面,并传值!
前端·javascript·vue.js
徐小夕2 小时前
花了3天,我写了一款开源AI公众号编辑器
前端·vue.js·github
PBitW2 小时前
git 中容易遗忘的点 (二) ⚡⚡⚡
前端·git·面试
Hilaku2 小时前
2026 年前端大洗牌:React 服务端到 Vite 的底层重构
前端·javascript·程序员
PBitW2 小时前
git 中容易遗忘的点 (三) 🚀🚀🚀
前端·git·面试
名字还没想好☜2 小时前
React setState 连续调用「丢更新」排查:批量更新与函数式更新
前端·javascript·react.js·react·usestate
sugar__salt2 小时前
DeepSeek-R1 WebGPU (1):在浏览器里跑大模型
react.js·reactjs·react·端模型
come112343 小时前
Vue 2 与 Vue 3 语法区别及使用技巧
前端·javascript·vue.js
腻害兔3 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:ERP 企业资源模块,一个轻量级进销存的完整实现?
前端·javascript·vue.js·人工智能·前端框架·产品经理·ai编程