想象你经营着一家奶茶店。TF.js 模型就像后厨里那位手艺精湛却只懂做茶的老师傅,React 和 Vue 组件则是店里的菜单屏、取餐屏和收银台。如果没有点单柜台,顾客会直接冲进后厨对师傅喊“我要芋泥奶茶”,下一位又喊“我也要芋泥奶茶”——师傅被迫把同一杯奶茶做两遍,前台也不知道进度,打烊时杯子和原料散落一地。本文要讲的“TF.js 与 React/Vue 集成方案”,就是给师傅配上一套标准的点单柜台:Hook 封装负责接单,推理去重把相同订单合成一杯,响应式更新让取餐屏自动翻号,卸载回收在打烊后把一切收拾干净。读完你会发现,把 AI 推理优雅嵌入组件树,就像开一家有条不紊的奶茶店一样简单。

岗位关联

掌握这套集成模式,你能在前端项目中标准化接入 AI 推理,让模型、组件与渲染性能和谐共处,成为团队里既懂模型又懂组件架构的 AI 应用工程师。

目录
  1. 可视化演示
  2. 集成架构总览
  3. 模型生命周期
  4. React集成模式
  5. Vue集成模式
  6. 性能优化策略
  7. 状态同步机制
  8. 最佳实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Hook封装、推理去重、响应式更新与卸载回收四大集成模式,让AI推理能力优雅嵌入组件树,含流动波形背景和发光动画。

本节配有交互动效(进入视口后自动加载,约 3KB)。

集成架构总览

想象你经营着一家奶茶店。TF.js 模型是后厨那位只懂做茶的老师傅,React 和 Vue 组件则是店里的菜单屏与取餐屏。没有点单柜台时,顾客直接冲进后厨点单,同样的芋泥奶茶被人重复喊了好几遍,师傅白做几杯,前台也说不清哪杯做到哪一步。混乱的根源,是缺少一层“接单”的中间环节。

在代码世界里,这个中间环节就是集成层。它通常以“Hook 封装”的面貌出现——React 里是自定义 Hook(useTensorFlow),Vue 3 里是组合式函数(useTensorFlow)。组件不直接触碰 tf.Model 对象,只向 Hook 提交请求、接收状态,一切加载与预测细节都被挡在柜台后面。

集成层要做四件事:推理去重,把相同入参的请求合并为一次模型计算;响应式更新,把预测结果安全写回组件状态;卸载回收,在组件销毁时清理订阅、缓存和模型引用。四件事配合得好不好,可以用去重命中率直观衡量:

有了这层柜台,顾客不必认识师傅,师傅也不必应付每个顾客。集成层把“接单—合并—制作—通知—收台”变成一条标准流水线,奶茶店从此有条不紊。

Hdedup=ChitChit+Cmiss\displaystyle H_{\text{dedup}} = \frac{C_{\text{hit}}}{C_{\text{hit}} + C_{\text{miss}}}
去重命中率越高,意味着越多请求复用了已有计算结果,AI 算力浪费越少,页面越流畅。
  1. C_hit(命中次数)入参与缓存中已有的请求完全相同、无需重新计算的请求数量,就像“再来一杯一模一样的芋泥奶茶”直接端走。
    ChitC_{\text{hit}}
  2. C_miss(未命中次数)入参第一次出现、必须真正调用模型推理的请求数量,相当于后厨第一次做这款新品。
    CmissC_{\text{miss}}
  3. H_dedup(去重命中率)命中请求占总请求的比例,反映合并订单省下的算力占比,越接近 1 说明柜台越聪明。
    Hdedup=ChitChit+CmissH_{\text{dedup}} = \frac{C_{\text{hit}}}{C_{\text{hit}} + C_{\text{miss}}}

模型生命周期

奶茶店的灵魂是那位常驻师傅。开店前他要经历招聘、学习和试做,这就是“模型加载”;师傅只有一位、全天在岗,这就是“单例缓存”;门口挂着“准备中/营业中”的灯牌,就是“加载状态”;打烊后师傅交还工牌、清理案台,则是“卸载释放”。

