🚀 端侧 AI DEEPSEEK-R1-WEBGPU 项目实战(二):封装进度条组件,看懂 React 事件与组件树

系列定位:从零开始,在浏览器里用 WebGPU 跑通一个 DeepSeek-R1 蒸馏模型 。 这是第二篇。上一篇我们搭好了骨架、吃透了 App.tsx 这个「第一个组件」;这一篇,我们往前进一大步------点下「Load Model」按钮会发生什么? 顺着这条线,我们把 React 事件、组件拆分(Props)、列表渲染、组件树 这四个概念一次打通。


📌 一、先看这一篇要解决什么问题

上一篇结束时,我们的 App.tsx 已经能渲染出静态页面了:标题、介绍文字、一个「Load Model」按钮,还有一个 progressItems 数据状态。

但还有几个「悬而未决」的疑问:

  1. 🤔 按钮上的 onClick 是干嘛的?它和原生 HTML 里的 onclick 是同一个东西吗?
  2. 🤔 下载模型的时候,会有好几个文件(模型、分词器、配置......)同时下载,界面上要怎么一次渲染一排进度条
  3. 🤔 进度条这种东西,别的页面也会用到,能不能抽成一个独立的组件复用?

这三个问题,正好对应这一篇要讲的三个主题:合成事件列表渲染 + 组件拆分组件树


🎯 二、点按钮:认识 React 合成事件

2.1 先看看我们写了什么

上一篇的 App.tsx 里,按钮长这样:

tsx 复制代码
<button
  className="border px-4 py-2 rounded-lg bg-blue-400 ..."
  disabled={status !== null || error !== null}
  onClick={() => {
    setStatus("loading");
  }}
>
  Load Model
</button>

最扎眼的就是这个 onClick。你可能会想:这不是 HTML 里的 onclick 吗?怎么还大写了个 C

🔥 它俩不是一个东西。 为了讲清楚 React 的 onClick 为什么长这样、以及它背后「合成事件」是怎么回事,我们得先往回倒 20 多年,看看浏览器原生的事件监听是怎么一步步演化的。

2.2 事件监听的「三朝更迭」

浏览器里「给元素绑定一个事件处理函数」这件事,经历了三代。

第一代:DOM 0 级 ------ onclick 内联写在标签上

最早,人们直接把 JS 塞进 HTML 标签里:

html 复制代码
<button onclick="doSomething()">点我</button>

它的毛病很明显:

🍝 三剑客耦合。HTML(结构)、CSS(样式)、JS(行为)本来应该各管各的、解耦分离。这种写法把 JS 硬生生写进了 HTML 标签里,结构和行为缠在一起,代码一多就乱成一锅粥,极难维护。

这就是 DOM 0 级事件------最原始、最简陋的阶段。

第二代:DOM 1 级 ------ 没更新事件

DOM 1 是 HTML 标准的一次迭代升级,但重点放在了节点、文档结构 等 API 上,事件相关的部分没有更新。所以「事件」还停留在 DOM 0 的原始状态。

💡 记住这个演进脉络就行:DOM 0 → DOM 1 → DOM 2,事件真正「现代化」是在 DOM 2。

第三代:DOM 2 级 ------ addEventListener 横空出世

DOM 2 带来了我们今天最常用的 addEventListener

js 复制代码
const btn = document.getElementById('btn');
btn.addEventListener('click', handler1);  // 挂一个监听
btn.addEventListener('click', handler2);  // 还能再挂一个!

它的两个关键进步:

  1. 🧩 彻底解耦:JS 不再写在标签里,而是通过「选择器找到元素 → 注册监听」的方式,把行为完全独立出来;
  2. 🔁 可多次监听同一个 DOM 元素、同一事件,可以注册多个监听函数,互不覆盖。

🎭 对比 DOM 0:btn.onclick = fn1; btn.onclick = fn2; 这种写法,fn2覆盖 fn1。而 addEventListener 让两者并存。

2.3 React 的洁癖:能不发明新概念就不发明

好,现在回到 React。Vue 和 React 给元素绑定事件的方式,暴露了两种截然不同的「性格」:

框架 写法 风格
🟢 Vue @click="handler" 发明了一个新符号 @,专门用来做事件绑定
⚛️ React onClick={handler} 直接沿用已有的概念 ,只是把 onclick 改成了驼峰 onClick

🧼 React 的代码洁癖 :能不发明新概念、新符号就不发明。Vue 里你要先学会「@ 表示绑定事件」这个专属语法;而 React 用 onClick,你只要学过 HTML 的 onclick,几乎零成本就能上手。「作为高手,没有额外的学习成本」------这正是 React 引以为傲的一点。

2.4 但注意:React 的事件并不是「原生事件」

关键来了。虽然 React 用 onClick 这种「看似熟悉」的写法,但它底层监听的不是原生 DOM 事件,而是 React 自己造的一套「合成事件(SyntheticEvent)」

