[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"i-ri:search-line":3,"i-ri:moon-line":8,"i-ri:sun-line":10,"i-ri:menu-3-line":12,"i-ri:close-line":14,"notes-module-items-frontend":16,"notes-page-frontend-01-forms\u002F02-async-persistence":42,"i-ri:arrow-left-s-line":197,"i-ri:menu-2-line":199,"i-ri:arrow-right-s-line":201},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"currentColor\" d=\"m18.031 16.617l4.283 4.282l-1.415 1.415l-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9s9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617m-2.006-.742A6.98 6.98 0 0 0 18 11c0-3.867-3.133-7-7-7s-7 3.133-7 7s3.133 7 7 7a6.98 6.98 0 0 0 4.875-1.975z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"currentColor\" d=\"M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2h.1A6.98 6.98 0 0 0 10 7m-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938A8 8 0 0 0 4 12\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"currentColor\" d=\"M12 18a6 6 0 1 1 0-12a6 6 0 0 1 0 12m0-2a4 4 0 1 0 0-8a4 4 0 0 0 0 8M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636L5.636 7.05zM16.95 18.364l1.414-1.414l2.121 2.121l-1.414 1.414zm2.121-14.85l1.414 1.415l-2.121 2.121l-1.414-1.414zM5.636 16.95l1.414 1.414l-2.121 2.121l-1.414-1.414zM23 11v2h-3v-2zM4 11v2H1v-2z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm6 7h12v2H9zm-6 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":15},"\u003Cpath fill=\"currentColor\" d=\"m12 10.587l4.95-4.95l1.414 1.414l-4.95 4.95l4.95 4.95l-1.415 1.414l-4.95-4.95l-4.949 4.95l-1.414-1.415l4.95-4.95l-4.95-4.95L7.05 5.638z\"\u002F>",[17,22,26,30,34,38],{"path":18,"title":19,"meta":20},"\u002Fnotes\u002Ffrontend\u002F01-forms\u002F01-clear-semantics","表单清空后，旧数据为什么又回来了？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},"2026-11-05T09:47:15.000Z",{"path":23,"title":24,"meta":25},"\u002Fnotes\u002Ffrontend\u002F01-forms\u002F02-async-persistence","保存请求回来时，文档已经变了",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":27,"title":28,"meta":29},"\u002Fnotes\u002Ffrontend\u002F02-canvas\u002F01-geometry-history","拖一下画布，为什么撤销回不去了？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":31,"title":32,"meta":33},"\u002Fnotes\u002Ffrontend\u002F03-printing\u002F01-pagination-fonts","预览正常，PDF 为何多一页或缺字？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":35,"title":36,"meta":37},"\u002Fnotes\u002Ffrontend\u002F04-web-delivery\u002F01-cache-diagnosis","API 更新了，博客页面为什么还是旧的？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":39,"title":40,"meta":41},"\u002Fnotes\u002Ffrontend","前端与 Web 工程",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"id":43,"type":44,"sourcePath":45,"routePath":23,"path":23,"title":24,"description":46,"updatedAt":21,"meta":47,"body":56,"toc":188},"bm90ZTpub3Rlcy9mcm9udGVuZC8wMS1mb3Jtcy8wMi1hc3luYy1wZXJzaXN0ZW5jZS5tZA","note","notes\u002Ffrontend\u002F01-forms\u002F02-async-persistence.md","表单设计器允许用户编辑字段配置、保存草稿，也可以随时切换到另一份表单。保存请求发出去后，页面并不会停止：用户还可以输入、切文档，甚至更换账号或权限。",{"id":43,"type":44,"sourcePath":45,"slug":48,"title":24,"updatedAt":21,"description":46,"tags":49,"date":53,"comment":54,"summary":55,"lastUpdated":21},"frontend\u002F01-forms\u002F02-async-persistence",[50,51,52],"frontend","编辑器","并发","2026-07-31",true,"异步保存只写入发出时的快照。继续编辑、切换文档、权限变化和新建超时，需要分别判断返回结果还能做什么。",{"type":57,"children":58},"root",[59,67,72,77,84,89,94,99,105,110,115,120,125,131,136,141,146,152,157,162,167,172,178,183],{"type":60,"tag":61,"props":62,"children":64},"element","h1",{"id":63},"保存请求回来时文档已经变了",[65],{"type":66,"value":24},"text",{"type":60,"tag":68,"props":69,"children":70},"p",{},[71],{"type":66,"value":46},{"type":60,"tag":68,"props":73,"children":74},{},[75],{"type":66,"value":76},"如果保存成功就统一清掉“未保存”标记，很容易丢掉请求期间的新修改。这里讨论的是客户端如何处理迟到结果，不涉及让服务端撤回已经发生的写入。",{"type":60,"tag":78,"props":79,"children":81},"h2",{"id":80},"保存成功为什么仍然有未保存内容",[82],{"type":66,"value":83},"保存成功，为什么仍然有未保存内容？",{"type":60,"tag":68,"props":85,"children":86},{},[87],{"type":66,"value":88},"点击保存时发出的是一个快照。假设请求带走版本 10，用户继续编辑到版本 11，然后版本 10 保存成功。服务端确实保存了内容，但页面上的版本 11 还没有保存。",{"type":60,"tag":68,"props":90,"children":91},{},[92],{"type":66,"value":93},"一种办法是在保存期间锁住编辑，处理简单，但用户需要等待。另一种是允许继续编辑，分别记录“已保存到哪个版本”和“现在编辑到哪个版本”。项目选择后者，成功后可以更新保存基线，却不能把新内容的 dirty 标记清掉。",{"type":60,"tag":68,"props":95,"children":96},{},[97],{"type":66,"value":98},"切换文档还要更严格。保存 A 的时候打开 B，A 的结果不能改 B 的标题、内容或保存状态。",{"type":60,"tag":78,"props":100,"children":102},{"id":101},"用取消请求或一个版本号够不够",[103],{"type":66,"value":104},"用取消请求或一个版本号，够不够？",{"type":60,"tag":68,"props":106,"children":107},{},[108],{"type":66,"value":109},"取消等待能减少无用工作，但请求可能已经到达服务端。客户端不再接纳结果，与服务端有没有完成写入，是两个问题。",{"type":60,"tag":68,"props":111,"children":112},{},[113],{"type":66,"value":114},"项目在操作开始时保存用户身份、权限版本、组件生命周期、当前文档和工作区代次，再记录内容版本。返回时先比较身份：已经换用户、换文档或重载工作区，就拒绝回写；身份相同而内容版本增加，则只处理原快照的保存事实，保留当前修改。",{"type":60,"tag":68,"props":116,"children":117},{},[118],{"type":66,"value":119},"这些信息不能全部塞进一个递增数字。用户身份需要稳定，权限变化要让旧操作失效，内容变化则只说明又有新修改，处理结果并不相同。",{"type":60,"tag":68,"props":121,"children":122},{},[123],{"type":66,"value":124},"项目用可手动完成的 Promise 和存储、网络替身制造这些交错：先暂停请求，继续编辑或切换身份，再放结果回来。它能检查客户端接受了哪份结果，真实多标签页行为和服务端写入原子性仍要在对应环境验证。",{"type":60,"tag":78,"props":126,"children":128},{"id":127},"确认框打开后才降权怎么办",[129],{"type":66,"value":130},"确认框打开后才降权，怎么办？",{"type":60,"tag":68,"props":132,"children":133},{},[134],{"type":66,"value":135},"删除确认框打开时有权限，点击确认之前被降权，旧回调仍可能持有先前的角色。只把按钮隐藏或用 effect 关闭弹窗，未必来得及阻止它执行。",{"type":60,"tag":68,"props":137,"children":138},{},[139],{"type":66,"value":140},"项目会给交互记录身份、权限和代次，执行确认动作前重新检查。如果中间还要读文件等异步操作，返回后再检查一次。关闭弹窗处理的是界面，拒绝旧回调处理的是已经启动的操作。",{"type":60,"tag":68,"props":142,"children":143},{},[144],{"type":66,"value":145},"服务端仍需独立鉴权。前端降权、关页面或取消等待，都不能撤销已经被服务端接受的请求。",{"type":60,"tag":78,"props":147,"children":149},{"id":148},"新建超时后为什么不能直接再发一次",[150],{"type":66,"value":151},"新建超时后，为什么不能直接再发一次？",{"type":60,"tag":68,"props":153,"children":154},{},[155],{"type":66,"value":156},"更新已有文档时，客户端知道服务端 ID。新建则可能是请求已发出，还没拿到 ID 就超时：服务端可能没收到，也可能已经创建成功。直接重发有可能得到两份文档。",{"type":60,"tag":68,"props":158,"children":159},{},[160],{"type":66,"value":161},"常见的处理思路是为同一次创建保留稳定身份，并由服务端识别重复请求。这个项目目前先在客户端登记：按用户和客户端文档 ID 保存进行中、已取得结果、结果未知三种状态。拿不准远端结果时保留未知，避免无条件再创建。",{"type":60,"tag":68,"props":163,"children":164},{},[165],{"type":66,"value":166},"登记使用稳定的用户身份，不包含权限版本，否则降权再升权后，会查不到原来的请求。登记还限制每个用户的条目数和快照字节数，满了在请求前拒绝新任务，不能为了腾位置删掉未知条目，再打开重复创建的入口。",{"type":60,"tag":68,"props":168,"children":169},{},[170],{"type":66,"value":171},"这份登记保存在内存里，尚不能解决进程退出或另一台设备提交的问题，服务端幂等仍需要服务端配合。",{"type":60,"tag":78,"props":173,"children":175},{"id":174},"远端保存成功本地草稿失败呢",[176],{"type":66,"value":177},"远端保存成功，本地草稿失败呢？",{"type":60,"tag":68,"props":179,"children":180},{},[181],{"type":66,"value":182},"远端成功后，本地存储也可能写不进去。项目不会直接清 dirty，让用户误以为所有保存步骤都结束了；写回时还要比较内容版本，防止旧快照覆盖请求期间的新内容。",{"type":60,"tag":68,"props":184,"children":185},{},[186],{"type":66,"value":187},"本地新格式草稿和旧格式兼容镜像也分别处理。新格式已写成功、只有镜像失败时，恢复优先读取新格式，避免再次从旧镜像恢复出过期内容。多标签页之间的并发写入，则还需要跨标签页的版本检查和锁策略。",{"title":189,"searchDepth":190,"depth":190,"links":191},"",2,[192,193,194,195,196],{"id":80,"depth":190,"text":83},{"id":101,"depth":190,"text":104},{"id":127,"depth":190,"text":130},{"id":148,"depth":190,"text":151},{"id":174,"depth":190,"text":177},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":198},"\u003Cpath fill=\"currentColor\" d=\"m10.828 12l4.95 4.95l-1.414 1.415L8 12l6.364-6.364l1.414 1.414z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":200},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm0 7h12v2H3zm0 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":202},"\u003Cpath fill=\"currentColor\" d=\"m13.172 12l-4.95-4.95l1.414-1.413L16 12l-6.364 6.364l-1.414-1.415z\"\u002F>"]