feat: Add owning memos to allow memos that re-use the previous value (#2139)

This commit is contained in:
Aphek 2024-02-19 23:16:19 -03:00 committed by GitHub
parent 330ebdb018
commit 0770b87cb7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 238 additions and 25 deletions

View file

@ -86,7 +86,88 @@ pub fn create_memo<T>(f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + 'static,
{
Runtime::current().create_memo(f)
Runtime::current().create_owning_memo(move |current_value| {
let new_value = f(current_value.as_ref());
let is_different = current_value.as_ref() != Some(&new_value);
(new_value, is_different)
})
}
/// Like [`create_memo`], `create_owning_memo` creates an efficient derived reactive value based on
/// other reactive values, but with two differences:
/// 1. The argument to the memo function is owned instead of borrowed.
/// 2. The function must also return whether the value has changed, as the first element of the tuple.
///
/// All of the other caveats and guarantees are the same as the usual "borrowing" memos.
///
/// This type of memo is useful for memos which can avoid computation by re-using the last value,
/// especially slices that need to allocate.
///
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// # let runtime = create_runtime();
/// pub struct State {
/// name: String,
/// token: String,
/// }
///
/// let state = create_rw_signal(State {
/// name: "Alice".to_owned(),
/// token: "abcdef".to_owned(),
/// });
///
/// // If we used `create_memo`, we'd need to allocate every time the state changes, but by using
/// // `create_owning_memo` we can allocate only when `state.name` changes.
/// let name = create_owning_memo(move |old_name| {
/// state.with(move |state| {
/// if let Some(name) =
/// old_name.filter(|old_name| old_name == &state.name)
/// {
/// (name, false)
/// } else {
/// (state.name.clone(), true)
/// }
/// })
/// });
/// let set_name = move |name| state.update(|state| state.name = name);
///
/// // We can also re-use the last allocation even when the value changes, which is usually faster,
/// // but may have some caveats (e.g. if the value size is drastically reduced, the memory will
/// // still be used for the life of the memo).
/// let token = create_owning_memo(move |old_token| {
/// state.with(move |state| {
/// let is_different = old_token.as_ref() != Some(&state.token);
/// let mut token = old_token.unwrap_or_else(String::new);
///
/// if is_different {
/// token.clone_from(&state.token);
/// }
/// (token, is_different)
/// })
/// });
/// let set_token = move |new_token| state.update(|state| state.token = new_token);
/// # runtime.dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
pub fn create_owning_memo<T>(
f: impl Fn(Option<T>) -> (T, bool) + 'static,
) -> Memo<T>
where
T: PartialEq + 'static,
{
Runtime::current().create_owning_memo(f)
}
/// An efficient derived reactive value based on other reactive values.
@ -216,6 +297,65 @@ impl<T> Memo<T> {
{
create_memo(f)
}
/// Creates a new owning memo from the given function.
///
/// This is identical to [`create_owning_memo`].
///
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// # let runtime = create_runtime();
/// pub struct State {
/// name: String,
/// token: String,
/// }
///
/// let state = RwSignal::new(State {
/// name: "Alice".to_owned(),
/// token: "abcdef".to_owned(),
/// });
///
/// // If we used `Memo::new`, we'd need to allocate every time the state changes, but by using
/// // `Memo::new_owning` we can allocate only when `state.name` changes.
/// let name = Memo::new_owning(move |old_name| {
/// state.with(move |state| {
/// if let Some(name) =
/// old_name.filter(|old_name| old_name == &state.name)
/// {
/// (name, false)
/// } else {
/// (state.name.clone(), true)
/// }
/// })
/// });
/// let set_name = move |name| state.update(|state| state.name = name);
///
/// // We can also re-use the last allocation even when the value changes, which is usually faster,
/// // but may have some caveats (e.g. if the value size is drastically reduced, the memory will
/// // still be used for the life of the memo).
/// let token = Memo::new_owning(move |old_token| {
/// state.with(move |state| {
/// let is_different = old_token.as_ref() != Some(&state.token);
/// let mut token = old_token.unwrap_or_else(String::new);
///
/// if is_different {
/// token.clone_from(&state.token);
/// }
/// (token, is_different)
/// })
/// });
/// let set_token = move |new_token| state.update(|state| state.token = new_token);
/// # runtime.dispose();
/// ```
#[inline(always)]
#[track_caller]
pub fn new_owning(f: impl Fn(Option<T>) -> (T, bool) + 'static) -> Memo<T>
where
T: PartialEq + 'static,
{
create_owning_memo(f)
}
}
impl<T> Clone for Memo<T>
@ -524,8 +664,8 @@ impl_get_fn_traits![Memo];
pub(crate) struct MemoState<T, F>
where
T: PartialEq + 'static,
F: Fn(Option<&T>) -> T,
T: 'static,
F: Fn(Option<T>) -> (T, bool),
{
pub f: F,
pub t: PhantomData<T>,
@ -535,8 +675,8 @@ where
impl<T, F> AnyComputation for MemoState<T, F>
where
T: PartialEq + 'static,
F: Fn(Option<&T>) -> T,
T: 'static,
F: Fn(Option<T>) -> (T, bool),
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
@ -551,24 +691,16 @@ where
)
)]
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
let (new_value, is_different) = {
let value = value.borrow();
let curr_value = value
.downcast_ref::<Option<T>>()
.expect("to downcast memo value");
let mut value = value.borrow_mut();
let curr_value = value
.downcast_mut::<Option<T>>()
.expect("to downcast memo value");
// run the effect
let new_value = (self.f)(curr_value.as_ref());
let is_different = curr_value.as_ref() != Some(&new_value);
(new_value, is_different)
};
if is_different {
let mut value = value.borrow_mut();
let curr_value = value
.downcast_mut::<Option<T>>()
.expect("to downcast memo value");
*curr_value = Some(new_value);
}
// run the memo
let (new_value, is_different) = (self.f)(curr_value.take());
// set new value
*curr_value = Some(new_value);
is_different
}

