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

控制流

Control Flow

在大多数应用程序中,你有时需要做出决定:我应该渲染视图的这一部分吗?我应该渲染 <ButtonA/> 还是 <WidgetB/>?这就是控制流

In most applications, you sometimes need to make a decision: Should I render this part of the view, or not? Should I render <ButtonA/> or <WidgetB/>? This is control flow.

几点提示

A Few Tips

当思考如何使用 Leptos 实现这一点时,记住以下几点很重要:

When thinking about how to do this with Leptos, it’s important to remember a few things:

  1. Rust 是一门面向表达式的语言:控制流表达式(如 if x() { y } else { z }match x() { ... })会返回它们的值。这使得它们在声明式用户界面中非常有用。

  2. 对于任何实现了 IntoView 的类型 T——换句话说,对于 Leptos 知道如何渲染的任何类型——Option<T>Result<T, impl Error> 实现了 IntoView。就像 Fn() -> T 渲染一个响应式的 T 一样,Fn() -> Option<T>Fn() -> Result<T, impl Error> 也是响应式的。

  3. Rust 有许多方便的辅助方法,如 Option::mapOption::and_thenOption::ok_orResult::mapResult::okbool::then,它们允许你以声明式的方式在几种不同的标准类型之间进行转换,而所有这些类型都是可以渲染的。花时间研读 OptionResult 的文档尤其是提升 Rust 水平的最佳途径之一。

  4. 永远记住:为了保持响应性,值必须是函数。在下面,你会看到我不断地将内容包裹在 move || 闭包中。这是为了确保当它们依赖的信号发生变化时,它们能够真正地重新运行,从而保持 UI 的响应性。

  5. Rust is an expression-oriented language: control-flow expressions like if x() { y } else { z } and match x() { ... } return their values. This makes them very useful for declarative user interfaces.

  6. For any T that implements IntoView—in other words, for any type that Leptos knows how to render—Option<T> and Result<T, impl Error> also implement IntoView. And just as Fn() -> T renders a reactive T, Fn() -> Option<T> and Fn() -> Result<T, impl Error> are reactive.

  7. Rust has lots of handy helpers like Option::map, Option::and_then, Option::ok_or, Result::map, Result::ok, and bool::then that allow you to convert, in a declarative way, between a few different standard types, all of which can be rendered. Spending time in the Option and Result docs in particular is one of the best ways to level up your Rust game.

  8. And always remember: to be reactive, values must be functions. You’ll see me constantly wrap things in a move || closure, below. This is to ensure that they actually rerun when the signal they depend on changes, keeping the UI reactive.

那又怎样?

So What?

简单串联一下这些知识点:这意味着你实际上可以用原生的 Rust 代码来实现大部分控制流,而不需要任何专门的控制流组件或特殊知识。

To connect the dots a little: this means that you can actually implement most of your control flow with native Rust code, without any control-flow components or special knowledge.

例如,让我们从一个简单的信号和派生信号开始:

For example, let’s start with a simple signal and derived signal:

let (value, set_value) = signal(0);
let is_odd = move || value.get() % 2 != 0;

我们可以使用这些信号和普通的 Rust 来构建大部分控制流。

We can use these signals and ordinary Rust to build most control flow.

if 语句

if statements

假设如果数字是奇数,我想渲染一些文本,如果是偶数,则渲染另一些文本。那么,这样写怎么样?

Let’s say I want to render some text if the number is odd, and some other text if it’s even. Well, how about this?

view! {
    <p>
        {move || if is_odd() {
            "Odd"
        } else {
            "Even"
        }}
    </p>
}

一个 if 表达式会返回它的值,而 &str 实现了 IntoView,所以 Fn() -> &str 也实现了 IntoView,所以这段代码……可以正常工作!

An if expression returns its value, and a &str implements IntoView, so a Fn() -> &str implements IntoView, so this... just works!

Option<T>

Option<T>

假设如果数字是奇数,我们想渲染一些文本,如果是偶数,则什么都不渲染。