在前端代码里,loadLayersModel(modelUrl) 从 URL 加载模型文件,返回一个 tf.Model 对象。这一步涉及下载权重、解析拓扑、初始化后端,耗时可达数秒,所以绝不能放在每次渲染或每次推理时执行。加载状态需要单独用一个布尔值维护,加载完成后再广播给所有等待的组件。

正确做法是模块级单例:第一次调用时加载一次,之后所有组件共享同一个实例,配合事件或状态位广播就绪消息。加载总耗时由三段构成:

明白加载为什么贵,就明白为什么要让师傅常驻:只招一次工,只培训一次,全店共享一个人。模型单例缓存正是同理——用一次下载成本,换全应用零重复加载。

Tload=Tfetch+Tparse+Twarmup\displaystyle T_{\text{load}} = T_{\text{fetch}} + T_{\text{parse}} + T_{\text{warmup}}
模型加载总耗时等于下载模型文件、解析模型结构、完成首次预热三段时间之和,理解了它就能明白为何要用单例缓存避免反复加载。
  1. T_fetch(下载耗时)从 CDN 或服务器把模型与权重文件拉取到浏览器的时间,网络差时会占大头。
    TfetchT_{\text{fetch}}
  2. T_parse(解析耗时)将 JSON 拓扑与二进制权重解析为内存张量的时间,模型越大越慢。
    TparseT_{\text{parse}}
  3. T_warmup(预热耗时)首次执行时编译着色器、初始化 WebGL 后端、跑通一次空推理的时间,只为让后续调用走快车道。
    TwarmupT_{\text{warmup}}

React集成模式

React 组件店的格局是“散座加自助屏”,每个组件各自为政。要让它们都喝上 AI 做的奶茶,最优雅的方式是设一个标准柜台——自定义 Hook。任何组件只需调用 useTensorFlow(modelUrl, input),就能点单取货,完全不用关心后厨细节。

自定义 Hook 把模型加载、推理、状态更新、卸载清理封装进一个函数。useEffect 是 React 提供的“生命周期指定时机回调”,负责在组件挂载时加载模型(开业上岗)、卸载时释放引用(打烊收工);useRef 保存模型实例的稳定引用,避免重渲染导致对象丢失——就像柜台上的固定工位,人换位子不换。

推理请求通过第二个 useEffect 监听入参变化而触发。此时一个关键陷阱是“陈旧覆盖”——快速切换输入时,旧请求的结果若后到会覆盖新结果,需要在 Hook 内用自增序号丢弃旧结果。无论怎么优化,推理都会占用主线程时间,直接影响帧率:

柜台虽小,五脏俱全:useEffect 是上班打卡,useRef 是固定工位,序号机制是排队叫号,卸载 return 是关门大扫除。React 组件遵守这套章程,AI 推理就能乖乖地“上桌服务”而不打翻整个店面。

FPSeff=1000tpaint‾+tinfer‾\displaystyle \text{FPS}_{\text{eff}} = \frac{1000}{\overline{t_{\text{paint}}} + \overline{t_{\text{infer}}}}
有效帧率由绘制一帧的平均耗时与主线程推理的平均耗时共同决定,推理越重帧率越低,这也是需要做去重和 Web Worker 隔离的原因。
  1. t_paint(绘制均时)React 渲染、样式计算与浏览器绘制一帧的平均时间,相当于前台端茶送水的速度。
    tpaint‾\overline{t_{\text{paint}}}
  2. t_infer(推理均时)每次推理阻塞主线程的时间,若不做去重会直接叠加进帧率分母,好比后厨做奶茶时堵住了传菜通道。
    tinfer‾\overline{t_{\text{infer}}}
  3. FPS_eff(有效帧率)每秒实际能刷新的帧数,数值越低页面越卡顿,顾客体验越差。
    FPSeff=1000tpaint‾+tinfer‾\text{FPS}_{\text{eff}} = \frac{1000}{\overline{t_{\text{paint}}} + \overline{t_{\text{infer}}}}

Vue集成模式

Vue 的店面是另一种风格:展示柜加响应式橱窗。店主习惯把一切都交给“响应式”打理——数据一变,橱窗自动更新。要让 AI 推理融入这套体系,同样需要一个组合式柜口:组合式函数 useTensorFlow。

