深入理解 Yew 的 Suspense 机制:从示例到源码剖析
2026/9/19 20:06:09 网站建设 项目流程

深入理解 Yew 的 Suspense 机制:从示例到源码剖析

【免费下载链接】yewRust / Wasm framework for creating reliable and efficient web applications项目地址: https://gitcode.com/gh_mirrors/ye/yew

导读

本篇文章以仓库中的 Suspense 示例(examples/suspense)为主体,系统讲解 Yew 框架中<Suspense />组件的核心概念、自定义 suspending hook 的编写方式,以及函数式组件与结构式组件两种消费 Suspense 的完整模式。读完本文,你将掌握:如何在 Yew 应用中用Suspense包裹异步渲染逻辑、如何通过SuspensionResult让组件"暂停"渲染并展示 fallback UI、如何利用use_reducerSuspension::from_future自建可重用的 suspense 钩子,并了解其底层实现(BaseSuspenseSuspension消息循环)是如何工作的。

一、示例概览:一个"写长故事"的应用

examples/suspense/README.md 指出,这是一个演示 Yew<Suspense />支持的示例,核心概念是展示<Suspense />在 Yew 中如何工作,以及如何创建"利用 suspense 的 hooks"。

示例应用页面左右分为两块区域:

  • 左侧标题为"Yew Suspense Demo -- function component consumer",展示函数式组件消费 Suspense;
  • 右侧标题为"Yew Suspense Demo -- struct component consumer",展示结构式组件消费 Suspense。

两块区域内部的交互完全一致:一个<textarea>允许用户输入一段"故事",下方是一个"Take a break!"按钮。点击按钮后,对应区域会重新进入 suspense(暂停渲染),显示 fallback 提示"Please wait 5 Seconds for ... component to load...",5 秒后恢复渲染,并且用户在 textarea 里输入的内容被完整保留——这正是 Suspense 与组件状态(State)配合的核心价值:暂停的只是"渲染",不是"状态"

从入口代码 examples/suspense/src/main.rs 可以看到,两个区块共用同一个Suspense容器,只是 fallback 文案来源不同:

#[function_component(App)] fn app() -> Html { let fallback_fn = html! {<PleaseWait from="function" />}; let fallback_struct = html! {<PleaseWait from="struct" />}; html! { <div class="layout"> <div class="content"> <h2>{" Yew Suspense Demo -- function component consumer"}</h2> <Suspense fallback={fallback_fn}> <AppContent /> </Suspense> </div> <div class="content"> <h2>{"Yew Suspense Demo -- struct component consumer"}</h2> <Suspense fallback={fallback_struct}> <struct_consumer::AppContent /> </Suspense> </div> </div> } }

PleaseWait是一个极简的函数组件,仅渲染一行 fallback 文案:

#[function_component(PleaseWait)] fn please_wait(props: &PleaseWaitProps) -> Html { html! {<div class="content-area">{"Please wait 5 Seconds for "}{props.from}{" component to load..."}</div>} }

应用通过yew::Renderer::<App>::new().render()挂载(main.rs),页面样式定义在 examples/suspense/index.scss(双栏.layout网格布局),HTML 模板位于 examples/suspense/index.html,并通过 Trunk.toml 配置wasm_opt = "version_129"进行发布时优化。

二、运行方式

README 给出的运行命令非常简单——使用 Trunk 开发服务器:

trunk serve --open

该命令会编译 Rust/Wasm 代码、启动本地开发服务器并自动打开浏览器。关于前置条件与依赖说明:

  • 示例的依赖声明在 examples/suspense/Cargo.toml:yew = { path = "../../packages/yew", features = ["csr"] },即本示例使用**客户端渲染(csr)**特性,不涉及 SSR;同时依赖gloo(开启futures特性,用于gloo::timers::future::sleep)、wasm-bindgen-futureswasm-bindgen以及web-sysHtmlTextAreaElement特性(用于读取 textarea 输入)。
  • 由于yew通过path指向仓库内的 packages/yew,直接运行的是当前仓库源码版本的 Yew;若希望按发布版体验,可相应调整依赖版本。
  • 首次运行前需确保已安装trunk(例如cargo install trunk)与 Rust 的wasm32-unknown-unknowntarget。
  • 若使用 Trunk.toml 中的wasm_opt配置,需要 Trunk 能下载对应版本的wasm-opt(一般会自动处理)。