Let’s say we want to render some text if it’s odd, and nothing if it’s even.

let message = move || {
    if is_odd() {
        Some("Ding ding ding!")
    } else {
        None
    }
};

view! {
    <p>{message}</p>
}

这运行得很好。如果我们愿意,可以使用 bool::then() 让它变得更短一些。

This works fine. We can make it a little shorter if we’d like, using bool::then().

let message = move || is_odd().then(|| "Ding ding ding!");
view! {
    <p>{message}</p>
}

如果你愿意,你甚至可以内联这段代码,尽管个人而言,我有时更喜欢把逻辑抽离到 view 之外,以获得更好的 cargo fmtrust-analyzer 支持。

You could even inline this if you’d like, although personally I sometimes like the better cargo fmt and rust-analyzer support I get by pulling things out of the view.

match 语句

match statements

我们仍然只是在写普通的 Rust 代码,对吧?所以你可以随心所欲地使用 Rust 模式匹配的所有强大功能。

We’re still just writing ordinary Rust code, right? So you have all the power of Rust’s pattern matching at your disposal.

let message = move || {
    match value.get() {
        0 => "Zero",
        1 => "One",
        n if is_odd() => "Odd",
        _ => "Even"
    }
};
view! {
    <p>{message}</p>
}

何乐而不为呢?尽情发挥吧,对吧?

And why not? YOLO, right?

防止过度渲染

Preventing Over-Rendering

不要太放飞自我。

Not so YOLO.

到目前为止,我们所做的一切基本上都没问题。但有一件事你应该记住并尽量小心。目前为止我们创建的每一个控制流函数基本上都是一个派生信号:它在每次值发生变化时都会重新运行。在上面的例子中,值在每次变化时都会在奇偶之间切换,所以这是可以的。

Everything we’ve just done is basically fine. But there’s one thing you should remember and try to be careful with. Each one of the control-flow functions we’ve created so far is basically a derived signal: it will rerun every time the value changes. In the examples above, where the value switches from even to odd on every change, this is fine.

但考虑以下例子:

But consider the following example:

let (value, set_value) = signal(0);

let message = move || if value.get() > 5 {
    "Big"
} else {
    "Small"
};

view! {
    <p>{message}</p>
}

这段代码当然可以运行。但如果你加上一条日志,你可能会感到惊讶:

This works, for sure. But if you added a log, you might be surprised

let message = move || if value.get() > 5 {
    logging::log!("{}: rendering Big", value.get());
    "Big"
} else {
    logging::log!("{}: rendering Small", value.get());
    "Small"
};

当用户反复点击按钮增加 value 时,你会看到类似这样的内容:

As a user repeatedly clicks a button incrementing value, you’d see something like this:

1: rendering Small
2: rendering Small
3: rendering Small
4: rendering Small
5: rendering Small
6: rendering Big
7: rendering Big
8: rendering Big
... 以此类推,直到永远

每当 value 发生变化,它都会重新运行 if 语句。考虑到响应式的工作原理,这是合理的。但它也有一个缺点。对于一个简单的文本节点,重新运行 if 语句并重新渲染并不是什么大不了的事。但想象一下如果是这样:

Every time value changes, it reruns the if statement. This makes sense, with how reactivity works. But it has a downside. For a simple text node, rerunning the if statement and rerendering isn’t a big deal. But imagine it were like this:

let message = move || if value.get() > 5 {
    <Big/>
} else {
    <Small/>
};

这将重新渲染 <Small/> 五次,然后重新渲染 <Big/> 无数次。如果它们正在加载资源、创建信号,甚至只是创建 DOM 节点,这都是不必要的开销。

This rerenders <Small/> five times, then <Big/> infinitely. If they’re loading resources, creating signals, or even just creating DOM nodes, this is unnecessary work.

<Show/>

<Show/>

<Show/> 组件就是答案。你给它传递一个 when 条件函数,一个在 when 函数返回 false 时显示的回退(fallback),以及在 whentrue 时渲染的子组件。

