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

使用 Effect 响应变化

Responding to Changes with Effects

到目前为止,我们还没有提到响应式系统的另一半:effect。

We’ve made it this far without having mentioned half of the reactive system: effects.

响应式系统由两部分组成:更新单个响应式值(“信号”,signals)会通知依赖它们的各段代码(“副作用”,effects)需要再次运行。响应式系统的这两部分是相互依赖的。如果没有 effect,信号可以在响应式系统内部改变,但永远无法以与外部世界交互的方式被观察到。如果没有信号,effect 只会运行一次而不会再次运行,因为没有可订阅的可观察值。Effect 从字面上看就是响应式系统的“副作用”:它们的存在是为了将响应式系统与外部的非响应式世界同步。

Reactivity works in two halves: updating individual reactive values (“signals”) notifies the pieces of code that depend on them (“effects”) that they need to run again. These two halves of the reactive system are inter-dependent. Without effects, signals can change within the reactive system but never be observed in a way that interacts with the outside world. Without signals, effects run once but never again, as there’s no observable value to subscribe to. Effects are quite literally “side effects” of the reactive system: they exist to synchronize the reactive system with the non-reactive world outside it.

渲染器使用 effect 来响应信号的变化并更新 DOM 的部分内容。你可以创建自己的 effect,以其他方式将响应式系统与外部世界同步。

The renderer uses effects to update parts of the DOM in response to changes in signals. You can create your own effects to synchronize the reactive system with the outside world in other ways.

Effect::new 接受一个函数作为参数。它会在响应式系统的下一个“tick”运行该函数。(例如,如果你在组件中使用它,它将在该组件渲染后运行。)如果你在该函数内部访问任何响应式信号,它会记录该 effect 依赖于该信号这一事实。每当 effect 依赖的其中一个信号发生变化时,effect 就会再次运行。

Effect::new takes a function as its argument. It runs this function on the next “tick” of the reactive system. (So for example, if you use it in a component, it will run just after that component has been rendered.) If you access any reactive signal inside that function, it registers the fact that the effect depends on that signal. Whenever one of the signals that the effect depends on changes, the effect runs again.

let (a, set_a) = signal(0);
let (b, set_b) = signal(0);

Effect::new(move |_| {
  // 立即打印 "Value: 0" 并订阅 `a`
  // immediately prints "Value: 0" and subscribes to `a`
  logging::log!("Value: {}", a.get());
});

Effect 函数调用时带有一个参数,该参数包含它上次运行时返回的任何值。在初始运行时,此值为 None

The effect function is called with an argument containing whatever value it returned the last time it ran. On the initial run, this is None.

默认情况下,effect 不在服务器上运行。这意味着你可以在 effect 函数中调用浏览器特有的 API 而不会引起问题。如果你需要 effect 在服务器上运行,请使用 Effect::new_isomorphic

By default, effects do not run on the server. This means you can call browser-specific APIs within the effect function without causing issues. If you need an effect to run on the server, use Effect::new_isomorphic.

自动追踪和动态依赖

Auto-tracking and Dynamic Dependencies

如果你熟悉像 React 这样的框架,你可能会注意到一个关键的区别。React 及类似框架通常要求你传递一个“依赖数组”,即一组显式变量,用于决定 effect 何时重新运行。

If you’re familiar with a framework like React, you might notice one key difference. React and similar frameworks typically require you to pass a “dependency array,” an explicit set of variables that determine when the effect should rerun.

由于 Leptos 源自同步响应式编程的传统,我们不需要这个显式依赖列表。相反,我们会根据 effect 内部访问了哪些信号来自动追踪依赖关系。

Because Leptos comes from the tradition of synchronous reactive programming, we don’t need this explicit dependency list. Instead, we automatically track dependencies depending on which signals are accessed within the effect.

这有两个效果(双关语,非本意)。依赖关系是:

This has two effects (no pun intended). Dependencies are:

  1. 自动的:你不需要维护依赖列表,也不必担心应该包含或不包含什么。框架会简单地追踪哪些信号可能导致 effect 重新运行,并为你处理好一切。

  2. Automatic: You don’t need to maintain a dependency list, or worry about what should or shouldn’t be included. The framework simply tracks which signals might cause the effect to rerun, and handles it for you.

  3. 动态的:依赖列表在每次 effect 运行时都会被清除并更新。例如,如果你的 effect 包含一个条件语句,那么只有当前分支中使用的信号会被追踪。这意味着 effect 重新运行的次数是最少的。

  4. Dynamic: The dependency list is cleared and updated every time the effect runs. If your effect contains a conditional (for example), only signals that are used in the current branch are tracked. This means that effects rerun the absolute minimum number of times.

如果这听起来像魔法,并且你想深入了解自动依赖追踪的工作原理,请观看此视频。(抱歉音量较低!)

If this sounds like magic, and if you want a deep dive into how automatic dependency tracking works, check out this video. (Apologies for the low volume!)

Effect 作为类零成本抽象

Effects as Zero-Cost-ish Abstraction

虽然在最严格的技术意义上它们不是“零成本抽象”——它们需要一些额外的内存使用,存在于运行时等——但在更高层次上,从你在其中进行的任何昂贵的 API 调用或其他工作的角度来看,effect 是一种零成本抽象。根据你的描述,它们重新运行的次数绝对是必要的最小值。

While they’re not a “zero-cost abstraction” in the most technical sense—they require some additional memory use, exist at runtime, etc.—at a higher level, from the perspective of whatever expensive API calls or other work you’re doing within them, effects are a zero-cost abstraction. They rerun the absolute minimum number of times necessary, given how you’ve described them.

假设我正在开发某种聊天软件,我希望人们能够显示他们的全名或仅显示名,并在名字更改时通知服务器:

Imagine that I’m creating some kind of chat software, and I want people to be able to display their full name, or just their first name, and to notify the server whenever their name changes:

let (first, set_first) = signal(String::new());
let (last, set_last) = signal(String::new());
let (use_last, set_use_last) = signal(true);

// 每当源信号之一发生变化时,这都会将名称添加到日志中
// this will add the name to the log
// any time one of the source signals changes
Effect::new(move |_| {
    logging::log!(
        "{}", if use_last.get() {
            format!("{} {}", first.get(), last.get())
        } else {
            first.get()
        },
    )
});

如果 use_lasttrue,则每当 firstlastuse_last 更改时,effect 都会重新运行。但如果我将 use_last 切换为 false,则 last 的更改将永远不会导致全名更改。事实上,last 将从依赖列表中移除,直到 use_last 再次切换。这避免了在 use_last 仍为 false 时,如果我多次更改 last,而向 API 发送多次不必要的请求。

If use_last is true, effect should rerun whenever first, last, or use_last changes. But if I toggle use_last to false, a change in last will never cause the full name to change. In fact, last will be removed from the dependency list until use_last toggles again. This saves us from sending multiple unnecessary requests to the API if I change last multiple times while use_last is still false.

创建 Effect,还是不创建 Effect?

To create an effect, or not to create an effect?

Effect 旨在将响应式系统与外部的非响应式世界同步,而不是在不同的响应式值之间进行同步。换句话说:使用 effect 从一个信号中读取值并将其设置到另一个信号中总是次优的。

Effects are intended to synchronize the reactive system with the non-reactive world outside, not to synchronize between different reactive values. In other words: using an effect to read a value from one signal and set it in another is always sub-optimal.

如果你需要定义一个依赖于其他信号值的信号,请使用派生信号或 Memo。在 effect 内部写入信号并不是世界末日,也不会让你的电脑着火,但派生信号或 memo 总是更好——不仅因为数据流清晰,而且因为性能更好。

If you need to define a signal that depends on the value of other signals, use a derived signal or a Memo. Writing to a signal inside an effect isn’t the end of the world, and it won’t cause your computer to light on fire, but a derived signal or memo is always better—not only because the dataflow is clear, but because the performance is better.

