news 2026/10/18 13:40:56

组件更新机制全解析:从状态变化到渲染优化的实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
组件更新机制全解析:从状态变化到渲染优化的实践指南

组件更新机制,说穿了就是一件事:状态变了之后,框架如何把新状态变成用户能看到的新页面。这个问题听起来基础,但几乎所有前端性能问题,卡顿、白屏、数据没刷新、重复请求,最后都能追溯到更新机制这一环。很多新手把“更新”理解成“setState 之后界面自动就改了”,实际完全不是这么回事。想弄明白优化该从哪里入手,首先得把这条链路看清楚。

如果你写过原生 HTML,应该体会过这种痛苦:页面上有 20 个地方要显示同一个状态,每次更新都要手动把这 20 个 DOM 节点挨个改一遍,改漏一个就出 bug。所以框架引入了状态驱动视图的思路:你只负责描述状态是什么,框架负责把状态变成界面。组件更新机制,就是这条转换路径上的核心引擎。这篇文章我会从机制原理讲到性能优化,再带上项目里的排障经验,适合所有写组件化页面的前端同学参考。

1. 组件更新机制到底在做什么:状态到界面的完整链路

1.1 为什么框架需要一套更新机制

最朴素的更新方式,是“状态变化,页面全部重建”。你不用细想哪个地方变了,直接重新生成整个页面结构丢给浏览器。这种方式在小页面里没问题,页面一复杂就没法用,因为重建会有闪烁、会丢失输入框焦点、会浪费大量性能。所以现代框架普遍采用局部更新的路线,但局部更新不是免费的,框架需要知道三个信息:哪些状态变了、哪些组件依赖这些状态、这些组件生成的结构和上一次相比差在哪。三个问题对应三个环节:触发、计算、提交。组件更新机制,就是这一整套闭环。

这也是为什么框架不能只做一个简单的“数据绑定”就完事。数据绑定只是解决了“状态到界面的映射”,但真正复杂的是“状态变化之后,如何高效地只改动需要改的界面”。一套完善的更新机制,需要在正确性、性能和开发体验之间做平衡。

1.2 一次 setState,框架内部发生了什么

以函数组件为例,看下面这段代码:

function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; }

点击按钮时,setCount(1)并不是像命令式代码那样,直接把按钮文本从 0 改成 1。它做的是:把一个状态更新请求交给框架核心。框架收到请求后,会重新执行Counter这个函数,得到一份新的虚拟结构,再和上一次保存的结构做差异对比,找出“这里从 0 变成了 1”这个最小改动,最后提交到真实 DOM。

这里最反直觉的一点,是组件函数本身每次更新都会重新执行。你写的const node = <button>{count}</button>只是描述你要什么,框架为了得到新描述,会把整个函数的代码再跑一遍。所以一个组件越复杂,render 一次的成本就越高。这是很多性能问题的土壤,也是理解后续所有优化手段的前提。

1.3 别被“局部更新”骗了:更新的粒度是组件级

很多新人以为“局部更新”等于“只改需要变的节点”,这个理解不够准确。主流框架的更新粒度是组件级,甚至默认是“子树级”:父组件状态一更新,整个子树里的所有组件默认都会重新计算。

function ListView({ items }) { return ( <div> {items.map(item => <Row key={item.id} item={item} />)} </div> ); }

ListView一旦重新渲染,所有Row都会重新渲染。这里的“重新渲染”不是指 DOM 被重建,而是指 Row 的渲染函数重新执行、生成虚拟结构、参与 diff。如果数据没变,框架能通过 diff 发现结果一致,不更新真实 DOM。但这不代表没有成本,那一大堆 JS 计算还是白跑了。优化组件更新,本质就是减少这类“白跑”。

2. 更新机制的分野:不同框架选择了不同的追踪路径

上面这套触发、计算、提交的流程,不同框架在第一步就有本质差异。理解这个差异,对选型和优化都很重要。

2.1 显式提交式:状态变了,通知框架刷新

以 React 为代表的思路是“显式提交”。你通过setState或dispatch告诉框架“我这个组件状态变了”,框架不关心这个状态具体怎么变的,也不关心有哪些组件依赖它,做法很简单:重新渲染这个组件以及它的子树,再通过 diff 找差异,最终更新 DOM。

