用户每生成或导出一张自制图,页面都会把它放进“最近生成”。这个列表看似只是数组头插,但实际要同时满足四个规则:只记录用户生成内容、当前项排在最前、同一 ID 不重复、最多保留 10 条。若分散成多次push、filter和slice,收藏页、作品页和导出流程很容易得到不同顺序。
拼豆制图把规则集中在savePatternRecord()中。本篇逐行验证现有循环的边界,说明为什么它最多得到 10 条而不是 11 条,并进一步处理异步持久化、重复对象版本和内置图收藏等容易被忽略的数据语义。
一、先把业务不变量写成一句话
最近记录的期望可以表达为:
next = [current] + old 中所有 id != current.id 的项 next 最多取前 10 条 只有 user-generated 开头的图案进入最近记录 当前图案无论是否自制,都可以加入收藏这里“最近记录”和“收藏”是两个集合,不能因为一次操作同时更新就混成同一规则。内置图可以收藏,但不应出现在“我的生成”;用户生成图可以既在最近列表又在收藏。
二、现有实现如何在一次循环中完成三件事
核心代码:
privatesavePatternRecord(pattern:Pattern):void{if(pattern.id.indexOf('user-generated')===0){constnextRecords:Pattern[]=[pattern];for(leti=0;i<this.generatedRecords.length;i++){if(this.generatedRecords[i].id!==pattern.id&&nextRecords.length<10){nextRecords.push(this.generatedRecords[i]);}}this.generatedRecords=nextRecords;}if(!this.isFavorite(pattern.id)){this.favoriteIds=this.favoriteIds.concat([pattern.id]);}}循环从[pattern]开始,因此当前项天然置顶;遇到相同 ID 就跳过,因此不会重复;只有nextRecords.length < 10才追加,因此最终长度不超过 10。最后一次从 9 追加到 10,后续项全部跳过。
三、为什么条件顺序不会产生第 11 条
以旧列表已有 10 条且当前项不在其中为例:
初始 next 长度:1 追加旧记录 1..9:长度依次到 10 旧记录第 10 条:next.length < 10 为 false,不追加 最终:10若当前项原本位于旧列表第 4 位:
初始放入新版 current 旧 1、2、3 依次追加 旧第 4 项同 ID,被跳过 继续追加后续项,直到总长 10因此它不仅去掉重复,还用传入的新对象替换旧对象。若同一 ID 的标题、统计或格子内容发生更新,置顶项会携带最新数据。
四、ID 前缀是来源协议,不应到处复制
当前来源判断为:
pattern.id.indexOf('user-generated')===0同一前缀还用于收藏列表中决定是否加入当前生成图。字符串协议简单,但若未来 ID 改成 UUID,这些判断会同时失效。更稳妥的模型是显式来源字段:
typePatternOrigin='built-in'|'user';interfacePattern{id:string;origin:PatternOrigin;// ...}保存逻辑变为pattern.origin === 'user'。业务 ID 只负责身份,来源字段负责分类,二者不再互相推断。
若暂时不能改模型,也应抽出isUserPattern(pattern),避免多个页面各写一遍前缀判断。
五、收藏去重与最近记录去重要共享同一身份
收藏只保存 ID:
privateisFavorite(id:string):boolean{returnthis.favoriteIds.indexOf(id)>=0;}if(!this.isFavorite(pattern.id)){this.favoriteIds=this.favoriteIds.concat([pattern.id]);}最近记录保存完整Pattern,但两者都用pattern.id判断身份。这样作品内容更新不会制造第二个收藏项,收藏页仍可通过 ID 找到最新版记录。
风险在于 ID 由Date.now()生成。如果极端情况下同一毫秒生成两个不同作品,它们会被视为同一项,第二个覆盖第一个。页面当前生成流程串行,概率很低,但数据层最好使用更强的唯一 ID 策略,例如时间戳加进程内递增序号。
privatestaticsequence:number=0;functionnextUserId():string{return`user-generated-${Date.now()}-${IdFactory.sequence++}`;}六、把数组规则抽成纯函数更容易回归
页面方法同时修改两份状态,不方便单独验证。可以提取:
functionupsertRecent(current:Pattern,oldRecords:Pattern[],limit:number):Pattern[]{if(limit<=0)return[];constnext:Pattern[]=[current];for(leti=0;i<oldRecords.length&&next.length<limit;i++){if(oldRecords[i].id!==current.id){next.push(oldRecords[i]);}}returnnext;}页面只负责来源判断和状态提交。纯函数参数显式包含limit,可以验证 0、1、10 等边界,也不会把“10”散落到界面代码。
这里不建议先filter再slice再展开,虽然写法短,但会创建多个中间数组。当前最多 10 条差异不大,显式单循环更容易看出何时停止。
七、列表限长不等于存储限长
当前generatedRecords是页面@State,应用重启后会恢复为空。未来接入持久化时,要决定存什么:
摘要:id、标题、尺寸、色数、创建时间、文件路径 完整图:4900 个 chartCells + 1225 个 previewCells + 色卡把 10 张完整图都塞进一个 Preferences JSON 会快速膨胀,也会让每次置顶都重写大量数据。更合理的是 Preferences 保存最近 ID 或摘要列表,完整图纸按 ID 写入独立 JSON 文件;删除第 11 条摘要时,再根据收藏和引用关系决定是否删除文件。
限长规则属于“最近视图”,不一定意味着用户作品只能存在 10 张。若产品要长期作品库,最近列表与作品仓库必须分离。
八、异步持久化要防止旧写覆盖新写
数组在内存中同步更新时,顺序确定。接入异步存储后,连续保存 A、B 可能出现:B 先写完,A 后写完,最终磁盘回到旧状态。
可在服务层串行写入:
privatewriteChain:Promise<void>=Promise.resolve();enqueueSave(snapshot:PatternSummary[]):Promise<void>{this.writeChain=this.writeChain.then(async()=>{awaitthis.repository.saveRecent(snapshot);});returnthis.writeChain;}或者每次写入带updatedAt/版本号,仓库拒绝旧版本覆盖新版本。页面不要一边先更新数组,一边由多个点击处理器各自读取旧快照再保存。
导出和生成都调用savePatternRecord(),集中入口已经为后续串行化创造了条件。
九、收藏列表合并还要防跨来源重复
favoritePatterns()先遍历用户记录,再检查当前生成图,最后遍历内置图。当前生成图只有在结果中不存在同 ID 时才追加:
if(this.generatedPattern.id.indexOf('user-generated')===0&&this.isFavorite(this.generatedPattern.id)&&!this.hasPatternInList(result,this.generatedPattern.id)){result.push(this.generatedPattern);}这一步说明“去重”不仅发生在保存时,也发生在不同数据源合并时。若未来从磁盘加载记录,应继续保证用户记录、当前编辑对象和内置仓库共享同一 ID 命名空间,或使用{origin,id}复合身份。
不要用对象引用去重。持久化反序列化后,同一业务图案会变成新对象,只有稳定 ID 才能跨生命周期识别。
十、用小数组覆盖所有边界
建议固定样本:
// 空旧列表 + A -> [A]// [A旧版] + A新版 -> [A新版]// [B,C] + A -> [A,B,C]// 10 条无 A + A -> A + 前 9 条// 10 条且 A 在末尾 + A新版 -> A新版 + 前 9 个非 A// limit=1 -> [A]// limit=0 -> []页面验收再验证:生成同一对象后多次导出不会重复,生成 11 个不同对象后只显示最近 10 个,收藏的内置图不会混入最近生成,删除收藏不会改变最近顺序。
若出现 11 条,检查限长条件是否放在 push 之后;若当前项重复,核对 ID 是否有空格或类型差异;若新内容没有替换旧内容,确认初始数组放入的是传入对象而不是旧列表对象。
十一、结语
最近列表并不是简单的unshift()。一条可靠的更新需要同时守住来源、身份、顺序和容量。拼豆制图用“新对象先入、循环跳过同 ID、长度小于 10 才追加”的单次遍历实现了最小闭环,收藏则复用相同 ID 保持跨集合一致。
下一步演进的重点不是把循环改得更短,而是把来源从 ID 前缀升级为字段、把数组规则提成纯函数、把最近摘要与完整作品分开持久化,并让异步写入按顺序提交。这样“最近 10 条”才不会意外变成“作品最多只能有 10 条”。