The <Show/> component is the answer. You pass it a when condition function, a fallback to be shown if the when function returns false, and children to be rendered if when is true.

let (value, set_value) = signal(0);

view! {
  <Show
    when=move || { value.get() > 5 }
    fallback=|| view! { <Small/> }
  >
    <Big/>
  </Show>
}

<Show/> 会对 when 条件进行记忆化(memoize),所以它只渲染一次 <Small/>,并持续显示同一个组件,直到 value 大于五;然后它渲染一次 <Big/>,并持续显示它直到永远,或者直到 value 低于五,届时它会再次渲染 <Small/>

<Show/> memoizes the when condition, so it only renders its <Small/> once, continuing to show the same component until value is greater than five; then it renders <Big/> once, continuing to show it indefinitely or until value goes below five and then renders <Small/> again.

这是在使用动态 if 表达式时避免过度渲染的一个有用工具。一如既往,这存在一些开销:对于一个非常简单的节点(如更新单个文本节点,或更新类名或属性),使用 move || if ... 会更高效。但如果渲染任何一个分支的开销稍微大一点,请务必使用 <Show/>

This is a helpful tool to avoid rerendering when using dynamic if expressions. As always, there's some overhead: for a very simple node (like updating a single text node, or updating a class or attribute), a move || if ... will be more efficient. But if it’s at all expensive to render either branch, reach for <Show/>.

注意:类型转换

Note: Type Conversions

本节最后有一件重要的事情需要说明。

There’s one final thing it’s important to say in this section.

Leptos 使用静态类型的视图树。view 宏为不同种类的视图返回不同的类型。

Leptos uses a statically-typed view tree. The view macro returns different types for different kinds of view.

以下代码无法编译,因为不同的 HTML 元素属于不同的类型。

This won’t compile, because the different HTML elements are different types.

view! {
    <main>
        {move || match is_odd() {
            true if value.get() == 1 => {
                view! { <pre>"One"</pre> }
            },
            false if value.get() == 2 => {
                view! { <p>"Two"</p> }
            }
            // 返回 HtmlElement<Textarea>
            // returns HtmlElement<Textarea>
            _ => view! { <textarea>{value.get()}</textarea> }
        }}
    </main>
}

这种强类型非常强大,因为它能够实现各种编译时优化。但在这种条件逻辑中可能会有点烦人,因为在 Rust 中,你不能从一个条件分支的不同分支返回不同的类型。有两种方法可以摆脱这种困境:

This strong typing is very powerful, because it enables all sorts of compile-time optimizations. But it can be a little annoying in conditional logic like this, because you can’t return different types from different branches of a condition in Rust. There are two ways to get yourself out of this situation:

  1. 使用枚举 Either(以及 EitherOf3EitherOf4 等)将不同的类型转换为相同的类型。

  2. 使用 .into_any() 将多种类型转换为一种类型擦除后的 AnyView

  3. Use the enum Either (and EitherOf3, EitherOf4, etc.) to convert the different types to the same type.

  4. Use .into_any() to convert multiple types into one typed-erased AnyView.

这是同一个例子,加上了转换逻辑:

Here’s the same example, with the conversion added:

view! {
    <main>
        {move || match is_odd() {
            true if value.get() == 1 => {
                // 返回 HtmlElement<Pre>
                // returns HtmlElement<Pre>
                view! { <pre>"One"</pre> }.into_any()
            },
            false if value.get() == 2 => {
                // 返回 HtmlElement<P>
                // returns HtmlElement<P>
                view! { <p>"Two"</p> }.into_any()
            }
            // 返回 HtmlElement<Textarea>
            // returns HtmlElement<Textarea>
            _ => view! { <textarea>{value.get()}</textarea> }.into_any()
        }}
    </main>
}

在线示例

点击打开 CodeSandbox。

Click to open CodeSandbox.

CodeSandbox 源代码 CodeSandbox Source
use leptos::prelude::*;

