鸿蒙 PC Markdown 编辑器目录树性能:惰性展开与状态保持

📅 2026/7/20 14:23:17 👁️ 阅读次数 📝 编程学习
鸿蒙 PC Markdown 编辑器目录树性能:惰性展开与状态保持

鸿蒙 PC Markdown 编辑器目录树性能:惰性展开与状态保持

文件树性能不是把 ArkUI List换成更快组件就结束。真正成本来自目录枚举、每项 stat、URI构造、排序、节点数量、展开状态和外部变化。一个根目录可能只有十项,也可能一层包含数千文件;递归扫描会让用户只想打开 README却等待整个仓库。

本文基于 OhMarkdown,分析非递归读取、两千项上限、扁平可见数组、展开插入与折叠删除,并说明当前缓存和刷新边界。代码位于 https://gitcode.com/VON-/codex_md_oh

首屏只读取一级

constlistedNames=awaitfileIo.listFile(directoryPath,{recursion:false,listNum:MAX_DIRECTORY_ENTRIES+1});

recursion: false是最重要的性能决策。根目录打开成本只与直接子项相关,深层归档、依赖和图片不会提前遍历。用户点击某目录时才调用同一服务读取下一层。

递归读取不仅慢,还会把某个深层权限错误升级为整个工作区打开失败,并增加循环链接风险。惰性展开让错误局部化到节点。

用 MAX+1 判断截断

constMAX_DIRECTORY_ENTRIES=2000;consttruncated=listedNames.length>MAX_DIRECTORY_ENTRIES;constvisibleNames=listedNames.slice(0,MAX_DIRECTORY_ENTRIES).filter(isSafeEntryName);

请求2001项是为了知道是否还有更多。只请求2000项,返回满额时无法区分“恰好2000”和“被截断”。UI显示 first 2000 entries shown,不无声遗漏。

上限控制后续 Promise、stat、排序和 List数据规模。数值是 Alpha保护边界,尚未在设备生成两千项语料做完整压力测试,因此报告必须标为代码保护而非规模实测通过。

类型查询是主要 I/O 成本

每个安全名称需要 stat:

constentries=awaitPromise.all(visibleNames.map(async(name)=>{constchildUri=createChildUri(directoryUri,name);conststat=awaitfileIo.stat(childUri);constisDirectory=stat.isDirectory();if(!isDirectory&&!isMarkdownDocument(name)){returnundefined;}return{name,uri:childUri,parentUri:directoryUri,isDirectory,depth,expanded:false};}));

Promise.all让本地 I/O并发,但两千个 stat可能形成峰值。未来应测不同 provider;云盘或网络文件系统可能不适合无界并发。可以用固定并发池逐批32项,在延迟和资源之间平衡。

如果平台目录 API能直接返回类型,可减少 stat;但替换前要验证 URI授权与 provider兼容,不应只为少一行调用改变边界。

过滤减少可见节点

目录始终保留,普通文件只显示.md.markdown.mdown.mkd.txt。图片和构建产物不进入 List,既符合当前编辑能力,也减少 UI项。

过滤发生在 stat后,因为先要知道同名后缀对象是不是目录。仅靠扩展名可能把notes.md目录当文件。未来显示资源文件时,应引入类型分类而不是取消所有过滤。

排序在服务层完成

filteredEntries.sort((left,right)=>{if(left.isDirectory!==right.isDirectory){returnleft.isDirectory?-1:1;}returnleft.name.localeCompare(right.name);});

目录优先、同类按名称。服务返回即为最终展示顺序,ArkUI重绘不重复排序。稳定 URI作为 ForEach key,插入子项不会让未变化行失去身份。

localeCompare符合系统语言习惯,但跨设备顺序可能不同。若测试依赖固定顺序,应使用明确 locale或只断言目录优先和集合内容。

扁平数组比递归组件更直接

当前可见树是一维workspaceEntries。每项有 depth和 expanded,父节点后紧跟所有可见后代。ArkUI List直接迭代:

ForEach(this.workspaceEntries,(entry:WorkspaceEntry)=>{ListItem(){this.workspaceEntryRow(entry)}},(entry:WorkspaceEntry):string=>entry.uri)

扁平模型适合虚拟 List,滚动索引明确,不需要递归组件树。缩进由 depth计算,URI为稳定 key。

代价是展开和折叠需要数组切片,复杂度 O(n)。最多两千可见项时可接受;若未来工作区达到数万可见节点,需要分页、虚拟树或增量数据结构。