View file

@ -1201,12 +1201,12 @@ impl RuntimeId {
#[track_caller]
#[inline(always)]
pub(crate) fn create_memo<T>(
pub(crate) fn create_owning_memo<T>(
self,
f: impl Fn(Option<&T>) -> T + 'static,
f: impl Fn(Option<T>) -> (T, bool) + 'static,
) -> Memo<T>
where
T: PartialEq + Any + 'static,
T: 'static,
{
Memo {
id: self.create_concrete_memo(

View file

@ -212,3 +212,84 @@ fn dynamic_dependencies() {
runtime.dispose();
}
#[test]
fn owning_memo_slice() {
use std::rc::Rc;
let runtime = create_runtime();
// this could be serialized to and from localstorage with miniserde
pub struct State {
name: String,
token: String,
}
let state = create_rw_signal(State {
name: "Alice".to_owned(),
token: "is this a token????".to_owned(),
});
// We can allocate only when `state.name` changes
let name = create_owning_memo(move |old_name| {
state.with(move |state| {
if let Some(name) =
old_name.filter(|old_name| old_name == &state.name)
{
(name, false)
} else {
(state.name.clone(), true)
}
})
});
let set_name = move |name| state.update(|state| state.name = name);
// We can also re-use the last token allocation, which may be even better if the tokens are
// always of the same length
let token = create_owning_memo(move |old_token| {
state.with(move |state| {
let is_different = old_token.as_ref() != Some(&state.token);
let mut token = old_token.unwrap_or_else(String::new);
if is_different {
token.clone_from(&state.token);
}
(token, is_different)
})
});
let set_token =
move |new_token| state.update(|state| state.token = new_token);
let count_name_updates = Rc::new(std::cell::Cell::new(0));
assert_eq!(count_name_updates.get(), 0);
create_isomorphic_effect({
let count_name_updates = Rc::clone(&count_name_updates);
move |_| {
name.track();
count_name_updates.set(count_name_updates.get() + 1);
}
});
assert_eq!(count_name_updates.get(), 1);
let count_token_updates = Rc::new(std::cell::Cell::new(0));
assert_eq!(count_token_updates.get(), 0);
create_isomorphic_effect({
let count_token_updates = Rc::clone(&count_token_updates);
move |_| {
token.track();
count_token_updates.set(count_token_updates.get() + 1);
}
});
assert_eq!(count_token_updates.get(), 1);
set_name("Bob".to_owned());
name.with(|name| assert_eq!(name, "Bob"));
assert_eq!(count_name_updates.get(), 2);
assert_eq!(count_token_updates.get(), 1);
set_token("this is not a token!".to_owned());
token.with(|token| assert_eq!(token, "this is not a token!"));
assert_eq!(count_name_updates.get(), 2);
assert_eq!(count_token_updates.get(), 2);
runtime.dispose();
}