在 Vue 3 里,onMounted 在组件挂载时加载模型,ref 将推理结果变成响应式状态,watch 监听输入字段变化并自动触发推理,onBeforeUnmount 在卸载前释放模型。相比 React 需要手动管理渲染副作用,Vue 的响应式系统会自动批处理状态变更,UI 更新更省心。

但要注意,watch 里直接调用 model.predict 返回的是 Promise,结果回传是异步的。Vue 虽然自动更新视图,却不会替你丢弃过期的异步结果——陈旧覆盖问题依然要靠请求序号解决。批量更新的心智模型可以表达为:

柜台仍然是那个柜台,只是 Vue 的橱窗会自动换新——你只管把奶茶放上货架,展示逻辑交给响应式系统。而序列号机制依然是你手里的“最新款”标签,防止过期的奶茶混进橱窗。

Vflush=⋃i=1nΔvi\displaystyle V_{\text{flush}} = \bigcup_{i=1}^{n} \Delta v_i
一次事件循环 tick 内,Vue 会把 n 个响应式状态变更(结果、loading、错误提示)合并成一次视图刷新,避免每改一个状态就重渲染一次。
  1. Δv_i(单个状态变更)推理结果、加载标志、错误信息各自算作一次响应式赋值,如同橱窗里同时换掉标签、灯光和告示。
    Δvi\Delta v_i
  2. n(变更数量)同一轮异步回调里累积的响应式赋值个数,越多越需要合并处理。
    nn
  3. V_flush(一次刷新视图)Vue 在下一个 tick 统一应用所有变更,最终只操作一次 DOM,顾客只看到橱窗“唰”地整体焕新。
    Vflush=⋃i=1nΔviV_{\text{flush}} = \bigcup_{i=1}^{n} \Delta v_i

性能优化策略

奶茶店火爆时,后厨是最大的瓶颈。精明的老板会让前台“攒单”——顾客连珠炮似的点单,先不急着喊给师傅,等安静下来统一报单;如果两份订单完全一样,就直接合并做一杯。这就是“防抖加去重”的管理艺术,也是性能优化策略的核心思路。

在前端代码里,防抖(debounce)让连续变化只触发一次推理,比如搜索框 300ms 不输入才调用模型;入参去重用输入指纹做缓存,相同数据直接返回上次结果;批处理则把多个入参堆叠成一批张量,一次 predict 产出多个结果。更彻底的做法是把推理搬进 Web Worker——相当于在后厨隔出一间独立的备料间,不干扰前台营业。

Worker 隔离了主线程,模型推理不再阻塞渲染,但传输数据与序列化有额外成本,更适合大模型、高耗时任务。防抖能把推理频率从“每次按键”降为“每个安静窗口一次”:

前台会攒单,后厨有备料间,一样的单一起做,忙碌时也不崩盘——性能优化的本质,就是把有限的算力用在刀刃上。用户感受不到“师傅太忙”,只看到奶茶一杯接一杯地出。

Npredict=⌈TtypingΔtdebounce⌉\displaystyle N_{\text{predict}} = \left\lceil \frac{T_{\text{typing}}}{\Delta t_{\text{debounce}}} \right\rceil
用户连续输入的时长除以防抖等待窗口再向上取整,就是实际触发的推理次数;窗口越大,次数越少。
  1. T_typing(输入持续时长)从用户开始输入到停止输入的连续时间段,相当于顾客连续点单的嘴不停期。
    TtypingT_{\text{typing}}
  2. Δt_debounce(防抖窗口)两次触发之间必须保持安静的等待时间,常用 200-500ms,相当于柜台“等我听清再报”的停顿。
    Δtdebounce\Delta t_{\text{debounce}}
  3. N_predict(触发次数)每个窗口最多触发一次推理,向上取整保证覆盖整个输入期,把高频噪声收敛成低频动作。
    Npredict=⌈TtypingΔtdebounce⌉N_{\text{predict}} = \left\lceil \frac{T_{\text{typing}}}{\Delta t_{\text{debounce}}} \right\rceil

状态同步机制

取餐屏是奶茶店与顾客之间的信任桥梁。顾客扫码下单后,屏幕上先是“制作中”,再变成“请取餐 A12”,偶尔还会出现“原料不足,请联系店员”。这三类信息,对应前端状态里的 loading、result 与 error,也就是“状态同步”要管理的三类数据。

