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

使用信号

Working with Signals

到目前为止,我们已经看了一些使用 signal 的简单示例,它返回一个 ReadSignal 获取器(getter)和一个 WriteSignal 设置器(setter)。

So far we’ve used some simple examples of using signal, which returns a ReadSignal getter and a WriteSignal setter.

获取与设置

Getting and Setting

有一些基本的信号操作:

There are a few basic signal operations:

获取

Getting

  1. .read() 返回一个读取守卫(read guard),它可以解引用为信号的值,并响应式地追踪信号值的任何未来变化。请注意,在该守卫被丢弃之前,你不能更新信号的值,否则会导致运行时错误。

  2. .read() returns a read guard which dereferences to the value of the signal, and tracks any future changes to the value of the signal reactively. Note that you cannot update the value of the signal until this guard is dropped, or it will cause a runtime error.

  3. .with() 接受一个函数,该函数通过引用(&T)接收信号的当前值,并追踪该信号。

  4. .with() takes a function, which receives the current value of the signal by reference (&T), and tracks the signal.

  5. .get() 克隆信号的当前值,并追踪值的后续变化。

  6. .get() clones the current value of the signal and tracks further changes to the value.

.get() 是访问信号最常用的方法。.read() 对于那些接受不可变引用且不需要克隆值的方法非常有用(例如 my_vec_signal.read().len())。如果你需要对该引用进行更多操作,但又想确保不会持有锁超过所需的时间,那么 .with() 会非常有用。

.get() is the most common method of accessing a signal. .read() is useful for methods that take an immutable reference, without cloning the value (my_vec_signal.read().len()). .with() is useful if you need to do more with that reference, but want to make sure you don’t hold onto the lock longer than you need.

设置

Setting

  1. .write() 返回一个写入守卫(write guard),它是对信号值的可变引用,并通知所有订阅者它们需要更新。请注意,在丢弃此守卫之前,你不能读取信号的值,否则会导致运行时错误。

  2. .write() returns a write guard which is a mutable reference to the value of the signal, and notifies any subscribers that they need to update. Note that you cannot read from the value of the signal until this guard is dropped, or it will cause a runtime error.

  3. .update() 接受一个函数,该函数接收信号当前值的可变引用(&mut T),并通知订阅者。(.update() 不返回闭包返回的值,但如果你需要,可以使用 .try_update();例如,如果你正从 Vec<_> 中移除一个项并想要获取被移除的项。)

  4. .update() takes a function, which receives a mutable reference to the current value of the signal (&mut T), and notifies subscribers. (.update() doesn’t return the value returned by the closure, but you can use .try_update() if you need to; for example, if you’re removing an item from a Vec<_> and want the removed item.)

  5. .set() 替换信号的当前值并通知订阅者。

  6. .set() replaces the current value of the signal and notifies subscribers.

.set() 是设置新值最常用的方式;.write() 对于原地更新值非常有用。就像 .read().with() 的情况一样,当你想要避免持有写入锁的时间超过预期时,.update() 会非常有用。

.set() is most common for setting a new value; .write() is very useful for updating a value in place. Just as is the case with .read() and .with(), .update() can be useful when you want to avoid the possibility of holding on the write lock longer than you intended to.

这些 trait 基于 trait 组合,并由泛型实现(blanket implementations)提供。例如,Read 为任何实现了 TrackReadUntracked 的类型实现。With 为任何实现了 Read 的类型实现。Get 为任何实现了 WithClone 的类型实现。依此类推。

These traits are based on trait composition and provided by blanket implementations. For example, Read is implemented for any type that implements Track and ReadUntracked. With is implemented for any type that implements Read. Get is implemented for any type that implements With and Clone. And so on.

WriteUpdateSet 之间也存在类似的关系。

Similar relationships exist for Write, Update, and Set.

阅读文档时值得注意:如果你只看到 ReadUntrackedTrack 被实现,你仍然可以使用 .with().get()(如果 T: Clone)等等。

This is worth noting when reading docs: if you only see ReadUntracked and Track as implemented traits, you will still be able to use .with(), .get() (if T: Clone), and so on.

使用信号

Working with Signals