let (a, set_a) = signal(0);

// ⚠️ 不太好
// ⚠️ not great
let (b, set_b) = signal(0);
Effect::new(move |_| {
    set_b.set(a.get() * 2);
});

// ✅ 很好!
// ✅ woo-hoo!
let b = move || a.get() * 2;

如果你需要将某些响应式值与外部非响应式世界(如 Web API、控制台、文件系统或 DOM)同步,那么在 effect 中写入信号是可行的方法。但在许多情况下,你会发现你实际上是在事件监听器或某些其他地方写入信号,而不是在 effect 内部。在这种情况下,你应该查看 leptos-use,看看它是否已经提供了执行该操作的响应式封装原语!

If you need to synchronize some reactive value with the non-reactive world outside—like a web API, the console, the filesystem, or the DOM—writing to a signal in an effect is a fine way to do that. In many cases, though, you’ll find that you’re really writing to a signal inside an event listener or something else, not inside an effect. In these cases, you should check out leptos-use to see if it already provides a reactive wrapping primitive to do that!

如果你想了解更多关于何时应该以及不应该使用 create_effect 的信息,请观看此视频进行更深入的思考!

If you’re curious for more information about when you should and shouldn’t use create_effect, check out this video for a more in-depth consideration!

Effect 与渲染

Effects and Rendering

到目前为止,我们一直没有提到 effect,是因为它们已经内置在 Leptos DOM 渲染器中。我们已经看到,你可以创建一个信号并将其传递给 view 宏,每当信号发生变化时,它都会更新相关的 DOM 节点:

We’ve managed to get this far without mentioning effects because they’re built into the Leptos DOM renderer. We’ve seen that you can create a signal and pass it into the view macro, and it will update the relevant DOM node whenever the signal changes:

let (count, set_count) = signal(0);

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

之所以有效,是因为框架本质上创建了一个包装此更新的 effect。你可以想象 Leptos 将此视图转换为类似以下的内容:

This works because the framework essentially creates an effect wrapping this update. You can imagine Leptos translating this view into something like this:

let (count, set_count) = signal(0);

// 创建一个 DOM 元素
// create a DOM element
let document = leptos::document();
let p = document.create_element("p").unwrap();

// 创建一个 effect 来响应式地更新文本
// create an effect to reactively update the text
Effect::new(move |prev_value| {
    // 首先,访问信号的值并将其转换为字符串
    // first, access the signal’s value and convert it to a string
    let text = count.get().to_string();

    // 如果这与之前的值不同,则更新节点
    // if this is different from the previous value, update the node
    if prev_value != Some(text) {
        p.set_text_content(&text);
    }

    // 返回此值,以便我们可以 memoize 下一次更新
    // return this value so we can memoize the next update
    text
});

每当 count 更新时,此 effect 都会重新运行。这就是允许对 DOM 进行响应式、细粒度更新的原因。

Every time count is updated, this effect will rerun. This is what allows reactive, fine-grained updates to the DOM.

使用 Effect::watch() 进行显式追踪

Explicit Tracking with Effect::watch()

除了 Effect::new() 之外,Leptos 还提供了一个 Effect::watch() 函数,可以通过显式传入一组要追踪的值来分离追踪和响应更改。

In addition to Effect::new(), Leptos provides an Effect::watch() function, which can be used to separate tracking and responding to changes by explicitly passing in a set of values to track.

watch 接受三个参数。dependency_fn 参数是被响应式追踪的,而 handlerimmediate 则不是。每当 dependency_fn 发生变化时,handler 就会运行。如果 immediate 为 false,则 handler 仅在检测到 dependency_fn 中访问的任何信号发生第一次更改后运行。watch 返回一个 Effect,可以调用 .stop() 来停止追踪依赖关系。

watch takes three arguments. The dependency_fn argument is reactively tracked while handler and immediate are not. Whenever dependency_fn is changed, handler is run. If immediate is false, the handler will run only after the first change is detected of any signal that is accessed in dependency_fn. watch returns an Effect, which can be called with .stop() to stop tracking the dependencies.

