欢迎回来
登录你的知识库账户
忘记密码?
还没有账户?立即注册
创建账户
注册你的专属知识库
已有账户?去登录
找回密码
输入注册邮箱获取验证码
返回登录
请输入图片中的验证码以继续注册
加载中...
取消
新建收藏
手动添加你喜欢的内容
取消
编辑头像与昵称
上传新头像或修改你的显示昵称
支持 JPG/PNG,最大 2MB
取消

问题反馈

notebasewww.notebase.cn
控制台
内容库
动态
管理
账户
U
用户
--
在线
v0.8.7 · 知识库
笔记
KnowledgeBase
网络无边,知识有迹。
0笔记
0工具
30推荐

分类导航

按主题直达

编辑精选

站内用户贡献 · 真实笔记

最新收录

每日更新
继续浏览全部内容 →
>
笔记
0
加载中...
工具
0
此页用于记录用户反馈问题后的每一次改进
笔记用法

“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。

md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。

要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。

工具用法

在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。

要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。

工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。

隐藏笔记

写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。

适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。

数据安全

你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。

技术

全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。

理念

这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。

原则

不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。

更多

产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。

举报

如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。

趋势
// 点击导航加载发现
归档
// 归档为空
最近浏览
// 暂无浏览记录
发布
// 加载中...
用户发布
// 加载中...
用户管理
// 加载中...
访问统计
// 加载中...
内容审核
// 加载中...
个人信息
// 加载中...
返回首页

将异步状态从UI生命周期中解耦

2026/7/6编程开发

我之前几篇文章一直在强调一个核心架构原则:一旦渲染层不再主宰整个数据流,State(状态)、Derived State(派生状态)和Effects(副作用)之间的边界就变得至关重要。当我们习惯性地把所有影响UI的变量都塞进泛化的“状态”里时,系统很快就会失去语义结构。

在现代前端应用中,这种架构缺失在处理异步工作时表现得最为明显。异步数据从来就不是“一个将来会出现的值”——它携带了关于来源、时效性、取消、错误恢复和失效的复杂语义。如果这些语义没有被显式建模,它们就不可避免地会下沉到UI框架的生命周期中,通过组件的挂载、effect依赖和回调守卫来间接拼凑。

这篇文章的核心问题就是:当异步工作的正确性被迫依赖于UI生命周期时,系统会失去什么?

我们都熟悉的模式

先来看个最基础的例子:

const data = await fetchSomething()
setState(data)

或者用标准的UI框架hook:

useEffect(() => {
  let cancelled = false
  fetchSomething().then(result => {
    if (!cancelled) {
      setData(result)
    }
  })
  return () => {
    cancelled = true
  }
}, [])

对于简单场景,这段代码没什么问题。它很直观,完美契合Promise的设计方式:触发操作,等待结果,把结果写回状态。

但这个思维模型有个隐蔽的副作用——它让我们把异步工作简单理解为“Promise resolve之后调用setState”。对于简单的页面,这确实够用。但随着应用规模增长,这个模型开始暴露出结构性问题。

Promise只描述了完成,没描述归属

Promise解决了一个非常具体的问题:某件工作会在将来完成,要么成功要么失败。它能完美描述从pending到fulfilled或pending到rejected的转换。

但Promise本身回答不了这些问题:

  • 这些数据归谁所有?
  • Promise resolve时,数据还依然有效吗?
  • 这个结果如何级联到其他派生数据节点?
  • 如果发生错误,谁拥有恢复状态?
  • 在重新获取数据时,我们是应该丢弃旧数据,还是实现stale-while-revalidate模式?

Promise描述的是一次执行的声明周期,但它没有定义这次执行在应用更广泛的数据流中属于哪个位置。

前端工程真正的挑战不是等待Promise完成,而是编排如何让这个结果正确地进入系统。如果我们只是把payload塞进组件状态,那就等于让UI生命周期来承担它从未被设计要处理的数据协调工作。

setState不是异步正确性的边界

setState的职责很简单:把一个值写入状态容器,并通知UI可能需要重新渲染。

但异步正确性远不止赋值这么简单。拿经典的竞态条件来说:

const userId = state.userId
const user = await fetchUser(userId)
setUser(user)

如果请求还在进行中时userId变了呢?想象一下这个序列:

fetchUser(1) 开始
fetchUser(2) 开始
fetchUser(2) 先完成
fetchUser(1) 后完成

如果我们只是简单地在每个Promise resolve后调用setState,UI最终可能会显示:

userId = 2
user data = user 1

这不是Promise的缺陷——它忠实地交付了payload。真正的问题是系统缺乏一个清晰的语义层来判断这个异步结果是否还属于当前的数据状态。

于是我们开始加各种守卫:

let requestId = 0

async function loadUser(userId: string) {
  const current = ++requestId
  const user = await fetchUser(userId)
  if (current === requestId) {
    setUser(user)
  }
}

或者使用取消机制:

const controller = new AbortController()
fetch(url, { signal: controller.signal })