你可能会注意到,.get().set() 可以通过 .read().write(),或者 .with().update() 来实现。换句话说,count.get() 等同于 count.with(|n| n.clone())count.read().clone(),而 count.set(1) 是通过执行 count.update(|n| *n = 1)*count.write() = 1 来实现的。

You might notice that .get() and .set() can be implemented in terms of .read() and .write(), or .with() and .update(). In other words, count.get() is identical to count.with(|n| n.clone()) or count.read().clone(), and count.set(1) is implemented by doing count.update(|n| *n = 1) or *count.write() = 1.

当然,.get().set() 的语法更漂亮。

But of course, .get() and .set() are nicer syntax.

然而,其他方法也有一些非常好的使用场景。

However, there are some very good use cases for the other methods.

例如,考虑一个持有 Vec<String> 的信号。

For example, consider a signal that holds a Vec<String>.

let (names, set_names) = signal(Vec::new());
if names.get().is_empty() {
	set_names(vec!["Alice".to_string()]);
}

从逻辑上讲,这足够简单,但它隐藏了一些明显的效率问题。请记住 names.get().is_empty() 会克隆该值。这意味着我们会克隆整个 Vec<String>,运行 is_empty(),然后立即丢弃该克隆。

In terms of logic, this is simple enough, but it’s hiding some significant inefficiencies. Remember that names.get().is_empty() clones the value. This means we clone the whole Vec<String>, run is_empty(), and then immediately throw away the clone.

同样,set_names 用一个全新的 Vec<_> 替换了旧值。这没问题,但我们不如直接原地修改原始的 Vec<_>

Likewise, set_names replaces the value with a whole new Vec<_>. This is fine, but we might as well just mutate the original Vec<_> in place.

let (names, set_names) = signal(Vec::new());
if names.read().is_empty() {
	set_names.write().push("Alice".to_string());
}

现在我们的函数只需通过引用获取 names 来运行 is_empty(),避免了克隆,然后原地修改 Vec<_>

Now our function simply takes names by reference to run is_empty(), avoiding that clone, and then mutates the Vec<_> in place.

线程安全与线程局部值

Thread Safety and Thread-Local Values

你可能已经注意到,无论是通过阅读文档还是通过实验自己的应用程序,存储在信号中的值必须满足 Send + Sync。这是因为响应式系统实际上支持多线程:信号可以跨线程发送,整个响应式图(reactive graph)也可以跨多个线程工作。(这在配合使用 Axum 等使用 Tokio 多线程执行器的服务器框架进行 服务端渲染 时特别有用。)在大多数情况下,这对你的操作没有影响:普通的 Rust 数据类型默认就是 Send + Sync 的。

You may have noticed, either by reading the docs or by experimenting with your own applications, that the values that are stored in signals must be Send + Sync. This is because the reactive system actually supports multi-threading: signals can be sent across threads, and the whole reactive graph can work across multiple threads. (This is especially useful when doing server-side rendering with server frameworks like Axum, which use Tokio’s multi-threaded executor.) In most cases, this has no effect on what you do: ordinary Rust data types are Send + Sync by default.

然而,除非你使用 Web Worker,否则浏览器环境是单线程的,而由 wasm-bindgenweb-sys 提供的 JavaScript 类型都被明确标记为 !Send。这意味着它们不能存储在普通的信号中。

However, the browser environment is only single-threaded unless you use a Web Worker, and the JavaScript types provided by wasm-bindgen and web-sys are all explicitly !Send. This mean they can’t be stored in ordinary signals.

因此,我们为每个信号原语提供了“局部(local)”替代方案,可用于存储 !Send 数据。只有当你需要将 !Send 浏览器类型存储在信号中时,才应该使用这些。

As a result, we provide “local” alternatives for each of the signal primitives, which can be used to store !Send data. You should only reach for these when you have a !Send browser type you need to store in a signal.

Nightly 语法

Nightly Syntax

当使用 nightly 特性和 nightly 语法时,像调用函数一样调用 ReadSignal.get() 的语法糖。像调用函数一样调用 WriteSignal.set() 的语法糖。所以

When using the nightly feature and nightly syntax, calling a ReadSignal as a function is syntax sugar for .get(). Calling a WriteSignal as a function is syntax sugar for .set(). So