这种方式的优点是简单、可预测。状态更新和渲染之间是单向流程,调试起来很直接,你只需要知道什么状态被 set 了,就能推导出哪些组件会重渲染。缺点也很明显,如果组件层级深、数据量大,默认的全量重渲染会产生大量无效计算。所以这类框架需要开发者自己通过memo、useCallback这类手段告诉框架“这里不需要跟随父组件更新”。

2.2 响应式追踪式:谁读了我,我就通知谁

以 Vue 为代表的思路是自动追踪。它的数据是响应式的,当你的模板或渲染函数读取了某个ref/reactive属性,框架就记下“这个组件依赖这个数据”。当数据发生变化,框架直接通知依赖它的组件,而不是把整棵子树跑一遍。

const count = ref(0); // 读取 count 的地方会自动记录依赖 // 修改 count 时,只有依赖它的组件重新计算

这就是为什么同一个列表页面,这种思路在默认情况下能精准跳过与列表无关的组件更新。但代价是依赖追踪本身有机制开销:每个响应式属性被读取时都要执行依赖收集逻辑,深层嵌套对象的数据流通也需要更谨慎的设计。如果滥用深层响应式,把整个 store 全部塞进reactive里,依赖追踪的成本和调试复杂度都会上升。

2.3 两种思路的取舍与迁移注意点

为了更直观地对比,我整理了一张表:

维度显式提交式响应式追踪式
更新触发手动调用 setState / dispatch赋值时自动触发
默认粒度组件子树全部重渲染精确到依赖该数据的组件
开发者成本需要手动控制无关组件重渲染自动收集依赖,机制本身更“重”
调优重点memo、useCallback、useMemo控制响应式边界、computed、nextTick

从“响应式追踪”类的框架切到“显式提交”式框架,很多人会觉得“为什么我只改了一个 count,整个页面都刷新了”。这不是框架写错了,而是它的设计哲学就是这样。你要做的是接受这种哲学,按它给的优化手段去拆分和 memo 化,而不是硬靠 setState 做细粒度控制,那样只会把流程搞得一团糟。

反过来,从“显式提交”式框架切到“类响应式”框架,要留意响应式依赖在异步流程里可能丢失。比如在定时器里修改一个变量,要确保这个变量还被组件“读取”过,否则组件可能不会更新。两种思路各有代价,没有孰优孰劣,只有合不合适。

3. 更新慢的根源:无效渲染与渲染阻塞

知道了更新机制的原理,接下来看它为什么慢。我总结下来就两个核心问题:无效渲染太多,单次渲染太重。

3.1 无效渲染是怎么悄悄发生的

无效渲染指与本次更新无关的组件,也被迫重新执行了渲染逻辑。最常见的场景是父组件状态更新,哪怕某个子组件收到的 props 原封未动,默认也会跟着跑一遍。你可能会想:框架不是有 diff 吗?为什么不让 diff 来判断?问题就在于,diff 发生在子组件重新执行之后。你得先重新执行,才能拿到新的虚拟结构和旧结构对比。所以 diff 能避免 DOM 更新,但不能避免渲染函数执行。

另一种来源是 context 或全局状态变化。比如登录状态放在全局 store 里,几乎每个组件都订阅了它,登录一变化,所有订阅组件全部重新渲染。有时这是必要的,但如果组件读的是整个上下文中一个不变字段,也会被无效渲染波及。

还有一种非常隐蔽的:组件内部“每次渲染都创建新对象”。比如你在渲染里直接写someProp={{ a: 1 }},就算内容没变,这个对象的引用每次都是新的。把它传给 memo 子组件时,浅比较会认为 props 变了,于是触发一次更新。

3.2 render 里的重活儿与同步阻塞

在渲染函数或模板里做重计算,是更新慢的直接推手。常规的 filter、sort、JSON 序列化这类操作,数据量小没事,数据量大了就是灾难。

const [keyword, setKeyword] = useState(''); // 假设 rows 是 5000 条记录 const filtered = rows.filter(row => row.name.includes(keyword));

每次输入一个字符,整个组件重新渲染,filter 就要把 5000 条数据全跑一遍,界面自然不跟手。这还算轻的,我见过有人把时间格式化、报表聚合逻辑直接写在 render 里,一次渲染几十毫秒,输入响应间隔完全被吃掉,用户打字就像踩在水里。