技术上的“状态同步”指的是:异步推理结果如何安全地流回组件状态并触发视图更新。它有三个要点:结果回传——用 setState 或 ref 赋值;loading 切换——推理开始置为 true、结束置为 false;错误处理——捕获 predict 异常并展示友好降级提示。

最难的是“陈旧结果”问题:用户快速连点,旧输入的结果晚到。集成层必须用请求序号校验,只允许最新序号的结果更新状态:

取餐屏的价值在于“只显示最新状态”——过期的取餐号永远不该出现在屏幕上。状态同步做得好,用户看到的每一个数字都代表当下真实的情况,信任感自然就起来了。

stale(r)={1,if tr<tlatest0,otherwise\text{stale}(r) = \begin{cases} 1, & \text{if } t_r < t_{\text{latest}} \\ 0, & \text{otherwise} \end{cases}
当结果对应的请求序号小于当前最新序号时,判定为陈旧结果并丢弃;只有最新结果才允许更新 UI。
  1. t_r(结果请求序号)每条推理请求被分配的自增编号,结果回传时携带该编号,就像每杯奶茶上的取餐号。
    trt_r
  2. t_latest(最新序号)当前已发起的最新请求编号,组件内部用 ref 或普通计数器维护,相当于取餐屏现在叫到的最高号。
    tlatestt_{\text{latest}}
  3. stale 判定t_r 小于最新序号即 stale=1,直接丢弃不触发更新;否则 stale=0,正常回写状态,保证屏幕永远只显示最新取餐号。
    stale(r)={1,if tr<tlatest0,otherwise\text{stale}(r) = \begin{cases} 1, & \text{if } t_r < t_{\text{latest}} \\ 0, & \text{otherwise} \end{cases}

最佳实践

一家成熟的奶茶店,不仅要做得出好茶,还要经得起长期运营:灭火器要常备(错误边界)、打烊必须清场(防泄漏)、后厨要隔音(渲染隔离)、多位师傅要分工明确(多模型管理)。这些就是集成方案落地时的最佳实践。

错误边界(React ErrorBoundary / Vue errorCaptured)捕获加载或推理异常并降级展示;防泄漏指组件卸载时取消订阅、清除定时器、释放模型缓存;渲染隔离指推理不阻塞主线程绘制;多模型管理则用统一的 gateway 容器按需懒加载多个模型,避免首屏一次性下载所有权重。

这些实践最终都指向同一件事:内存和显存是有上限的。模型与组件引用若不释放,累积起来会拖垮整个应用。一次彻底回收释放的内存可以写成求和式:

开店容易守店难。最佳实践的每一条,都是为了让 AI 推理像店里的老师傅一样:招之能战,战完能歇,不留烂摊子。遵守它们,你的前端 AI 应用才能长期稳定地运转。

Mreleased=Mmodel+∑k=1mMhook(k)\displaystyle M_{\text{released}} = M_{\text{model}} + \sum_{k=1}^{m} M_{\text{hook}}^{(k)}
一次彻底回收释放的内存,等于模型本体占用加上所有已卸载组件残留的 Hook 资源之和。
  1. M_model(模型内存)调用 model.dispose() 可释放的张量与权重缓存,相当于师傅离职后清空他的专属工具台。
    MmodelM_{\text{model}}
  2. M_hook(组件残留)第 k 个已卸载组件持有的订阅、定时器与请求序号引用,是细碎却累积惊人的“杯底残渣”。
    Mhook(k)M_{\text{hook}}^{(k)}
  3. m(残留组件数)未正确清理的组件数量,越多回收收益越大,也说明清场工作越需要认真执行。
    mm

AI 应用场景

React 电商商品图片自动打标

阶段:推理集成与前端部署

解决的问题:传统做法把每张商品图上传后端做分类,带宽和延迟高,同一商品反复出现在推荐流中造成重复推理。集成层在商品卡片组件内直接分类,入参指纹去重后相同图片只推理一次,滚动列表时响应式更新标签。

具体案例:某服装电商后台,商品列表页每张卡片调用 useTensorFlow 分类模型识别“T恤/卫衣/裤装”。用户快速滑动时,卡片入参被去重缓存,滑过的商品再次出现时瞬间从缓存拿结果;组件卸载时自动 dispose 模型引用,长时间浏览不卡顿。

Vue 报表系统的实时异常校验

阶段:推理集成与响应式更新

解决的问题:财务人员录入报表时,传统方案每次输入都请求后端校验,网络延迟明显。Vue 组合式函数 watch 表单字段,防抖 500ms 后调用轻量异常检测模型,响应式结果即时提示。

具体案例:某企业报销系统,费用项输入框失焦后触发模型判断“单价是否偏离历史均值、科目是否异常”,结果通过 ref 回写并在行内红字提示;onBeforeUnmount 中自动清理定时器与模型引用,批量录入时既不阻塞界面也不泄漏内存。

Python 演示

python96 行
"""模拟 TF.js 与 React/Vue 集成网关:推理去重 + 响应式广播 + 生命周期回收"""import hashlibimport time  def tiny_model(payload):    """模拟一次神经网络推理:对入参求和"""    time.sleep(0.05)    return sum(payload)  class ModelGateway:    """模拟浏览器中的 TF.js 模型单例"""     def __init__(self, predict_fn):        self._predict_fn = predict_fn        self._cache = {}        self._subscribers = []        self._seq = 0        self.gc_count = 0     def _key(self, payload):        return hashlib.md5(repr(payload).encode("utf-8")).hexdigest()     def infer(self, payload, caller_id):        self._seq += 1        seq = self._seq        key = self._key(payload)        start = time.time()        if key in self._cache:            print("[去重命中] %s 复用缓存 seq=%d" % (caller_id, seq))            return self._cache[key]        result = self._predict_fn(payload)        self._cache[key] = result        cost = (time.time() - start) * 1000        print("[推理完成] %s 耗时 %.1fms seq=%d" % (caller_id, cost, seq))        self._notify(caller_id, result, seq)        return result     def _notify(self, caller_id, result, seq):        for cb in self._subscribers:            cb(caller_id, result, seq)     def subscribe(self, callback):        self._subscribers.append(callback)     def release(self):        self._cache.clear()        self._subscribers.clear()        self.gc_count += 1        print("[资源释放] 第 %d 次回收,缓存与订阅已清空" % self.gc_count)  class ReactiveBoard:    """模拟前端组件:只接受最新序号的结果并刷新『UI』"""     def __init__(self, cid, gateway):        self.cid = cid        self.last_seq = 0        self.ui_text = "待推理"        gateway.subscribe(self.on_result)     def on_result(self, caller_id, result, seq):        if caller_id != self.cid:            return        if seq < self.last_seq:            print("[陈旧丢弃] %s 忽略 seq=%d" % (self.cid, seq))            return        self.last_seq = seq        self.ui_text = "结果=%s" % result        print("[UI 更新] %s -> %s" % (self.cid, self.ui_text))  def main():    gateway = ModelGateway(tiny_model)    board_a = ReactiveBoard("A", gateway)    board_b = ReactiveBoard("B", gateway)    board_c = ReactiveBoard("C", gateway)     payload_a = [1, 2, 3, 4, 5]    payload_b = [1, 2, 3, 4, 5]   # 与 A 相同入参    payload_c = [2, 4, 6, 8, 10]  # 新入参     gateway.infer(payload_a, "A")   # 首次推理    gateway.infer(payload_b, "B")   # 命中缓存,推理去重    gateway.infer(payload_c, "C")   # 新推理,广播给组件 C    gateway.release()               # 模拟组件卸载,回收资源     assert board_a.ui_text == "结果=15"    assert board_b.ui_text == "结果=15"    assert board_c.ui_text == "结果=30"    print("全部组件 UI 状态正确,集成网关工作正常")  if __name__ == "__main__":    main()
目的:用 Python 抽象模拟前端集成网关,演示“推理去重、响应式广播、陈旧结果丢弃、卸载回收”四大机制如何协同工作。
  1. class ModelGateway相当于柜台对象,集中管理模型推理、缓存与订阅,组件不再直接触碰“模型师傅”。
  2. self._notify(caller_id, result, seq)推理完成后广播结果,对应响应式更新:所有订阅组件都能收到通知。

常见误区

误区“我把模型挂到了模块级变量上,所有组件共享,反正不用重复加载,组件卸载时应该也不用清理吧?”

正解单例模型解决的只是“重复加载”问题,组件自己持有的订阅、定时器、请求序号引用仍然需要清理。就像奶茶店师傅只有一个,但每个顾客用过的杯子还得收走。组件卸载后,若回调还在引用 setState 或 ref,轻则内存泄漏,重则触发“组件已卸载但更新状态”的告警。正确做法是:模型本身靠单例复用,组件的订阅和回调在卸载钩子里逐一解绑。

为何常见:内存泄漏在浏览器里是隐性的,不像报错那样立刻可见;且单例模型确实不会因一个组件卸载而消失,容易让人误以为“什么都不用做”。

误区“Vue 的响应式这么聪明,我在 watch 里直接 `model.predict(input)`,input 一变它就会自动推理更新界面吧?”

正解响应式保证的是“数据变了回调会执行”,但不保证“回调执行的频率合理”。连续输入时 watch 会疯狂触发推理,阻塞主线程,页面直接卡成幻灯片。正确的做法是:防抖收敛触发频率、入参去重合并相同请求、必要时丢进 Web Worker,再把结果通过 ref 回写。柜台要“攒单”,不能顾客说一个字就冲进后厨一次。

为何常见:小 demo 中输入次数少,看不出推理频率问题;响应式系统的“自动化”容易让人误以为它也自动优化了性能。

误区“我把模型实例用 `ref` 包起来,或者放进 `useState`,这样模型加载好之后视图就能自动更新了?”

正解tf.Model 是重型、非序列化、内部持有 WebGL 纹理的对象,放进响应式容器既不会被 Vue 深度代理,也不需要被 React 比较。它们只应该被普通变量或模块级变量引用。模型加载完成要更新 UI,正确做法是单独维护一个 loading ref/state,加载完成后把它置为 false,视图自然刷新。

为何常见:新手习惯“一切数据都放 state”,但框架的响应式设计目标是一般业务数据,不是模型这类底层资源句柄。

误区“我的模型很小,直接在主线程跑,应该不至于卡顿吧?”

正解推理是否卡顿不只取决于模型大小,还取决于输入尺寸、并发请求频率、设备 GPU 能力。小模型高频调用同样会持续抢占主线程,导致滚动掉帧;把推理放进 Web Worker 是低成本的心理保障,且 TF.js 官方支持 Worker 推理。本地开发用高端设备、低频场景测不出差别,生产环境的用户设备参差不齐,主线程阻塞的代价会被无限放大。

为何常见:开发机的性能掩盖了推理开销,加上“能用就行”的思维惯性,让主线程推理看起来毫无问题。

面试题

Q1. React 应用中,封装 TF.js 推理逻辑的推荐方式是?

单选题 · easy

  • A.封装成自定义Hook来调用← 正确答案
  • B.在render函数里直接调用
  • C.挂载到window全局复用
  • D.每次请求都重新下载模型

答案:A。自定义 Hook 把模型加载、推理、序号防陈旧与卸载清理收进一个函数,组件只需声明依赖即可接入;render 内直接调用会在每次渲染时重复推理;window 全局难以追踪生命周期与清理;每次重新下载模型浪费带宽,正是单例策略要避免的。

Q2. TF.js 模型加载完成后,应该把模型实例存放在哪里?

单选题 · easy

  • A.组件的state状态里面
  • B.模块级单例变量引用← 正确答案
  • C.每条请求新建一个实例
  • D.序列化后存到localStorage

答案:B。模块级单例让模型在第一个组件使用时加载一次,后续组件共享,配合加载状态广播;放进组件 state 不仅可能触发多余渲染,模型这类重型非序列化对象也不属于响应式数据;每次新建实例浪费带宽和内存;localStorage 只能存字符串,无法存放模型对象。

Q3. 两个组件发起相同入参的推理请求,集成层应如何处理?

单选题 · mid

  • A.并行执行两次独立推理
  • B.合并为一次推理并共享结果← 正确答案
  • C.丢弃第二个组件的请求
  • D.两个组件互不影响各算各

答案:B。相同入参意味着模型输出必然相同,合并成一次推理、把结果广播给所有请求者是去重的核心收益,能显著提升并发场景的帧率;并行执行两次是最大的浪费;丢弃请求会让组件永远等不到结果;互不干扰等于放弃去重,违背集成层设计初衷。

Q4. 自定义 Hook 在组件卸载时,最关键的一步操作是什么?

单选题 · mid

  • A.清除定时器和取消订阅← 正确答案
  • B.立即触发一次模型推理
  • C.将模型存到全局缓存中
  • D.调用浏览器的强制刷新

答案:A。卸载时清理定时器、取消订阅并释放模型引用,能防止内存泄漏和“组件卸载后更新状态”的警告;卸载时再去推理或重载模型都是雪上加霜,收尾阶段的组件不该发起任何计算;强制刷新与卸载清理毫无关系。

Q5. Vue 3 组合式 API 中集成 TF.js 可以配合使用?

多选题 · mid

  • A.ref 存放推理结果状态← 正确答案
  • B.onMounted 挂载时加载模型← 正确答案
  • C.watch 监听输入自动推理← 正确答案
  • D.computed 直接执行模型推理

答案:ABC。ref 让推理结果获得响应式能力,onMounted 在最合适的时机加载模型,watch 监听输入变化自动触发推理,三者配合正是组合式集成三板斧;computed 是同步派生数据的地方,执行持续数百毫秒的模型推理会阻塞渲染,应该用 watch 或显式调用来完成。

Q6. 推理结果回传时出现“陈旧结果覆盖新结果”,应该如何解决?

单选题 · mid

  • A.用自增请求序号丢弃旧结果← 正确答案
  • B.强制所有推理变成同步调用
  • C.提高模型返回数据的速度
  • D.在全局变量中保存所有结果

答案:A。自增请求序号让每个结果携带身份,小于最新序号的结果直接丢弃——确保屏幕上永远是最新请求的答案,就像取餐屏只亮最新号;强制同步会锁死主线程;提高模型速度治标不治本;保存所有结果会让 UI 无所适从。

Q7. 关于 Web Worker 中运行 TF.js 模型,正确的说法是?

单选题 · hard

  • A.支持推理但需序列化传数据← 正确答案
  • B.Worker 中无法加载任何模型
  • C.WebGPU 后端在 Worker 内禁用
  • D.Worker 只能处理字符串拼接

答案:A。TF.js 官方支持在 Worker 中推理,把耗时计算移出主线程可避免卡顿,但入参和结果需要结构化序列化传输;Worker 完全可以加载模型并访问 WebGL/WebGPU 后端;“只能处理字符串拼接”是对 Worker 能力的严重误解。

Q8. “响应式更新”的集成要点通常包括哪些方面?

多选题 · hard

  • A.推理结果映射到组件状态← 正确答案
  • B.loading 状态自动切换← 正确答案
  • C.推理错误提示并降级处理← 正确答案
  • D.修改框架源码加速更新

答案:ABC。响应式更新的三要素是:结果进状态、loading 随请求生命周期切换、错误被捕获并降级展示;修改框架源码属于过度工程,响应式系统已经替你完成了视图同步,应用层只需要更新自己的状态即可。

Q9. Vue 组件卸载后不释放 TF.js 模型引用,最可能的结果是?

单选题 · easy

  • A.内存泄漏导致页面卡顿← 正确答案
  • B.模型推理精度变差
  • C.打包后的体积变大
  • D.组件的样式全部丢失

答案:A。模型与缓存不释放会持续占用内存,尤其 WebGL 纹理显存,累积后页面逐渐卡顿甚至崩溃;推理精度由权重决定,与引用是否释放无关;打包体积由构建和模型文件位置决定;样式丢失是 CSS 问题,与模型引用无关。

Q10. 关于模型加载的“单例缓存”策略,正确的理解包括?

多选题 · hard

  • A.多个组件共享同一个模型实例← 正确答案
  • B.模型文件只下载和解析一次← 正确答案
  • C.每次进入页面都重新下载权重
  • D.加载完成后统一广播就绪状态← 正确答案

答案:ABD。单例缓存意味着加载动作全局只发生一次,所有组件共享实例,加载完成后用状态广播通知订阅者,避免重复等待;每次进入页面重新下载权重会让首屏体验严重退化,违背单例缓存的初衷。