
摘要TaskPool 很适合把搜索、排序、统计这类耗时逻辑挪出去但它返回复杂对象以后页面不一定会跟着刷新。这个问题在中式美食这种列表页里很容易出现后台已经算出了“鱼香”相关菜谱页面也拿到了对象可卡片上的命中数量还是旧的。这次我不把它写成一个概念说明而是按开发时真正会碰到的流程拆开先复现“不刷新”再解释对象从子线程回来以后发生了什么最后给出可以跑通的写法。问题现场我在做中式美食搜索页时把候选菜谱的命中统计放到了后台任务里。原因很简单关键词可能会触发拼音、食材、分类、最近浏览几路数据一起算如果都放在 UI 线程里输入框会有明显卡顿。一开始我以为只要 TaskPool 返回一个对象然后赋值给页面状态就行。结果问题来了日志里能看到 TaskPool 已经返回新结果页面标题偶尔变了内部字段却不刷新点一下页面上的按钮以后某些字段才突然更新同一份数据再传回子线程处理时边界变得很乱不知道哪边负责改状态。这类问题不是“框架不刷新”而是状态边界没有分清楚。现象表面看起来真正要查的点TaskPool 有返回值后台逻辑没问题返回对象是否已经接入 UI 观察页面局部不刷新组件像卡住了字段变化有没有被状态系统观察到改字段后表现不稳定像是异步时序问题可观察对象有没有被传回子线程代码越来越绕到处都能改数据数据计算和 UI 状态接管没有分层先把几个概念说清楚官方文档里会同时出现Sendable、TaskPool、UIUtils.makeObserved。如果直接看名词很容易把它们混在一起用。我自己的判断方式比较简单名称我在项目里的理解放在哪里更合适Sendable这个对象可以安全地跨并发实例传递子线程返回的数据模型TaskPool把耗时计算放到后台跑搜索、排序、统计、规则计算UIUtils.makeObserved让普通对象接入 UI 状态观察回到 UI 线程以后Local当前页面持有并驱动刷新的状态页面组件内部所以这条链路不要写反。正确顺序应该是子线程只负责算出一个干净的结果对象结果对象用Sendable表示可以跨线程传递回到 UI 线程以后再用UIUtils.makeObserved接管页面只改自己持有的可观察对象如果还要传回子线程传普通快照不要把 UI 状态对象直接丢过去。案例一中式美食异步搜索结果为什么不刷新先看一个缩小后的场景。中式美食里有一个搜索框输入“鱼香”以后后台需要算出推荐结果和命中数量。这个过程不应该挡住输入框所以我把计算放进 TaskPool。下面这个模型代表后台返回的搜索结果。import{taskpool}fromkit.ArkTS;import{UIUtils}fromkit.ArkUI;SendableclassDemoRecipeResult{id:string;name:string;keyword:string;matchCount:number0;updatedAt:number0;constructor(id:string,name:string,keyword:string,matchCount:number,updatedAt:number){this.idid;this.namename;this.keywordkeyword;this.matchCountmatchCount;this.updatedAtupdatedAt;}}后台任务只做一件事根据关键词算结果不碰页面状态。ConcurrentfunctionbuildRecipeResult(keyword:string):DemoRecipeResult{constnormalizedkeyword.trim().length0?keyword.trim():家常菜;constcountnormalized.length*32;returnnewDemoRecipeResult(recipe-${normalized.length},${normalized}推荐结果,normalized,count,Date.now());}页面拿到结果以后关键点在这里不要觉得 TaskPool 返回的对象天然就能驱动 UI。它只是一个跨线程回来的结果对象。回到 UI 线程以后需要重新让它变成可观察数据。ComponentV2exportstruct CsdnMakeObservedSendableDemo{Localkeyword:string鱼香;Localresult:DemoRecipeResultUIUtils.makeObserved(newDemoRecipeResult(recipe-init,等待搜索,鱼香,0,Date.now()));Locallogs:string[][等待 TaskPool 返回数据];privateasyncrefreshFromTaskPool():Promisevoid{constrawResultawaittaskpool.execute(buildRecipeResult,this.keyword)asDemoRecipeResult;this.resultUIUtils.makeObserved(rawResult);this.logs[子线程返回${rawResult.name}回到 UI 线程后重新 makeObserved,...this.logs].slice(0,4);}}这段代码解决的是“结果已经回来了但页面不稳定刷新”的问题。它把边界固定住了TaskPool 只给数据页面自己接管状态。案例二后台统计卡片怎么避免状态边界乱掉第二个场景更通用不一定和菜谱有关。假设页面上有一个统计卡片后台负责算matchCount页面上还有一个按钮可以临时加一用来验证 UI 线程上的字段变化能不能立即刷新。privateaddOneMatchOnUiThread():void{this.result.matchCount1;this.result.updatedAtDate.now();this.logs[UI 线程直接改 matchCount${this.result.matchCount}页面应立即刷新,...this.logs].slice(0,4);}这个按钮的意义不是业务功能而是验证边界如果result已经被makeObserved接管matchCount 1后页面应该马上刷新如果页面没有刷新说明 UI 线程拿到的不是可观察对象如果又把这个可观察对象直接丢回 TaskPool就会让“数据计算”和“页面状态”混在一起后面很难查问题。我最后采用的规则是子线程和 UI 线程之间传普通结果对象UI 状态只留在页面这一侧。写法能不能用问题TaskPool 直接改页面对象不建议子线程和 UI 状态绑在一起问题很难定位TaskPool 返回普通对象页面直接用不稳定字段变化不一定被 UI 观察到TaskPool 返回SendableUI 线程再makeObserved推荐边界清楚刷新可验证UI 可观察对象再传回 TaskPool不建议页面状态和后台计算互相污染我怎么复现和验证这次我把 demo 放到了独立验证页里避免牵扯完整业务。验证步骤如下步骤操作应该看到什么1打开 demo 页面初始显示“等待搜索”2点“TaskPool 查询”结果变成“鱼香 推荐结果”3点“UI 线程 1”命中数量立即加一4切换关键词后再查询结果按新关键词重新生成5连续点击查询和加一日志能看出后台返回和 UI 修改是两条边界本地验证代码已经进项目并通过了assembleHap。这一步很重要因为这种文章如果只给伪代码读者照着写很容易踩到导入、装饰器、线程函数这些细节。为什么不把所有对象都 makeObserved这个问题我也想过。看起来最省事的做法是把项目里所有复杂对象都包一层可观察。这样页面想改什么都能刷新。但实际项目里这么做会有两个麻烦数据来源会变乱。你后面分不清对象是后台算出来的还是页面改出来的。并发边界会变模糊。TaskPool 负责计算页面负责渲染如果两边都拿着同一个状态对象改问题会变成时序问题。所以我更倾向于把对象分成两类。对象类型负责什么是否进入 UI 状态后台结果对象承载 TaskPool 算出的结果回到 UI 线程后再接管页面状态对象驱动组件刷新留在页面或 ViewModel传回后台的参数只表达计算输入用普通快照日志和错误信息帮助排查过程可以单独放页面状态这样写代码稍微多一点但排查问题会轻很多。什么时候该用这套写法不是所有场景都需要Sendable makeObserved。如果只是页面里的一个数字、一个开关、一个字符串用普通Local就够了。真正适合这套写法的是下面几类后台任务会返回复杂对象对象回到页面后还要继续改字段页面需要根据对象字段局部刷新这个对象不能只当一次性渲染数据后续还要做分页、缓存、搜索历史、错误重试。中式美食的搜索结果、推荐结果、离线草稿恢复结果都属于这类。它们不是简单展示一下就结束而是会继续被页面操作影响。以后怎么避免我现在给这类代码定了一个小规则。// 子线程只返回结果constrawResultawaittaskpool.execute(buildRecipeResult,keyword)asDemoRecipeResult;// UI 线程接管观察this.resultUIUtils.makeObserved(rawResult);// 再进子线程重新组装普通参数不把 UI 状态对象直接传回去constnextKeywordthis.result.keyword;这三行基本能挡住大部分混乱。再具体一点就是后台任务函数里不要读页面字段后台任务返回值尽量是干净的数据模型回到 UI 线程以后再决定怎么刷新页面页面上可点击、可编辑、会继续变化的数据用页面状态接住再次进入后台任务时只传必要参数不传整棵页面状态对象。小结Sendable和makeObserved不是一组“写上就完事”的装饰。它们解决的是两个不同问题一个管跨线程传递一个管 UI 观察。我这次踩到的点是把“后台返回了对象”和“页面能观察对象字段变化”混成了一件事。拆开以后就清楚了TaskPool 只负责把结果算出来UI 线程再决定怎么把结果变成页面状态。后面遇到异步搜索、推荐排序、离线草稿恢复这类逻辑我都会先问自己一句这个对象回来以后还会不会在页面上继续变化如果会就不要直接塞进组件里用先在 UI 线程把状态边界接好。