为什么影响这么大?因为浏览器主线程是单线程的:用户输入、点击事件、JS 执行、布局、绘制全得排队。组件渲染在主线程上花掉 30ms,这 30ms 里用户输入事件就得不到处理。你以为用户在打字,实际每次敲击都要等前面的渲染完成才能响应,体感就是卡。

3.3 被忽略的“每次渲染都重建对象”

这一节单独说一下“每次渲染都新建对象、数组、函数”的问题,因为它太常见,又最容易被忽视。

const Row = memo(({ item }) => { return <div>{item.name}</div>; }); function App() { const rows = [/* 数据 */]; return rows.map(item => ( <Row key={item.id} item={item} onPress={() => handlePress(item.id)} /> )); }

handlePress如果没有useCallback包一层,每次 App 渲染都会生成一个新的箭头函数引用。Row 即使被 memo 包住,浅比较发现 onPress 引用变了,照样重新渲染。你可能会觉得“一个箭头函数而已”,但当一个列表有几百行时,每一行都要生成新函数、重新触发渲染,积少成多就是卡顿源。

要解决也很简单,把 onPress 换成稳定引用:

const handlePress = useCallback((id) => { // 处理选中逻辑 }, []);

稳定引用是缩小更新范围的前提。这句话值得在项目里重点落实。

4. 组件更新优化的实操路线:从触发到对比逐层压榨

前面分析完问题根源,这一章进入实操。优化方向可以按四个层次递进:减少触发,缩小范围,降低对比成本,缓存计算结果。

4.1 减少触发:合并更新与节流

不要做无意义的 setState。有时候开发者为了“刷新一下”而调 set,把无关状态一起重置,这就是最常见的无效触发。

现代框架的批处理帮了大忙。比如在 React 18 中,事件回调、异步回调里连续调用多个 setState,框架只触发一次渲染:

function save() { setSaving(true); setSaved(false); setProgress(0); }

这三次 setState 最终合成一次渲染。在旧版本的某些异步场景里,这三次调用可能触发三次渲染,现在不需要手动包 batch 了。

但在滚动、拖动这类高频事件里,还是需要节流和防抖。比如实时搜索框:

const onInput = debounce((e) => { setKeyword(e.target.value); }, 300);

注意防抖会改变交互时机。如果业务要求每次输入都立即反馈,就不要盲目用防抖,可以换成后面讲到的并发更新方案。

4.2 缩小范围:组件拆分与 memo 的正确姿势

组件拆分是缩小更新范围最有效的手段。把经常变化的输入框和相对稳定的结果列表拆开,输入框内部独立管理自己的状态,列表只接收稳定的 props:

function SearchPage() { return ( <> <SearchInput /> <ResultList /> </> ); }

这样 SearchInput 里的关键词变化不会导致 ResultList 重新渲染。如果必须共享关键词,就让共享状态放在一个尽量小的父组件里,这样影响范围仍然是可控的。

接着是 memo。memo 的作用是浅比较 props 的前一帧和后一帧,如果不相等就跳过这次重渲染。它配合稳定的引用才能发挥作用:

const ResultList = memo(function ResultList({ items, onSelect }) { return items.map(item => <Row key={item.id} item={item} onSelect={onSelect} />); }); function SearchPage() { const [items, setItems] = useState(initialItems); const onSelect = useCallback((id) => { // 处理选中 }, []); return <ResultList items={items} onSelect={onSelect} />; }

这里 onSelect 引用稳定,items 只要是同一个引用,ResultList 就不会重新执行。如果 items 确实被上游更新了并生成了新引用,那 ResultList 重新执行也是合理的。

但不要矫枉过正。一个只含几行简单 JSX 的组件,重渲染成本可能只有 0.1ms,而 memo 的浅比较本身也要遍历 props 字段。给所有组件无脑包 memo,优化收益可能是负的。先量化,再决定。

4.3 降低对比成本:稳定 key 与不可变状态

先说列表 key。diff 算法通过 key 判断两个列表节点是不是同一个组件。key 必须稳定、唯一,最理想的是业务 id。

不推荐用 index。用 index 作为 key,当列表中间插入或删除一项时,后面所有项的 key 都会变,框架会认为这些项不再是原来的项,从而销毁再创建组件,导致性能问题和状态错乱。这在排序、拖拽、可编辑列表里特别明显。

然后是“不可变状态”。很多人以为不可变状态是 React 的特色,实际上它对任何基于浅比较优化的方案都很重要。当你直接修改 state 对象的属性时:

setUser(user); // user.age = 20 之后,引用没有变化

框架对比新旧状态时,发现引用相同,判定没变化,界面不刷新。正确做法是生成新引用:

setUser({ ...user, age: 20 });

浅比较只需要对比引用是否变化,成本是 O(1)。如果每次修改都做深比较,几百个字段的深拷贝和对比成本可能比重渲染还高。这就是不可变数据的价值:它把“判断有没有变”这个操作变得极廉价,同时让 memo、diff、useMemo 都能正常工作。

4.4 组件内部计算缓存:useMemo 用对地方

useMemo 适合缓存“昂贵且依赖稳定”的计算结果。

const filteredList = useMemo( () => largeList.filter(item => item.keyword.includes(keyword)), [largeList, keyword] );

但 useMemo 不是免费的。它要保存上一次的依赖数组和计算结果,每次渲染都要浅比较依赖数组。如果一个计算只需跑 1ms,而它所在的组件每秒渲染几十次,用不用 memo 差别不大,用了还可能因为维护依赖数组而引入 bug。

我的经验是:当你在 profiler 里明确看到这个计算耗时占大头时,再上 useMemo;或者计算量达到数百条以上且每次渲染都跑时,用它才比较值。另外,useMemo 也常用于保持引用稳定:

const style = useMemo(() => ({ color: 'red' }), []);

这种用法更多是配合 memo 使用,让子组件能通过浅比较跳过更新。

5. 从同步到优先级:框架调度机制与并发模式

前面谈的是减少渲染次数和单次渲染成本,这一章谈框架的调度器如何决定“什么时候渲染、以什么优先级渲染”。

5.1 批处理:一次事件里的多次更新只渲染一次

现代框架会把一次事件里的多个状态更新收集起来,等事件结束再统一渲染。React 18 的自动批处理和 Vue 的批量更新思路一样,这能避免中间状态导致的页面闪烁,也能避免每次 setState 都渲染一次。

Vue 中对应的机制是异步更新队列:你在同一段代码里改了三个 ref 的值,组件不会马上渲染,而是在微任务里等所有变更收集完毕,再执行一次渲染。所以如果你在修改数据后立刻读取 DOM,拿到的可能是旧值。想拿新值,要用 nextTick:

count.value++; await nextTick(); console.log(dom.textContent); // 更新后的值

理解这个机制,能避免写出一堆“改了状态又马上查 DOM 却拿到旧值”的诡异 bug。

5.2 可中断更新:把非紧急渲染交给后台

在 React 18 的并发渲染机制出现之前,React 的渲染是同步的:开始渲染后,主线程必须一路执行到渲染完成,才能处理用户输入。如果一次渲染耗时 50ms,输入延迟 50ms 是跑不掉的。React 18 引入了并发渲染,把渲染拆成可中断的时间片,让高优先级的输入事件可以插队。

现在处理“输入框即时响应 + 列表大数据量过滤”这类场景,可以这样:

const [isPending, startTransition] = useTransition(); const [keyword, setKeyword] = useState(''); const [list, setList] = useState([]); function handleChange(value) { setKeyword(value); // 紧急更新:输入框本身要立刻响应 startTransition(() => { setList(generateLargeList(value)); // 非紧急更新:可以慢慢算 }); }

用户在输入时,输入框保持跟手,列表的更新在后台进行,等结果算好了再一次性渲染出来。isPending可以用来显示 loading 提示,避免用户看到旧列表误认为没生效。

useDeferredValue也是同一个思路。它让你把某个值的“滞后版本”用于耗时渲染,框架会在空闲时更新这个滞后值:

const deferredKeyword = useDeferredValue(keyword); const filteredList = useMemo( () => largeData.filter(item => item.name.includes(deferredKeyword)), [deferredKeyword] );

这本质上是在“把同步工作延迟到后台帧”,体感上类似防抖,但没有固定延迟时长。

5.3 交给框架前,你需要想清楚的事

调度机制给了你“来得及算”的窗口,但不要让该保留的紧急更新变得迟钝。如果所有更新都包一层 transition,输入框的内容可能也会滞后,第一印象会很差。紧急与否的判断标准很简单:用户正在交互的那部分必须是紧急的,用户看不到的后台结果可以降级。

另外,调度不能替代业务层的无效渲染优化。如果 list 本来就不是该更新的组件,你就算用 startTransition 也只能把它从“同步卡顿”变成“异步卡顿”,整体体验改善,但 CPU 浪费依然存在。好的做法是先用 memo 剪掉无效更新,再用 transition 处理之后仍显昂贵的必要更新。

6. 项目里的组件更新排障实录与常见问题速查

最后来点实战经验。这些都是我在真实项目里反复遇到和验证过的问题。

6.1 状态更新了,界面却纹丝不动的三个坑

坑一是直接改对象属性。很多人为了省事直接改 state 里的某个字段,我见过最典型的写法是user.age = 20之后发现页面没有变化,因为引用没变,框架不认为 state 变了。统一用展开、不可变更新函数或状态管理工具解决。

坑二是“组件定义在组件里面”。如果你在某个组件内部定义子组件,父组件每次渲染时,这个子组件的类型都是新的,框架会把它当成一个全新组件,卸载旧实例、挂载新实例。这不仅白跑一轮更新,还会让子组件内部的 state 丢失。

function Parent() { // 千万别这样写 function Child() { const [v, setV] = useState(0); return <div>{v}</div>; } return <Child />; }

正确做法是把Child移到Parent外部。

坑三是响应式数据新增属性没有走框架可观察的通道。这个问题在 Vue 2 时代是经典大坑,在 Vue 3 里虽然用 Proxy 解决了,但新手依然会犯“忘了解包 ref”这类小失误。排查思路都一样:检查你对数据的修改方式,是否进入了框架可观察的通道。

6.2 输入卡顿与长列表卡顿的定位思路

遇到卡顿,先不急着优化,先定位。我用 React 的 DevTools Profiler 比较多,它能告诉我每次渲染里哪个组件耗时最久、哪个组件不该渲染却渲染了。

定位思路可以按这个顺序来:

  1. 录制一次交互,看哪些组件重新渲染了。如果我只是改了一个 keyword,结果列表里几千行 Row 全部重渲染,问题就是更新范围过大。
  2. 再看每个组件 render 的耗时。如果单行耗时不长但行数多,问题在数量,考虑虚拟滚动;如果单行 render 就要几十毫秒,问题在行内计算,先在行内做缓存或拆分。
  3. 优化之后重新录制,对比渲染耗时和重渲染数量,用数据说话。

这一套思路在 Vue 里也通用:打开组件树,观察更新时哪些组件被标记为更新;用性能时间线看每次更新的耗时占比。

6.3 高频动作触发的更新风暴:防抖与过渡

滚动、resize、拖拽这类高频事件如果直接 setState,等于每秒触发几十次渲染,浏览器很难跟上。这类场景我常用的组合是:紧急部分直接更新,比如滑块的位置;非紧急部分用防抖或 transition 延迟,比如基于位置计算的结果。

一个容易踩的坑是防抖的回调里 setState 会带来界面延迟。如果用户停止输入后 300ms 才看到结果,这个延迟是可接受的;但如果业务要求“每输入一个字符立即联想”,那就不要用防抖,改用 useTransition 或 useDeferredValue,让输入跟手、结果异步算。

6.4 常见问题速查表

现象可能原因解决方向
状态改了,页面没变直接改对象属性,引用未变用展开或不可变更新
只改一个 count,整棵树都重渲染父组件更新,默认子树全部重跑拆组件,配合 memo
memo 没生效传入 props 的对象或函数每次新建useCallback / useMemo 稳定引用
列表插入一条,所有行重挂载key 用了 index换成稳定业务 id
输入卡顿,尤其是搜索每次渲染执行大列表过滤拆分组件、useMemo、useDeferredValue
改了数据,马上读 DOM 还是旧值框架异步批量更新Vue 用 nextTick;React 按需 flushSync
数据变化触发无限请求或渲染render 里 setState 或 effect 依赖不稳定避免 render 中更新,梳理 effect 依赖

关于速查表里最后一行,我想补充一句:如果你在 React 里发现某个请求在渲染后立刻又触发了一次更新,很可能是useEffect的依赖数组里放了每次渲染都新建的对象或函数,导致执行完 effect 又触发 setState,setState 又触发渲染,形成循环。处理办法是把依赖项稳定下来,或者在 effect 里加条件判断。

把上面这些串起来,优化组件更新的完整动作其实只有三件事:减少无意义的更新触发,缩小更新影响的组件范围,降低一次渲染本身的成本。我在实际项目里有过一次比较极端的教训:一个报表页面,我一开始把所有组件都包了 memo,useCallback 满天飞,结果页面反而更卡了。后来用 profiler 一测,发现最耗时的并不是某个组件渲染,而是我在父组件里把几千条数据重复做了一次聚合计算。把那个计算移到 useMemo,同时撤回一半无用的 memo 之后,页面立即丝滑了。

从那之后我给自己定了个规矩:优化前先看数据,让 profiler 告诉你瓶颈在哪,而不是凭感觉猜。组件更新机制本身不复杂,难的是分清“框架帮你处理了哪一段”和“哪一段需要由你来控制”。把这个边界想清楚,绝大多数组件更新相关的坑都不会再踩。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 13:40:01

核心期刊投稿全攻略:查重、期刊匹配与学术表达优化

1. 科研人投稿核心期刊的真实困境与破局思路我做了这么多年科研支持工作&#xff0c;听过的抱怨比祝福多得多。最常听到的一句话就是&#xff1a;"明明我的实验数据真实、逻辑也通顺&#xff0c;怎么投出去就被拒&#xff1f;" 这背后往往不是学术水平问题&#xff0…

作者头像 李华
网站建设 2026/10/18 13:39:41

比亚迪汉EV旗舰版上市:续航破千公里,起售价约35000美元

比亚迪最新旗舰电动车型"汉L"&#xff08;海外称Great Han&#xff09;正式在中国市场上市&#xff0c;起售价约为35000美元&#xff0c;CLTC续航里程超过1000公里&#xff08;621英里&#xff09;。比亚迪汉L电动车售价与续航在中国市场&#xff0c;这款车型被称为&…

作者头像 李华
网站建设 2026/10/18 13:38:47

传送网网管系统NCE-T部署运维指南:从选型到告警管理

简介&#xff1a;《NCE-T网管操作系统概述》是一份面向网络运维工程师、通信项目交付人员及华为网管初学者的技术资料&#xff0c;重点回答“NCE-T是什么、与U2000有何区别、如何部署”等核心问题。文档基于华为官方培训材料&#xff0c;以图文方式对比了NCE-T与U2000的架构差异…

作者头像 李华
网站建设 2026/10/18 13:38:14

逻辑严谨吗?8款AI写作辅助平台榜单,毕业季救星!

四大测评维度解析&#xff1a; 学术专业性&#xff1a;工具是否理解学术规范&#xff0c;输出内容是否符合论文逻辑与深度要求。文献支撑能力&#xff1a;能否引用真实、权威的参考文献&#xff0c;避免"空中楼阁"式生成。查重合规性&#xff1a;生成内容的原创性如何…

作者头像 李华
网站建设 2026/10/18 13:36:53

本地大模型量化指南:GGUF、GPTQ、AWQ原理拆解与选型实操

前几天有个朋友问我&#xff0c;同样是发模型&#xff0c;别人分享的文件后缀有的是 .gguf&#xff0c;有的是 .safetensors&#xff0c;名字里还分什么 GPTQ、AWQ&#xff0c;到底该下载哪个&#xff1f;这问题几乎是每个玩本地大模型的人都会遇到的第一道坎。模型量化&#x…

作者头像 李华
网站建设 2026/10/18 13:33:56

基于联邦学习与Streamlit的高校成绩预测可视化平台

简介&#xff1a;一套基于联邦学习的高校学生成绩预测Python项目&#xff0c;采用Streamlit搭建可视化平台&#xff0c;适合作为毕业设计、课程设计或科研训练的参考。面向计算机、人工智能、数据科学等相关专业的初学者与进阶者&#xff0c;也适合需要完成课程作业或毕业设计的…

作者头像 李华