let (num, set_num) = signal(0);

let effect = Effect::watch(
    move || num.get(),
    move |num, prev_num, _| {
        leptos::logging::log!("Number: {}; Prev: {:?}", num, prev_num);
    },
    false,
);

set_num.set(1); // > "Number: 1; Prev: Some(0)"

effect.stop(); // 停止监听
// stop watching

set_num.set(2); // (没有任何反应)
// (nothing happens)

在线示例

点击打开 CodeSandbox。

Click to open CodeSandbox.

CodeSandbox 源码 CodeSandbox Source
use leptos::html::Input;
use leptos::prelude::*;

#[derive(Copy, Clone)]
struct LogContext(RwSignal<Vec<String>>);

#[component]
fn App() -> impl IntoView {
    // 只是在这里做一个可见的日志
    // Just making a visible log here
    // 你可以忽略这个...
    // You can ignore this...
    let log = RwSignal::<Vec<String>>::new(vec![]);
    let logged = move || log.get().join("\n");

    // 在这里,newtype 模式不是 *必需的*,但这是一个好的做法
    // the newtype pattern isn't *necessary* here but is a good practice
    // 它可以避免与其他未来可能出现的 RwSignal<Vec<String>> 上下文混淆
    // it avoids confusion with other possible future `RwSignal<Vec<String>>` contexts
    // 并使引用它变得更容易
    // and makes it easier to refer to it
    provide_context(LogContext(log));

    view! {
        <CreateAnEffect/>
        <pre>{logged}</pre>
    }
}

#[component]
fn CreateAnEffect() -> impl IntoView {
    let (first, set_first) = signal(String::new());
    let (last, set_last) = signal(String::new());
    let (use_last, set_use_last) = signal(true);

    // 这会将名称添加到日志中
    // this will add the name to the log
    // 每当源信号之一发生变化时
    // any time one of the source signals changes
    Effect::new(move |_| {
        log(if use_last.get() {
            let first = first.read();
            let last = last.read();
            format!("{first} {last}")
        } else {
            first.get()
        })
    });

    view! {
        <h1>
            <code>"create_effect"</code>
            " Version"
        </h1>
        <form>
            <label>
                "First Name"
                <input
                    type="text"
                    name="first"
                    prop:value=first
                    on:change:target=move |ev| set_first.set(ev.target().value())
                />
            </label>
            <label>
                "Last Name"
                <input
                    type="text"
                    name="last"
                    prop:value=last
                    on:change:target=move |ev| set_last.set(ev.target().value())
                />
            </label>
            <label>
                "Show Last Name"
                <input
                    type="checkbox"
                    name="use_last"
                    prop:checked=use_last
                    on:change:target=move |ev| set_use_last.set(ev.target().checked())
                />
            </label>
        </form>
    }
}

#[component]
fn ManualVersion() -> impl IntoView {
    let first = NodeRef::<Input>::new();
    let last = NodeRef::<Input>::new();
    let use_last = NodeRef::<Input>::new();

    let mut prev_name = String::new();
    let on_change = move |_| {
        log("      listener");
        let first = first.get().unwrap();
        let last = last.get().unwrap();
        let use_last = use_last.get().unwrap();
        let this_one = if use_last.checked() {
            format!("{} {}", first.value(), last.value())
        } else {
            first.value()
        };

        if this_one != prev_name {
            log(&this_one);
            prev_name = this_one;
        }
    };

    view! {
        <h1>"Manual Version"</h1>
        <form on:change=on_change>
            <label>"First Name" <input type="text" name="first" node_ref=first/></label>
            <label>"Last Name" <input type="text" name="last" node_ref=last/></label>
            <label>
                "Show Last Name" <input type="checkbox" name="use_last" checked node_ref=use_last/>
            </label>
        </form>
    }
}

fn log(msg: impl std::fmt::Display) {
    let log = use_context::<LogContext>().unwrap().0;
    log.update(|log| log.push(msg.to_string()));
}

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