系列定位:从零开始,在浏览器里用 WebGPU 跑通一个 DeepSeek-R1 蒸馏模型 。 这是第二篇。上一篇我们搭好了骨架、吃透了
App.tsx这个「第一个组件」;这一篇,我们往前进一大步------点下「Load Model」按钮会发生什么? 顺着这条线,我们把 React 事件、组件拆分(Props)、列表渲染、组件树 这四个概念一次打通。
📌 一、先看这一篇要解决什么问题
上一篇结束时,我们的 App.tsx 已经能渲染出静态页面了:标题、介绍文字、一个「Load Model」按钮,还有一个 progressItems 数据状态。
但还有几个「悬而未决」的疑问:
- 🤔 按钮上的
onClick是干嘛的?它和原生 HTML 里的onclick是同一个东西吗? - 🤔 下载模型的时候,会有好几个文件(模型、分词器、配置......)同时下载,界面上要怎么一次渲染一排进度条?
- 🤔 进度条这种东西,别的页面也会用到,能不能抽成一个独立的组件复用?
这三个问题,正好对应这一篇要讲的三个主题:合成事件 、列表渲染 + 组件拆分 、组件树。
🎯 二、点按钮:认识 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); // 还能再挂一个!
它的两个关键进步:
- 🧩 彻底解耦:JS 不再写在标签里,而是通过「选择器找到元素 → 注册监听」的方式,把行为完全独立出来;
- 🔁 可多次监听 :同一个 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.onnx、model2.onnx 好几个文件,每个进度条显示的文件名、百分比、大小都不一样。
- ❌ 如果不用参数,你就得复制粘贴写好几个「长得一模一样、只是数字不同」的进度条;
- ✅ 用 Props,你只写一个
Progress组件,然后传不同的参数进去,就能渲染出内容各不相同的多个进度条。
🎭 这就像同一个「表格模板」,填上不同的姓名、成绩,就变成不同学生的成绩单。模板(组件)只有一份,数据(Props)每次不同。
3.4 JSX 里怎么用组件?
组件写好了,怎么在 App.tsx 里用?答案是------把它当成一个自定义的 HTML 标签:
tsx
<Progress text={text} percentage={percentage} total={total} />
几个要点:
- 🏷️ 自定义标签:组件函数可以直接以「类 HTML 标签」的方式插入到 JSX 里;
- 📦 属性传参 :
text={text}这种写法,就是「以 HTML 属性的方式」把数据作为 Props 传进去; - 🚪 自闭合标签 :没有子内容时,可以用
<Progress />这种 XML 自闭合写法。
💡 这正是 React 组件化的魅力:你写的组件,用起来和
div、button这些原生标签一样自然------这就是为什么说 React 是「搭积木」式开发。
🔁 四、列表渲染:用 map 一次渲染一排进度条
现在我们知道「单个进度条怎么来」,可模型有好几个文件,要一排进度条怎么办?
4.1 数据已经是一组了
回到 App.tsx 的 progressItems 状态,它已经是一个数组:
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 树 ------一堆 div、p、span 层层嵌套的标签结构。
但 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 只是改了状态,还没真正去下载、去推理。