🎭 合成事件 :React 并不是把每个 onClick 都直接 addEventListener 到那个元素上,而是统一在顶层做事件委托,包装成一个跨浏览器、行为一致的「合成事件对象」再派发给你。

为什么这么做?简单说就是为了抹平不同浏览器的事件差异,并配合 React 的「批量更新」机制,让事件处理和状态更新更高效、更一致。

所以结论是:

  • 写法上 用的还是熟悉的 onClick(心智负担小);
  • 底层它经过 React 合成事件的封装(更可靠、更高效)。

💡 到这里,onClick={() => setStatus("loading")} 也就读懂了:点按钮 → 把 status 状态改成 "loading" 。还记得上一篇的「响应式」吗?状态一变,界面自动跟着变------下一篇你就会看到 status === "loading" 时那排进度条是怎么冒出来的。


🧩 三、封装进度条组件:抽一个 <Progress />

3.1 为什么要单独抽出来?

下载模型时,页面上会显示「正在下载哪个文件、进度多少、文件多大」。这套逻辑:

  • 🔩 比较独立:它只关心「文件 + 进度 + 大小」三个数据;
  • ♻️ 可复用:模型由好几个文件组成,每个文件都要一个进度条;将来别的页面也可能用到。

这种「独立、可复用的业务模块」,就适合App.tsx 里抽离出来,单独做成一个组件 。于是我们新建了 src/components/Progress.tsx

🗂️ 注意:组件单独放进了 components/ 目录,而不是和 App.tsx 平铺在一起------这是 React 项目的通用约定:大而全的页面组件放 pages/,小而可复用的功能组件放 components/

3.2 组件 = 带参数的函数(Props)

Progress.tsx 的完整代码:

tsx 复制代码
// 返回 jsx 的函数就是组件
// 函数接受参数,复用组件的时候,进度、文件、大小不一样
// 组件的属性 以 html 属性的方式传过来 ------ props
const Progress = ({ text, percentage, total }) => {
  console.log(text, percentage, total);
  return (
    <div>
      <p>{text}</p>
      <p>{percentage}%</p>
      <p>{total}</p>
    </div>
  )
}

export default Progress;

还记得第一篇的核心认知吗?「组件就是一个函数,函数返回 JSX」。现在往前再补一步:

🧩 组件 = 返回 JSX 的函数,而且这个函数还能接收参数。 这些参数就叫 Props(属性)

这里 ({ text, percentage, total }) 用的还是我们熟悉的对象解构 ------父组件用「HTML 属性的样子」把数据传进来,组件内部直接解构取出。这行 console.log 打印出来的,就是每次传入的不同数据。

3.3 为什么需要 Props?

试想:模型有 model.onnxmodel2.onnx 好几个文件,每个进度条显示的文件名、百分比、大小都不一样

  • ❌ 如果不用参数,你就得复制粘贴写好几个「长得一模一样、只是数字不同」的进度条;
  • ✅ 用 Props,你只写一个 Progress 组件,然后传不同的参数进去,就能渲染出内容各不相同的多个进度条。

🎭 这就像同一个「表格模板」,填上不同的姓名、成绩,就变成不同学生的成绩单。模板(组件)只有一份,数据(Props)每次不同。

3.4 JSX 里怎么用组件?

组件写好了,怎么在 App.tsx 里用?答案是------把它当成一个自定义的 HTML 标签

tsx 复制代码
<Progress text={text} percentage={percentage} total={total} />

几个要点:

  1. 🏷️ 自定义标签:组件函数可以直接以「类 HTML 标签」的方式插入到 JSX 里;
  2. 📦 属性传参text={text} 这种写法,就是「以 HTML 属性的方式」把数据作为 Props 传进去;
  3. 🚪 自闭合标签 :没有子内容时,可以用 <Progress /> 这种 XML 自闭合写法。

💡 这正是 React 组件化的魅力:你写的组件,用起来和 divbutton 这些原生标签一样自然------这就是为什么说 React 是「搭积木」式开发。


🔁 四、列表渲染:用 map 一次渲染一排进度条

现在我们知道「单个进度条怎么来」,可模型有好几个文件,要一排进度条怎么办?

4.1 数据已经是一组了

回到 App.tsxprogressItems 状态,它已经是一个数组:

tsx 复制代码
const [progressItems, setProgressItems] = useState([{
  text: 'model.onnx',
  percentage: 0,
  total: 34353543453
}, {
  text: 'model2.onnx',
  percentage: 10,
  total: 14353543453
}]);

这是一个由两个对象组成的 JSON 数组,每个对象对应一个要下载的文件。

4.2 Vue 用 v-for,React 用原生 map

