Svelte pending 一直不结束
同时含脚本与模板 await 的子组件提前销毁,旧 Svelte 续行可能抛 async_deriveds 并留住 pending。核对 5.57.2 计数清理修复。
异步子组件已经从页面移除,外层 <svelte:boundary> 却仍显示 loading。等子组件里的 promise 完成,控制台又报:
TypeError: Cannot read properties of null (reading 'async_deriveds')
在启用 experimental.async 的 Svelte 5.57.1 中,如果子组件同时有脚本顶层 await 和模板 await,并在前者完成之前被销毁,可能进入这条续行缺口。正式版 5.57.2 修正了对应的销毁判断和 pending 计数释放。
两处 await 与提前销毁要同时存在
子组件可以有这样的结构:
<script>
const value = await loadValue();
</script>
<p>{await Promise.resolve(value)}</p>
loadValue 是应用自己的异步加载函数,需要在原组件中正确提供。这里只展示两个 await 的关系,不是完整的数据请求程序。
外层条件分支先挂载组件,又在 loadValue 尚未完成时把它移除。随后脚本 promise 完成,模板 await 的续行仍尝试创建异步派生值,便可能触发空 batch 错误。
普通模板 {value}、只有脚本里的 $derived(await ...),或只有模板 await 而没有脚本顶层 await,不是同一组合。这个缺陷在开发与生产路径都可能出现,并不要求一定存在 boundary;没有边界时,还可能使根节点一直不渲染。
抛错之前漏掉了计数清理
模板的异步表达式需要先等待脚本 blockers。旧 flatten 的回调在 blockers 完成后恢复上下文,再启动这些表达式,却没有先确认父 effect 已经销毁。
已销毁 effect 不会重新激活 batch,后续创建 async derived 时就访问到空值。异常发生得太早,正常用于减少 pending 的 finally 也没有接上,于是即使目标子组件已经消失,边界仍残留它的等待计数。
只捕获或隐藏 TypeError,不能补上这次计数释放。把所有子组件一直保留在页面里,也会改变业务生命周期,并不适合作为通用处理。
正式修复在续行前检查父组件
5.57.2 在 blocker 完成的回调中先检查父 effect 的销毁状态。如果已经销毁,就减少这次 pending 并返回,不再创建该组件的异步派生值。仍存活的组件继续走正常流程。
项目应选择兼容的修复版本,重新生成实际客户端资源,保留原来的显示与卸载条件。不能因为不再显示 loading,就关闭所有异步边界或取消必要的数据验证。
应用本身还要按业务需要处理已经发出的异步工作。框架停止销毁后的渲染续行,不表示外部请求自动取消,也不表示此前可能发生的副作用被撤销。
保留晚完成的条件验收
在受控环境中,让加载持续到组件卸载之后,再使其完成,确认没有 async_deriveds 空值错误,目标组件对应的 pending 不再残留。若边界里还有其他真正未完成的 await,它们仍应按正常规则保持等待。
随后验证不卸载时组件能显示正确数据,以及后续页面更新仍然工作。只看某个 loading 消失,无法区分计数正确释放、组件被强制移走或错误被隐藏。