#[component]
fn App() -> impl IntoView {
    let (value, set_value) = signal(0);
    let is_odd = move || value.get() & 1 == 1;
    let odd_text = move || if is_odd() {
        Some("How odd!")
    } else {
        None
    };

    view! {
        <h1>"Control Flow"</h1>

        // 用于更新和显示值的简单 UI
        // Simple UI to update and show a value
        <button on:click=move |_| *set_value.write() += 1>
            "+1"
        </button>
        <p>"Value is: " {value}</p>

        <hr/>

        <h2><code>"Option<T>"</code></h2>
        // 对于任何实现了 IntoView 的 T,
        // Option<T> 同样实现了 IntoView
        // For any `T` that implements `IntoView`,
        // so does `Option<T>`

        <p>{odd_text}</p>
        // 这意味着你可以在其上使用 Option 的方法
        // This means you can use `Option` methods on it
        <p>{move || odd_text().map(|text| text.len())}</p>

        <h2>"Conditional Logic"</h2>
        // 你可以通过几种方式实现动态的 conditional if-then-else 逻辑
        // You can do dynamic conditional if-then-else
        // logic in several ways
        //
        // a. 函数中的 "if" 表达式
        //    这将在每次值发生变化时简单地重新渲染,
        //    这使得它适用于轻量级 UI
        // a. An "if" expression in a function
        //    This will simply re-render every time the value
        //    changes, which makes it good for lightweight UI
        <p>
            {move || if is_odd() {
                "Odd"
            } else {
                "Even"
            }}
        </p>

        // b. 切换某种 class
        //    对于会被频繁切换的元素,这很明智,
        //    因为它不会在状态切换期间销毁元素
        //    (你可以在 index.html 中找到 `hidden` 类)
        // b. Toggling some kind of class
        //    This is smart for an element that's going to
        //    toggled often, because it doesn't destroy
        //    it in between states
        //    (you can find the `hidden` class in `index.html`)
        <p class:hidden=is_odd>"Appears if even."</p>

        // c. <Show/> 组件
        //    这只会懒加载渲染一次 fallback 和子组件,
        //    并在需要时在它们之间切换。
        //    在许多情况下,这比 {move || if ...} 块更高效
        // c. The <Show/> component
        //    This only renders the fallback and the child
        //    once, lazily, and toggles between them when
        //    needed. This makes it more efficient in many cases
        //    than a {move || if ...} block
        <Show when=is_odd
            fallback=|| view! { <p>"Even steven"</p> }
        >
            <p>"Oddment"</p>
        </Show>

        // d. 因为 `bool::then()` 将 `bool` 转换为 `Option`,
        //    你可以使用它来创建一个显示/隐藏切换
        // d. Because `bool::then()` converts a `bool` to
        //    `Option`, you can use it to create a show/hide toggled
        {move || is_odd().then(|| view! { <p>"Oddity!"</p> })}

        <h2>"Converting between Types"</h2>
        // e. 注意:如果分支返回不同的类型,
        //    你可以使用 `.into_any()` 或使用 `Either` 枚举
        //    (`Either`、`EitherOf3`、`EitherOf4` 等)在它们之间进行转换
        // e. Note: if branches return different types,
        //    you can convert between them with
        //    `.into_any()` or using the `Either` enums
        //    (`Either`, `EitherOf3`, `EitherOf4`, etc.)
        {move || match is_odd() {
            true if value.get() == 1 => {
                // <pre> 返回 HtmlElement<Pre>
                // <pre> returns HtmlElement<Pre>
                view! { <pre>"One"</pre> }.into_any()
            },
            false if value.get() == 2 => {
                // <p> 返回 HtmlElement<P>
                // 所以我们将其转换为更通用的类型
                // <p> returns HtmlElement<P>
                // so we convert into a more generic type
                view! { <p>"Two"</p> }.into_any()
            }
            _ => view! { <textarea>{value.get()}</textarea> }.into_any()
        }}
    }
}

fn main() {
    leptos::mount::mount_to_body(App)
}