三、核心概念一:SuspenseSuspensionSuspensionResult

要理解示例,先要建立三个底层概念,它们都定义在 packages/yew/src/suspense 模块中。

3.1Suspense组件

Suspense是 Yew 暴露给用户的公共组件,声明于 packages/yew/src/suspense/component.rs。它的SuspenseProps只有两个字段:

字段类型说明
childrenHtmlSuspense 包裹的子内容,即需要等待异步就绪才渲染的部分(#[prop_or_default],可省略)
fallbackHtml子内容尚未就绪时展示的备用 UI(#[prop_or_default],可省略)

csr/ssr特性开启时,Suspense组件实际渲染为内部的BaseSuspense结构式组件,并把 fallback 与 children 分别作为其 props 传入:

#[component] pub fn Suspense(props: &SuspenseProps) -> Html { let SuspenseProps { children, fallback } = props.clone(); let fallback = html! { <BaseSuspense> {fallback} </BaseSuspense> }; html! { <BaseSuspense {fallback}> {children} </BaseSuspense> } }

需要特别注意的是:fallback 本身也是被另一个BaseSuspense包裹的——从源码注释与update中的断言可以看出,fallback 内部不允许再次 suspend(否则会触发assert"You cannot suspend from a component rendered as a fallback."),这个双层设计保证了 fallback 一定是同步可渲染的。

3.2Suspension:可挂起的异步任务

Suspension定义在 packages/yew/src/suspense/suspension.rs。它是一个轻量句柄,内部维护:

  • id: usize:全局递增的唯一标识(用于PartialEq比较,判断是否为同一挂起);
  • listeners: Rc<RefCell<Vec<Callback<Self>>>>:恢复(resume)时通知的回调列表;
  • resumed: Rc<AtomicBool>:标记该挂起是否已恢复。

关键 API:

/// 创建一对 (Suspension, SuspensionHandle) pub fn new() -> (Self, SuspensionHandle) /// 由 Future 创建:future 完成时自动 resume pub fn from_future(f: impl Future<Output = ()> + 'static) -> Self /// 是否已经恢复 pub fn resumed(&self) -> bool

from_future的实现体现了"自动恢复"的机制:它内部用crate::platform::spawn_local启动一个本地任务,await传入的 future,完成后调用handle.resume()

pub fn from_future(f: impl Future<Output = ()> + 'static) -> Self { let (self_, handle) = Self::new(); spawn_local(async move { f.await; handle.resume(); }); self_ }

SuspensionHandle则承担"手动控制"职责:调用resume()或当其被Drop时,都会触发resume_by_ref(),把resumed标记置为true并通知所有 listener(见 suspension.rs)。这为"取消/超时/外部事件触发恢复"等场景提供了灵活性。

Suspension还实现了Future:当其未恢复时返回Poll::Pending,恢复后被唤醒返回Poll::Ready(()),这意味着你也可以在async块里直接await一个Suspension

3.3SuspensionResult<T>:让组件"抛出挂起"

/// A Suspension Result. pub type SuspensionResult<T> = std::result::Result<T, Suspension>;

这是示例代码中最核心的桥接类型:一个返回SuspensionResult<T>的组件(或 hook),用Ok(T)表示"可以渲染",用Err(Suspension)表示"还需要等待"。当返回Err(_)时,Yew 会向最近的BaseSuspense发送挂起消息,从而切换到 fallback UI。

四、核心概念二:如何编写"利用 suspense 的 hooks"

示例中的自定义 hookuse_sleep定义在 examples/suspense/src/use_sleep.rs,它演示了创建 suspending hook 的标准套路。这里完整给出并逐段解释:

use std::rc::Rc; use std::time::Duration; use gloo::timers::future::sleep; use yew::prelude::*; use yew::suspense::{Suspension, SuspensionResult}; #[derive(PartialEq)] pub struct SleepState { s: Suspension, } impl SleepState { fn new() -> Self { let s = Suspension::from_future(async { sleep(Duration::from_secs(5)).await; }); Self { s } } } impl Reducible for SleepState { type Action = (); fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> { Self::new().into() } } #[hook] pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> { let sleep_state = use_reducer(SleepState::new); if sleep_state.s.resumed() { Ok(Rc::new(move || sleep_state.dispatch(()))) } else { Err(sleep_state.s.clone()) } }

这套实现的巧妙之处在于把"是否挂起"变成组件状态的一部分

  1. SleepStateSuspension放进 reducer 状态:通过use_reducer(SleepState::new)创建状态。Suspension本身是Clone + PartialEq(按id比较),可以安全地存储在状态里。
  2. use_sleep()返回SuspensionResult<Rc<dyn Fn()>>
    • sleep_state.s.resumed()为真,说明 5 秒已完成,返回Ok,闭包内执行dispatch(())
    • 否则返回Err(sleep_state.s.clone()),把Suspension作为"未完成"信号抛出。
  3. 点击"Take a break!"触发重新挂起:调用返回的闭包 →dispatch(())Reducible::reduce执行SleepState::new(),创建新的Suspension(新一轮 5 秒计时)→ 状态变化触发组件重渲染 → 此时新Suspension尚未resumed,组件再次返回Err,于是又进入挂起状态、展示 fallback,5 秒后自动恢复。

值得注意:use_sleep返回的Err(Suspension)中的Suspension带有#[error("suspend component rendering")]的 thiserror 派生,Suspension本身实现了std::error::Error,因此它可以与?运算符配合使用——这正是示例中函数组件AppContentlet resleep = use_sleep()?;能直接编译的原因。

4.1 官方通用 hook:use_futureuse_future_with

如果不想每次手写 reducer,Yew 在 packages/yew/src/suspense/hooks.rs 提供了两个官方通用 hook:

  • use_future(init_f)use_future_with((), move |_| init_f())的特化版本。首次调用即启动 future 并总是至少挂起一次,返回SuspensionResult<UseFutureHandle<O>>UseFutureHandle<O>实现了Deref<Target = O>,因此可以直接*res取值。官方文档注释中还给出了一个 Wikipedia 搜索的示例用法(Request::get(URL).send().await?.text().await)。
  • use_future_with(deps, f):带依赖版本。当deps变化时会重新生成 future;通过latest_id(一个Cell<u32>)保证只有最新一次 future 的结果才会被提交,旧任务的迟到结果会被丢弃,避免覆盖新状态或触发无意义更新。

这两个 hook 与use_sleep的原理一致:内部都通过Suspension::from_future创建挂起,并在resumed()后返回Ok,否则返回Err(Suspension)。区别在于use_future/use_future_with把结果缓存进UseStateHandle<Option<O>>,更加通用。

五、函数式组件消费 Suspense

函数式消费端是 examples/suspense/src/main.rs 中的AppContent

#[function_component(AppContent)] fn app_content() -> HtmlResult { let resleep = use_sleep()?; let value = use_state(|| "I am writing a long story...".to_string()); let on_text_input = { let value = value.clone(); Callback::from(move |e: InputEvent| { let input: HtmlTextAreaElement = e.target_unchecked_into::<HtmlTextAreaElement>(); value.set(input.value()); }) }; let on_take_a_break = Callback::from(move |_| resleep()); Ok(html! { <div class="content-area"> <textarea value={value.to_string()} oninput={on_text_input} /> <div class="action-area"> <button onclick={on_take_a_break}>{"Take a break!"}</button> <div class="hint">{"You can take a break at anytime"}<br />{"and your work will be preserved."}</div> </div> </div> }) }

关键点:

  • 函数组件返回类型是HtmlResult(即Result<Html, yew::html::Error>)。use_sleep()?Suspension错误直接上抛——Yew 的渲染管线在收到该错误类型的值(本质是Err(Suspension))时会向最近的Suspense边界发送"挂起"消息。
  • 组件状态用use_state管理(初始文案"I am writing a long story..."),textarea 的oninput通过target_unchecked_into::<HtmlTextAreaElement>()读取输入值并写回状态。
  • 由于状态(value)存储在组件自身而非局部变量中,即使组件因挂起而暂时不渲染、5 秒后重新渲染,输入内容也不会丢失——这正是 UI 上提示"You can take a break at anytime and your work will be preserved."的技术原因。

六、结构式组件消费 Suspense

结构式消费端在 examples/suspense/src/struct_consumer.rs,其结构比函数式版本更复杂,展示了如何在struct component(Componenttrait 实现)体系内消费 suspense。

6.1 通过泛型包装组件接入 suspense

因为Componenttrait 的view等方法返回普通Html,无法像HtmlResult那样直接抛Err(Suspension),示例引入了一个泛型包装组件WithSleep<Comp>

#[function_component] pub fn WithSleep<Comp>() -> HtmlResult where Comp: BaseComponent<Properties = AppContentProps>, { let sleep = use_sleep()?; let sleep = Callback::from(move |_| sleep()); Ok(yew::virtual_dom::VChild::<Comp>::new(AppContentProps { resleep: sleep }, None).into()) }
  • WithSleep<Comp>本身是函数式组件,因此可以直接use_sleep()?上抛挂起;
  • 一旦挂起结束,它通过VChild::<Comp>::new(AppContentProps { resleep: sleep }, None)动态构造并渲染目标 struct 组件Comp,把"重新休眠"的回调通过 props 传入;
  • 类型别名pub type AppContent = WithSleep<BaseAppContent>;将两者绑定,让外部使用struct_consumer::AppContent就像使用一个普通组件。

6.2 底层 struct 组件

BaseAppContent是一个标准 struct 组件:

pub struct BaseAppContent { value: String, } impl Component for BaseAppContent { type Message = Msg; type Properties = AppContentProps; fn create(_ctx: &Context<Self>) -> Self { Self { value: "I am writing a long story...".to_string() } } fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool { match msg { Msg::ValueUpdate(v) => { self.value = v; } Msg::TakeABreak => { ctx.props().resleep.emit(()); } }; true } fn view(&self, ctx: &Context<Self>) -> Html { let oninput = ctx.link().callback(|e: InputEvent| { let input: HtmlTextAreaElement = e.target_unchecked_into::<HtmlTextAreaElement>(); Msg::ValueUpdate(input.value()) }); let on_take_a_break = ctx.link().callback(|_| Msg::TakeABreak); html! { <div class="content-area"> <textarea value={self.value.clone()} {oninput} /> <div class="action-area"> <button onclick={on_take_a_break}>{"Take a break!"}</button> <div class="hint">{"You can take a break at anytime"}<br />{"and your work will be preserved."}</div> </div> </div> } } }

交互逻辑与函数式版本一一对应:Msg::ValueUpdate更新状态,Msg::TakeABreak通过ctx.props().resleep.emit(())触发外层挂起。注意这里 "Take a break" 的触发点位于 struct 组件内部,而挂起的产生位于外层包装组件WithSleep,二者通过Callback解耦。

七、底层原理:BaseSuspense的消息驱动生命周期

要理解 Suspense 的恢复机制,需要看BaseSuspense如何管理"挂起集合"。相关实现位于 packages/yew/src/suspense/component.rs。

7.1 消息类型与状态

pub(crate) enum BaseSuspenseMsg { Suspend(Suspension), Resume(Suspension), } pub(crate) struct BaseSuspense { suspensions: Vec<Suspension>, // csr 特性下用于延迟执行子组件的 rendered 生命周期 #[cfg(feature = "csr")] pending_rendered: RefCell<Vec<(usize, PendingRendered)>>, }

BaseSuspenseVec<Suspension>记录当前所有未完成的挂起。子组件(或 hooks)返回Err(Suspension)时,会通过BaseSuspense::suspend(scope, s)发送Suspend(s)消息;Suspension恢复时会通知 listener,进而发送Resume(s)消息。

7.2update:挂起与恢复的去重逻辑

fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool { match msg { Self::Message::Suspend(m) => { assert!( ctx.props().fallback.is_some(), "You cannot suspend from a component rendered as a fallback." ); if m.resumed() { return false; } if self.suspensions.iter().any(|n| n == &m) { return false; } self.suspensions.push(m); true } Self::Message::Resume(ref m) => { let suspensions_len = self.suspensions.len(); self.suspensions.retain(|n| m != n); suspensions_len != self.suspensions.len() } } }
  • 收到Suspend:先断言存在 fallback(防止在 fallback 内部再挂起);已恢复或已存在的挂起直接忽略(按id去重);否则加入集合并触发重渲染。
  • 收到Resume:从集合中移除对应挂起;集合从非空变为空时返回true,触发重渲染,此时view!self.suspensions.is_empty()false,子内容才会真正渲染。

7.3view:切换 fallback 与真实内容

fn view(&self, ctx: &Context<Self>) -> Html { let BaseSuspenseProps { children, fallback } = (*ctx.props()).clone(); let children = VNode::VList(::std::rc::Rc::new( crate::virtual_dom::VList::with_children(vec![children], None), )); match fallback { Some(fallback) => { let vsuspense = VSuspense::new( children, fallback, !self.suspensions.is_empty(), None, ); VNode::from(vsuspense) } None => children, } }
  • suspensions非空(仍有挂起未完成)时,构造VSuspense(虚拟节点),展示 fallback
  • suspensions为空时,展示真实 children
  • 若调用方未提供 fallback,则直接渲染 children(退化为无 fallback 行为)。

7.4rendered:挂起结束后延迟执行子组件渲染回调

csr特性下,BaseSuspense还维护了pending_rendered队列。源码注释(component.rs)解释了原因:当子组件在 Suspense 仍处于挂起时(因为其他兄弟挂起未完成)就已经恢复,它们的rendered生命周期会被延迟,直到整个 Suspense 解除挂起、DOM 真正移入活动树后才通过p.schedule(comp_id)触发,从而保证use_effect等副作用只在元素进入真实 DOM 后执行。这也说明Suspense 边界可以同时挂起多个后代:任意一个未完成都会使整体停留在 fallback。

八、自定义 suspending hook 的通用范式总结

综合示例中的use_sleep与官方的use_future/use_future_with,可以提炼出编写 suspending hook 的三步通用范式:

  1. 创建Suspension:用Suspension::from_future(async { ... })包装异步任务,任务完成时自动恢复;或使用Suspension::new()SuspensionHandle手动控制恢复时机。
  2. 返回SuspensionResult<T>:若suspension.resumed()为真则返回Ok(结果),否则返回Err(suspension.clone())。这样组件里用hook()?一行即可上抛挂起。
  3. 把挂起绑定到组件状态:如需"重新挂起",把Suspension放入 reducer/state,通过 dispatch 触发重渲染,重渲染时新Suspension尚未恢复即再次Err

由此得到的收益:

  • 声明式异步 UI:异步就绪前自动展示 fallback,无需手动管理 loading 标志;
  • 状态与渲染解耦:挂起只影响渲染过程,use_state/struct 组件字段等状态在恢复后原样保留;
  • 可组合性:多个挂起后代可共存于同一Suspense边界,全部就绪后一次性切换;
  • 函数式与结构式统一:struct 组件可通过泛型包装(如WithSleep<Comp>)复用同一套 suspense 逻辑。

九、结语

本示例虽然页面简单,却完整覆盖了 Yew Suspense 的三大层次:用户 API<Suspense fallback={...}>)、自定义 hookuse_sleep返回SuspensionResult)、以及底层机制BaseSuspenseSuspend/Resume消息循环与VSuspense虚拟节点)。如果你想进一步深入:

  • 阅读 packages/yew/src/suspense/component.rs 了解 Suspense 边界组件完整生命周期;
  • 阅读 packages/yew/src/suspense/suspension.rs 了解Suspension/SuspensionHandle的精细控制;
  • 阅读 packages/yew/src/suspense/hooks.rs 了解官方use_future/use_future_with的依赖更新语义;
  • 参考仓库测试 packages/yew/tests/suspense.rs 了解 Suspense 在测试环境中的行为断言。

将示例跑起来后,试着把use_sleep换成use_future(|| async { ... })请求真实数据,即可立刻体会到 Suspense 在异步数据加载场景下的价值。

【免费下载链接】yewRust / Wasm framework for creating reliable and efficient web applications项目地址: https://gitcode.com/gh_mirrors/ye/yew

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询