展开只插入直接子项

constchildWorkspace=awaitlistWorkspaceDirectory(entry.uri,entry.depth+1);constexpandedEntry={...entry,expanded:true};this.workspaceEntries=this.workspaceEntries.slice(0,entryIndex).concat([expandedEntry],childWorkspace.entries,this.workspaceEntries.slice(entryIndex+1));

读成功后才把父节点标为 expanded。失败时旧数组不变,用户可以重试。子项 depth为父加一,不递归加载孙节点。

操作期间operationInProgress=true,防止双击产生两个并发读取并重复插入。它是全工作台互斥,简单但会阻止同时打开另一个文件;未来可改为按 URI加载集合。

折叠删除连续后代

letremoveEnd=entryIndex+1;while(removeEnd<this.workspaceEntries.length&&this.workspaceEntries[removeEnd].depth>entry.depth){removeEnd+=1;}this.workspaceEntries=this.workspaceEntries.slice(0,entryIndex).concat([{...entry,expanded:false}],this.workspaceEntries.slice(removeEnd));

深度优先可见顺序保证后代连续,扫描到同级或更浅节点结束。整个子树一次移除。折叠会丢弃子节点缓存,再展开重新读取,因此能看到部分外部变化,也增加重复 I/O。

如果要保留状态,可建立 URI到子项缓存,折叠只隐藏;但缓存需要失效策略。目录外部变化、权限撤销和重命名都会使旧结果过期。当前选择重新读取,优先正确性。

展开状态的当前含义

expanded只存在运行时可见数组。应用重启、重新选择工作区或折叠后重新展开,都不会恢复深层展开状态。这不是功能缺陷伪装,而是当前边界。

要持久化展开状态,应保存 URI集合而非数组索引。重开工作区后按层逐步恢复,并设置最大恢复深度和超时,避免启动重新扫描大量目录。不存在的 URI要静默移除。

错误反馈不清空已有树

目录读取失败只设置:

this.operationStatus=`Folder failed:${errorinstanceofError?error.message:String(error)}`;

父节点保持折叠,其他节点仍可用。局部故障不应清空整个工作区。截断则使用明确状态和根面板提示,不把它当异常弹框。

后续可在失败节点旁显示重试图标,比全局状态栏更接近问题位置。错误文案要避免暴露完整私有路径。

鸿蒙 PC 实际树

下图来自 MateBook Pro 2in1模拟器,目录展开后插入嵌套 Markdown文件,树仍保持紧凑的一维列表。

模拟器已经验证一级读取、按需展开、折叠和树内打开。规模测试还应生成宽目录、深目录、空目录、无权限目录、大小写重名和大量非 Markdown文件,分别记录打开耗时、内存和滚动帧率。

可观测指标

目录性能不能只记录“打开成功”。建议记录选择器返回到首屏树可见的耗时、listFile耗时、stat总量、过滤后节点数、截断标记、展开耗时和失败类型。日志只记录数量与毫秒,不记录用户名称和 URI。

对两千项目录,应分别测本地 Documents、外部存储和云 provider。Promise并发策略以 P95延迟和内存为依据。ArkUI List滚动还需用帧率和卡顿判断,不能由服务耗时推断。

文件监听与刷新

当前没有实时监听。外部新增文件在已展开节点中不会立刻出现;折叠再展开会刷新。下一步可先增加手动刷新,保存展开 URI,重新加载可见层。实时监听要处理事件合并、重命名、父目录删除和应用后台。

监听不能绕过两千项上限,也不能把每个事件直接触发全树扫描。可将事件按目录聚合,在动画或短防抖窗口后刷新目标层。

当前边界

全局互斥限制并发操作;stat并发未分批;折叠不缓存;展开状态不持久化;没有刷新与监听;两千项尚缺设备规模实测;符号链接和循环语义未完整验证。

这些边界已经被限制在服务与树模型内,不影响文档会话和保存。后续优化可以替换目录加载策略,而无需改编辑器内核。

结语

目录树性能来自控制工作量:非递归首屏、MAX+1截断判断、类型过滤、稳定排序、扁平可见数组、按需插入和整段折叠。它不是无限扫描后期待 List虚拟化解决一切。

鸿蒙 PC Markdown编辑器首先保证小工作区立即可用、大目录有边界、局部失败可恢复。缓存、监听和更高规模必须建立在真实 provider数据上逐步增加。