Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

插曲:响应式与函数

Interlude: Reactivity and Functions

我们的一位核心贡献者最近对我说道:“在开始使用 Leptos 之前,我从未如此频繁地使用闭包。”事实确实如此。闭包是任何 Leptos 应用程序的核心。有时它看起来甚至有点滑稽:

One of our core contributors said to me recently: “I never used closures this often until I started using Leptos.” And it’s true. Closures are at the heart of any Leptos application. It sometimes looks a little silly:

// 信号持有一个值,并且可以被更新
// a signal holds a value, and can be updated
let (count, set_count) = signal(0);

// 派生信号是一个访问其他信号的函数
// a derived signal is a function that accesses other signals
let double_count = move || count.get() * 2;
let count_is_odd = move || count.get() & 1 == 1;
let text = move || if count_is_odd() {
    "odd"
} else {
    "even"
};

// 副作用(effect)自动追踪它所依赖的信号
// an effect automatically tracks the signals it depends on
// 并在它们改变时重新运行
// and reruns when they change
Effect::new(move |_| {
    logging::log!("text = {}", text());
});

view! {
    <p>{move || text().to_uppercase()}</p>
}

到处都是闭包,到处都是!

Closures, closures everywhere!

但这是为什么呢?

But why?

函数与 UI 框架

Functions and UI Frameworks

函数是每个 UI 框架的核心。这完全合情合理。创建一个用户界面基本上分为两个阶段:

Functions are at the heart of every UI framework. And this makes perfect sense. Creating a user interface is basically divided into two phases:

  1. 初始渲染

  2. initial rendering

  3. 更新

  4. updates

在 Web 框架中,框架会进行某种初始渲染。然后它将控制权交还给浏览器。当某些事件触发(如鼠标点击)或异步任务完成(如 HTTP 请求结束)时,浏览器会重新唤醒框架以更新某些内容。框架运行某种代码来更新你的用户界面,然后继续进入睡眠状态,直到浏览器再次唤醒它。

In a web framework, the framework does some kind of initial rendering. Then it hands control back over to the browser. When certain events fire (like a mouse click) or asynchronous tasks finish (like an HTTP request finishing), the browser wakes the framework back up to update something. The framework runs some kind of code to update your user interface, and goes back asleep until the browser wakes it up again.

这里的关键短语是“运行某种代码”。在 Rust 或任何其他编程语言中,在任意时间点“运行某种代码”的自然方式就是调用一个函数。事实上,每个 UI 框架都是基于一遍又一遍地重新运行某种函数的:

The key phrase here is “runs some kind of code.” The natural way to “run some kind of code” at an arbitrary point in time—in Rust or in any other programming language—is to call a function. And in fact every UI framework is based on rerunning some kind of function over and over:

  1. 虚拟 DOM(VDOM)框架,如 React、Yew 或 Dioxus,会反复运行组件或渲染函数,以生成一个虚拟 DOM 树,该树可以与之前的结果进行比对(reconcile),从而对 DOM 进行补丁式更新

  2. virtual DOM (VDOM) frameworks like React, Yew, or Dioxus rerun a component or render function over and over, to generate a virtual DOM tree that can be reconciled with the previous result to patch the DOM

  3. 编译型框架,如 Angular 和 Svelte,将你的组件模板分为“创建(create)”和“更新(update)”函数,当它们检测到组件状态发生变化时,会重新运行更新函数

  4. compiled frameworks like Angular and Svelte divide your component templates into “create” and “update” functions, rerunning the update function when they detect a change to the component’s state

  5. 在细粒度响应式框架(如 SolidJS、Sycamore 或 Leptos)中,由 来定义重新运行的函数

  6. in fine-grained reactive frameworks like SolidJS, Sycamore, or Leptos, you define the functions that rerun

这就是我们所有组件正在做的事情。

That’s what all our components are doing.

以我们最典型的 <SimpleCounter/> 示例的最简形式为例:

Take our typical <SimpleCounter/> example in its simplest form:

#[component]
pub fn SimpleCounter() -> impl IntoView {
    let (value, set_value) = signal(0);

    let increment = move |_| *set_value.write() += 1;

    view! {
        <button on:click=increment>
            {value}
        </button>
    }
}

SimpleCounter 函数本身只运行一次。value 信号只创建一次。框架将 increment 函数作为事件监听器交给浏览器。当你点击按钮时,浏览器调用 increment,它通过 set_value 更新 value。这接着更新了我们在视图中由 {value} 表示的单个文本节点。

The SimpleCounter function itself runs once. The value signal is created once. The framework hands off the increment function to the browser as an event listener. When you click the button, the browser calls increment, which updates value via set_value. And that updates the single text node represented in our view by {value}.

函数是响应式的关键。它们使框架能够根据变化重新运行应用程序中尽可能小的单位。

Functions are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in response to a change.

所以请记住两件事:

So remember two things:

  1. 你的组件函数是一个设置(setup)函数,而不是渲染函数:它只运行一次。

  2. Your component function is a setup function, not a render function: it only runs once.

  3. 为了使视图模板中的值具有响应性,它们必须是响应式函数:要么是信号,要么是捕获并读取信号的闭包。

  4. For values in your view template to be reactive, they must be reactive functions: either signals or closures that capture and read from signals.

这实际上是 Leptos 的 stable 版本和 nightly 版本之间的主要区别。如你所知,使用 nightly 编译器和 nightly 特性允许你直接像调用函数一样调用信号:因此,是 value() 而不是 value.get()

This is actually the primary difference between the stable and nightly versions of Leptos. As you may know, using the nightly compiler and the nightly feature allows you to call a signal directly, as a function: so, value() instead of value.get().

但这不仅仅是语法糖。它带来了一个极其一致的语义模型:响应式事物就是函数。通过调用函数来访问信号。要说“给我一个信号作为参数”,你可以接受任何 impl Fn() -> T 的类型。这种基于函数的接口在信号(signal)、备忘录(memo)和派生信号(derived signal)之间没有区别:它们中的任何一个都可以通过作为函数调用来访问。

But this isn’t just syntax sugar. It allows for an extremely consistent semantic model: Reactive things are functions. Signals are accessed by calling functions. To say “give me a signal as an argument” you can take anything that impl Fn() -> T. And this function-based interface makes no distinction between signals, memos, and derived signals: any of them can be accessed by calling them as functions.

遗憾的是,在像信号这样的任意结构体上实现 Fn trait 需要 nightly Rust,尽管这个特定的特性大多只是停滞不前,而且不太可能在短期内改变(或稳定)。出于各种原因,许多人避免使用 nightly。因此,随着时间的推移,我们已经将文档等内容的默认设置转向了 stable。不幸的是,这使得“信号就是函数”这一简单的思维模型变得不那么直观了。

Unfortunately implementing the Fn traits on arbitrary structs like signals requires nightly Rust, although this particular feature has mostly just languished and is not likely to change (or be stabilized) any time soon. Many people avoid nightly, for one reason or another. So, over time we’ve moved the defaults for things like documentation toward stable. Unfortunately, this makes the simple mental model of “signals are functions” a bit less straightforward.