let (count, set_count) = signal(0);
set_count(1);
logging::log!(count());

等同于

is the same as

let (count, set_count) = signal(0);
set_count.set(1);
logging::log!(count.get());

这不仅仅是语法糖,它通过使信号在语义上与函数相同,从而实现了一个更一致的 API:参见 插曲

This is not just syntax sugar, but makes for a more consistent API by making signals semantically the same thing as functions: see the Interlude.

让信号互相依赖

Making signals depend on each other

经常有人问,在某些情况下,某个信号需要根据另一个信号的值而改变。有三种很好的方法可以做到这一点,还有一种方法虽然不太理想,但在受控情况下也可以接受。

Often people ask about situations in which some signal needs to change based on some other signal’s value. There are three good ways to do this, and one that’s less than ideal but okay under controlled circumstances.

好的选择

Good Options

1) B 是 A 的函数。 为 A 创建一个信号,为 B 创建一个派生信号或备忘录。

1) B is a function of A. Create a signal for A and a derived signal or memo for B.

// A
// A
let (count, set_count) = signal(1);
// B 是 A 的函数
// B is a function of A
let derived_signal_double_count = move || count.get() * 2;
// B 是 A 的函数
// B is a function of A
let memoized_double_count = Memo::new(move |_| count.get() * 2);

有关是使用派生信号还是备忘录的指导,请参阅 Memo 的文档。

For guidance on whether to use a derived signal or a memo, see the docs for Memo

2) C 是 A 和另一项 B 的函数。 为 A 和 B 创建信号,为 C 创建一个派生信号或备忘录。

2) C is a function of A and some other thing B. Create signals for A and B and a derived signal or memo for C.

// A
// A
let (first_name, set_first_name) = signal("Bridget".to_string());
// B
// B
let (last_name, set_last_name) = signal("Jones".to_string());
// C 是 A 和 B 的函数
// C is a function of A and B
let full_name = move || format!("{} {}", &*first_name.read(), &*last_name.read());

3) A 和 B 是独立的信号,但有时会同时更新。 当你调用更新 A 时,另外发起一次调用来更新 B。

3) A and B are independent signals, but sometimes updated at the same time. When you make the call to update A, make a separate call to update B.

// A
// A
let (age, set_age) = signal(32);
// B
// B
let (favorite_number, set_favorite_number) = signal(42);
// 使用它来处理 “清除” 按钮的点击
// use this to handle a click on a `Clear` button
let clear_handler = move |_| {
  // 同时更新 A 和 B
  // update both A and B
  set_age.set(0);
  set_favorite_number.set(0);
};

如果你真的必须这样做……

If you really must...

4) 创建一个副作用(effect),在 A 改变时写入 B。 官方不鼓励这样做,原因如下: a) 它的效率总是较低,因为这意味着每次 A 更新时,你都要完整地走两遍响应式过程。(你设置了 A,这导致副作用运行,以及依赖 A 的任何其他副作用。然后你设置了 B,这又导致依赖 B 的任何副作用运行。) b) 它增加了意外创建无限循环或过度重复运行副作用的机会。这种乒乓式的、响应式面条代码在 2010 年代初期很常见,我们试图通过读写分离以及不鼓励在副作用中写入信号等做法来避免这种情况。

4) Create an effect to write to B whenever A changes. This is officially discouraged, for several reasons: a) It will always be less efficient, as it means every time A updates you do two full trips through the reactive process. (You set A, which causes the effect to run, as well as any other effects that depend on A. Then you set B, which causes any effects that depend on B to run.) b) It increases your chances of accidentally creating things like infinite loops or over-re-running effects. This is the kind of ping-ponging, reactive spaghetti code that was common in the early 2010s and that we try to avoid with things like read-write segregation and discouraging writing to signals from effects.

在大多数情况下,最好重写逻辑,使之基于派生信号或备忘录,形成清晰的自顶向下的数据流。但这也不是什么世界末日。

In most situations, it’s best to rewrite things such that there’s a clear, top-down data flow based on derived signals or memos. But this isn’t the end of the world.

我故意不在这里提供示例。阅读 Effect 文档来弄清楚这是如何工作的。

I’m intentionally not providing an example here. Read the Effect docs to figure out how this would work.