这些技术很实用,也确实解决了局部问题。但本质上,它们都在修补同一个问题:异步工作没有被放入数据流的语义中。它被当作一个外部过程,在完成后把结果推回状态。

Loading、Error和Data不是独立状态

另一个常见模式是把异步工作拆成几个独立的状态:

const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)

然后在请求过程中手动同步它们:

setLoading(true)
setError(null)
try {
  const result = await fetchSomething()
  setData(result)
} catch (err) {
  setError(err)
} finally {
  setLoading(false)
}

从UI角度看,把异步工作拆成data、loading和error似乎很合理。UI确实需要根据不同的异步状态渲染不同的内容。

但语义上,这三个值不是互不相关的状态片段。它们描述的是同一个异步资源的不同方面。换句话说,data、loading和error不应该只是组件里散落的字段——它们应该被理解为同一个资源状态的不同投影。

否则,系统很容易进入不一致的组合状态:

loading = false, error = null, data = 过期数据
loading = true, error = 之前的错误, data = 之前的数据
loading = false, error = 错误, data = 新数据

有些组合是有效的,有些是bug,有些取决于产品语义。比如在重新加载数据时,是否应该保留之前的数据?

status = loading, data = 之前的数据

这完全可以合理——因为UI可能想要展示stale-while-revalidate行为。但如果这些值只是分散在多个setState调用中,系统本身不知道哪些组合是有效的。开发者必须在每个组件中手动维护这些关系。

Effect不应该协调每一个异步任务

在很多前端架构中,Effect被当作异步工作的万能解决方案。这可以理解——API请求、WebSocket、定时器都是与外部世界的交互。

但当Effect承担了过多的异步协调工作,它们就会成为系统复杂性的中心。很容易出现这样的逻辑:

Effect A: 获取数据
Effect B: 监听数据,更新派生状态
Effect C: 把派生状态写入缓存
Effect D: 监听变更结果,触发重新获取

表面上看,这很“响应式”。但实际上,它已经退化成了一个靠执行顺序维持的过程网络。正确性不再依赖于清晰的依赖关系,而是依赖于:

  • 哪个Effect先运行?
  • 哪个状态先被清除?
  • 哪个回调观察到了哪个中间值?

Effect的本质是执行副作用,而不是表达数据关系。如果启动请求、取消请求、检查有效性、处理错误、级联更新这些逻辑都散落在不同的Effect里,你很快就会失去对数据流的全局理解。

那应该怎么做?

核心思路是:把异步资源当作一等公民来建模,而不是把它拆解成UI框架的原子状态。

一个异步资源应该包含:

  • 当前值(可能为null或过期数据)
  • 加载状态(idle/loading/refreshing)
  • 错误信息
  • 最后更新时间戳
  • 失效策略

这样,组件只需要声明“我需要这个资源”,而不需要关心请求何时发起、何时取消、如何处理竞态。所有这些逻辑都封装在资源层。

以React Query、SWR、TanStack Query这类库为例,它们做的其实就是这件事:

// 声明式地声明一个异步资源
const { data, isLoading, error } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  staleTime: 5 * 60 * 1000, // 5分钟内认为数据有效
})

这里的关键区别是:UI不再是异步工作的调度者,而是消费者。资源层负责管理请求的生命周期、缓存、失效、重试、竞态处理。UI只需要根据资源的不同状态渲染不同的视图。

更进一步的思考

如果你不想引入外部库,也可以自己构建一个轻量的资源管理层。核心是定义好这些概念:

资源标识:每个异步资源应该有一个唯一的键,用来标识它是什么。这个键可以包含参数,比如['user', userId]。当键变化时,资源层知道需要重新获取。

资源状态:定义一个不可变的状态对象,包含data、status、error、updatedAt等字段。所有对状态的修改都通过reducer或类似机制进行,确保状态转换是合法的。

资源获取:封装fetch逻辑,处理竞态(通过请求ID或AbortController)、缓存、重试、错误恢复。

资源订阅:组件通过hook订阅资源,当资源状态变化时自动重新渲染。

这样,你的组件代码就从“我该怎么发起请求、处理竞态、管理三个独立状态”变成了“我需要这个资源,给我渲染”。

总结

回到开头的问题:当异步工作的正确性被迫依赖于UI生命周期时,系统会失去什么?

它会失去:

  1. 语义清晰度:异步数据的来源、时效性、归属关系变得模糊
  2. 一致性保证:data/loading/error可能进入非法组合
  3. 可维护性:竞态处理、取消逻辑散落在各个组件和Effect中
  4. 可测试性:异步逻辑和UI生命周期深度耦合,难以单独测试

解决方案不是发明新的框架或范式,而是重新把异步资源当作一个有生命周期的实体来建模,而不是把它拆解成UI框架的原子状态片段。

UI应该消费资源,而不是管理资源。

编写使用方法
Markdown 格式 · Ctrl+Enter 确定
新建笔记
预览
数据表格
点击单元格编辑 · Tab 移动
A1fx
Sheet1
BIH1H2≡🔗</>
隐私提醒

取消
编辑工具
取消