渲染一排进度条,本质是「把一个 JSON 数组,变成一个 JSX 数组 」。Vue 为此专门造了个指令 v-for;而 React------还是那个洁癖------绝不发明新概念,直接用原生 JS 的 map

tsx 复制代码
progressItems.map(({ text, percentage, total }, i) => (
  <Progress text={text} percentage={percentage} total={total} />
))

拆开看:

  • 🔁 map 是原生 JS 数组方法:遍历数组每一项,返回一个「新数组」;
  • 🧩 ({ text, percentage, total }, i) :回调函数的第一个参数是「数组的每一项」(这里用对象解构直接拆出三个字段),第二个参数 i 是下标;
  • 🎯 返回值 :每一项都返回一个 <Progress />,最后 map 把这一串 <Progress /> 收集成一个新的 JSX 数组,React 会把它整体渲染出来。

🎭 一句话map 就是把「数据数组」翻译成「组件数组」。2 个文件 → 2 个进度条;以后 progressItems 变成 5 项,进度条也自动变成 5 个------这又和「数据驱动界面」呼应上了。


🌳 五、组件树:页面结构的新视角

5.1 从 DOM 树到组件树

以前我们分析一个网页,习惯看它的 DOM 树 ------一堆 divpspan 层层嵌套的标签结构。

但 React 里,我们更应该看的是组件树

xml 复制代码
App (应用根组件)
├── <h1> 标题
├── <h2> 副标题
├── <p>  介绍文字
├── <button> Load Model 按钮
└── <div> 下载区
    ├── <Progress />  ← 第 1 个文件
    └── <Progress />  ← 第 2 个文件

🌳 组件树代替了 DOM 树App 是根组件,Progress 是它的子组件。页面不再是「一堆标签」,而是「一棵由组件拼成的树」。

5.2 一眼看出页面的构成

组件树最大的好处是可读性 ------你扫一眼 App 的 JSX,就知道这个页面由哪几个组件拼成,页面的组件化程度、拆分的粒度一目了然。

💡 一个健康的大型前端项目,页面一定是「组件的组合」,而不是「标签的堆砌」。组件树的清晰程度,直接反映项目结构的质量。

5.3 为什么组件树是前端的必然

  • 📈 页面和交互越来越复杂:靠「平铺一地的标签 + 散落的脚本」根本撑不住;
  • 🧱 组件成为开发的最小单元:就像建筑业以「预制板」为单位盖楼,前端以「组件」为单位搭界面;
  • 🤝 团队好协作、好复用、好维护:每个人负责几个组件,互不干扰;组件写好一次,处处复用;改 bug 只需定位到某一个组件,不用在一整坨代码里大海捞针。

🎭 一句话收束这一篇的核心思想:从「写页面」到「组组件」------这就是 React 组件化开发的心法。


🔍 六、小结

这一篇我们顺着「点按钮 → 下载模型」这条线,打通了四个概念:

概念 一句话理解
🎯 合成事件 React 用 onClick(不造新概念),底层却是封装过的「合成事件」
📜 DOM 0/1/2 事件从「标签内联」→「addEventListener 解耦」,一步步现代化
🧩 Props 组件 = 返回 JSX 的带参函数,参数以 HTML 属性方式传入
🔁 map React 不造 v-for,直接用原生 map 把「数据数组」翻译成「组件数组」
🌳 组件树 页面从「标签堆砌」进化为「组件拼装」,好复用、好协作、好维护

到这里,我们已经能点下「Load Model」,让界面进入 loading 状态并渲染出进度条了。但真正的「加载模型」还没发生------onClick 只是改了状态,还没真正去下载、去推理。

相关推荐
皮皮狗工坊1 小时前
贡院计划第一弹:DeepSeek Harness考生翻墙抄了答案,还企图隐藏罪证
人工智能
Dawson Zhu1 小时前
Agent自我纠错死循环:从原理剖析到工程化防御体系构建
人工智能·语言模型·架构·aigc
ZJU_统一阿萨姆1 小时前
【算子开发】卷积算子基础实现与优化
人工智能·语言模型
Justin3go1 小时前
DeepSeek Harness 如何做到 99% 缓存命中率(原理详解)
人工智能·开源·agent·deepseek
luckystar513~1 小时前
自己动手写Agent Harness【hooks】:生命周期钩子实现
人工智能
fightcrap2 小时前
DeepSeek Harness:Cordis 插件树与 Agent 主链路
人工智能·后端·程序员
呆萌很2 小时前
Sigmoid 与 Tanh 激活函数(S 型饱和激活函数)
人工智能·深度学习·机器学习
leeyi2 小时前
MultiAgent Host 源码 + ADK prebuilt 三种预制模式(第92篇-E78)
人工智能·aigc·agent
前沿在线2 小时前
WRC2026丨当机器开始理解人的意图,人机交互走向更多场景
人工智能·ai·大模型