Deduplicate reactive scope updates/Reset subscriptions on reruns/fix use memo double update (#2506)
* deduplicate effect updates * only subscribe to signals read in the current run of reactive scopes * subscribe to memo reads after recomputing the value
This commit is contained in:
1 parent
24d247dc85
commit
b6dc2a2230
18 files changed
+459
-250
No files matched your search
@@ -39,6 +39,3 @@ pub use write::*;
|
||||
|
||||
mod props;
|
||||
pub use props::*;
|
||||
|
||||
mod reactive_context;
|
||||
pub use reactive_context::*;
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::read_impls;
|
||||
use crate::write::Writable;
|
||||
use crate::{read::Readable, ReactiveContext, ReadableRef, Signal};
|
||||
use crate::{read::Readable, ReadableRef, Signal};
|
||||
use crate::{CopyValue, ReadOnlySignal};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
@@ -10,7 +10,7 @@ use std::{
|
||||
|
||||
use dioxus_core::prelude::*;
|
||||
use futures_util::StreamExt;
|
||||
use generational_box::UnsyncStorage;
|
||||
use generational_box::{AnyStorage, UnsyncStorage};
|
||||
|
||||
struct UpdateInformation<T> {
|
||||
dirty: Arc<AtomicBool>,
|
||||
@@ -53,7 +53,7 @@ impl<T: 'static> Memo<T> {
|
||||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
let dirty = Arc::new(AtomicBool::new(true));
|
||||
let dirty = Arc::new(AtomicBool::new(false));
|
||||
let (tx, mut rx) = futures_channel::mpsc::unbounded();
|
||||
|
||||
let callback = {
|
||||
@@ -67,7 +67,7 @@ impl<T: 'static> Memo<T> {
|
||||
ReactiveContext::new_with_callback(callback, current_scope_id().unwrap(), location);
|
||||
|
||||
// Create a new signal in that context, wiring up its dependencies and subscribers
|
||||
let mut recompute = move || rc.run_in(&mut f);
|
||||
let mut recompute = move || rc.reset_and_run_in(&mut f);
|
||||
let value = recompute();
|
||||
let recompute = RefCell::new(Box::new(recompute) as Box<dyn FnMut() -> T>);
|
||||
let update = CopyValue::new(UpdateInformation {
|
||||
@@ -134,24 +134,30 @@ where
|
||||
fn try_read_unchecked(
|
||||
&self,
|
||||
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
|
||||
let read = self.inner.try_read_unchecked();
|
||||
match read {
|
||||
Ok(r) => {
|
||||
let needs_update = self
|
||||
.update
|
||||
.read()
|
||||
.dirty
|
||||
.swap(false, std::sync::atomic::Ordering::Relaxed);
|
||||
if needs_update {
|
||||
drop(r);
|
||||
self.recompute();
|
||||
self.inner.try_read_unchecked()
|
||||
} else {
|
||||
Ok(r)
|
||||
}
|
||||
// Read the inner generational box instead of the signal so we have more fine grained control over exactly when the subscription happens
|
||||
let read = self.inner.inner.try_read_unchecked()?;
|
||||
|
||||
let needs_update = self
|
||||
.update
|
||||
.read()
|
||||
.dirty
|
||||
.swap(false, std::sync::atomic::Ordering::Relaxed);
|
||||
let result = if needs_update {
|
||||
drop(read);
|
||||
// We shouldn't be subscribed to the value here so we don't trigger the scope we are currently in to rerun even though that scope got the latest value because we synchronously update the value: https://github.com/DioxusLabs/dioxus/issues/2416
|
||||
self.recompute();
|
||||
self.inner.inner.try_read_unchecked()
|
||||
} else {
|
||||
Ok(read)
|
||||
};
|
||||
// Subscribe to the current scope before returning the value
|
||||
if let Ok(read) = &result {
|
||||
if let Some(reactive_context) = ReactiveContext::current() {
|
||||
tracing::trace!("Subscribing to the reactive context {}", reactive_context);
|
||||
reactive_context.subscribe(read.subscribers.clone());
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
result.map(|read| <UnsyncStorage as AnyStorage>::map(read, |v| &v.value))
|
||||
}
|
||||
|
||||
/// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.**
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
use dioxus_core::prelude::{
|
||||
current_scope_id, has_context, provide_context, schedule_update_any, ScopeId,
|
||||
};
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use generational_box::SyncStorage;
|
||||
use std::{cell::RefCell, hash::Hash};
|
||||
|
||||
use crate::{CopyValue, Writable};
|
||||
|
||||
#[doc = include_str!("../docs/reactivity.md")]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ReactiveContext {
|
||||
inner: CopyValue<Inner, SyncStorage>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static CURRENT: RefCell<Vec<ReactiveContext>> = const { RefCell::new(vec![]) };
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ReactiveContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
use crate::Readable;
|
||||
if let Ok(read) = self.inner.try_read() {
|
||||
if let Some(scope) = read.scope {
|
||||
return write!(f, "ReactiveContext(for scope: {:?})", scope);
|
||||
}
|
||||
return write!(f, "ReactiveContext created at {}", read.origin);
|
||||
}
|
||||
}
|
||||
write!(f, "ReactiveContext")
|
||||
}
|
||||
}
|
||||
|
||||
impl ReactiveContext {
|
||||
/// Create a new reactive context
|
||||
#[track_caller]
|
||||
pub fn new() -> (Self, UnboundedReceiver<()>) {
|
||||
Self::new_with_origin(std::panic::Location::caller())
|
||||
}
|
||||
|
||||
/// Create a new reactive context with a location for debugging purposes
|
||||
/// This is useful for reactive contexts created within closures
|
||||
pub fn new_with_origin(
|
||||
origin: &'static std::panic::Location<'static>,
|
||||
) -> (Self, UnboundedReceiver<()>) {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
let callback = move || {
|
||||
let _ = tx.unbounded_send(());
|
||||
};
|
||||
let _self = Self::new_with_callback(callback, current_scope_id().unwrap(), origin);
|
||||
(_self, rx)
|
||||
}
|
||||
|
||||
/// Create a new reactive context that may update a scope. When any signal that this context subscribes to changes, the callback will be run
|
||||
pub fn new_with_callback(
|
||||
callback: impl FnMut() + Send + Sync + 'static,
|
||||
scope: ScopeId,
|
||||
#[allow(unused)] origin: &'static std::panic::Location<'static>,
|
||||
) -> Self {
|
||||
let inner = Inner {
|
||||
self_: None,
|
||||
update: Box::new(callback),
|
||||
#[cfg(debug_assertions)]
|
||||
origin,
|
||||
#[cfg(debug_assertions)]
|
||||
scope: None,
|
||||
};
|
||||
|
||||
let mut self_ = Self {
|
||||
inner: CopyValue::new_maybe_sync_in_scope(inner, scope),
|
||||
};
|
||||
|
||||
self_.inner.write().self_ = Some(self_);
|
||||
|
||||
self_
|
||||
}
|
||||
|
||||
/// Get the current reactive context
|
||||
///
|
||||
/// If this was set manually, then that value will be returned.
|
||||
///
|
||||
/// If there's no current reactive context, then a new one will be created for the current scope and returned.
|
||||
pub fn current() -> Option<Self> {
|
||||
let cur = CURRENT.with(|current| current.borrow().last().cloned());
|
||||
|
||||
// If we're already inside a reactive context, then return that
|
||||
if let Some(cur) = cur {
|
||||
return Some(cur);
|
||||
}
|
||||
|
||||
// If we're rendering, then try and use the reactive context attached to this component
|
||||
if !dioxus_core::vdom_is_rendering() {
|
||||
return None;
|
||||
}
|
||||
if let Some(cx) = has_context() {
|
||||
return Some(cx);
|
||||
}
|
||||
let update_any = schedule_update_any();
|
||||
let scope_id = current_scope_id().unwrap();
|
||||
let update_scope = move || {
|
||||
tracing::trace!("Marking scope {:?} as dirty", scope_id);
|
||||
update_any(scope_id)
|
||||
};
|
||||
|
||||
// Otherwise, create a new context at the current scope
|
||||
#[allow(unused_mut)]
|
||||
let mut reactive_context = ReactiveContext::new_with_callback(
|
||||
update_scope,
|
||||
scope_id,
|
||||
std::panic::Location::caller(),
|
||||
);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// Associate the reactive context with the current scope for debugging
|
||||
reactive_context.inner.write().scope = Some(scope_id);
|
||||
}
|
||||
Some(provide_context(reactive_context))
|
||||
}
|
||||
|
||||
/// Run this function in the context of this reactive context
|
||||
///
|
||||
/// This will set the current reactive context to this context for the duration of the function.
|
||||
/// You can then get information about the current subscriptions.
|
||||
pub fn run_in<O>(&self, f: impl FnOnce() -> O) -> O {
|
||||
CURRENT.with(|current| current.borrow_mut().push(*self));
|
||||
let out = f();
|
||||
CURRENT.with(|current| current.borrow_mut().pop());
|
||||
out
|
||||
}
|
||||
|
||||
/// Marks this reactive context as dirty
|
||||
///
|
||||
/// If there's a scope associated with this context, then it will be marked as dirty too
|
||||
///
|
||||
/// Returns true if the context was marked as dirty, or false if the context has been dropped
|
||||
pub fn mark_dirty(&self) -> bool {
|
||||
if let Ok(mut self_write) = self.inner.try_write_unchecked() {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
tracing::trace!(
|
||||
"Marking reactive context created at {} as dirty",
|
||||
self_write.origin
|
||||
);
|
||||
}
|
||||
|
||||
(self_write.update)();
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the scope that inner CopyValue is associated with
|
||||
pub fn origin_scope(&self) -> ScopeId {
|
||||
self.inner.origin_scope()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for ReactiveContext {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.inner.id().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
self_: Option<ReactiveContext>,
|
||||
|
||||
// Futures will call .changed().await
|
||||
update: Box<dyn FnMut() + Send + Sync>,
|
||||
|
||||
// Debug information for signal subscriptions
|
||||
#[cfg(debug_assertions)]
|
||||
origin: &'static std::panic::Location<'static>,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
// The scope that this reactive context is associated with
|
||||
scope: Option<ScopeId>,
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{write::Writable, ReactiveContext};
|
||||
use crate::write::Writable;
|
||||
use std::hash::Hash;
|
||||
|
||||
use crate::read::Readable;
|
||||
@@ -57,7 +57,7 @@ impl<R: Eq + Hash, S: Storage<SignalData<bool>>> SetCompare<R, S> {
|
||||
spawn(async move {
|
||||
loop {
|
||||
// Recompute the value
|
||||
rc.run_in(&mut recompute);
|
||||
rc.reset_and_run_in(&mut recompute);
|
||||
|
||||
// Wait for context to change
|
||||
let _ = changed.next().await;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::{default_impl, fmt_impls, write_impls};
|
||||
use crate::{read::*, write::*, CopyValue, GlobalMemo, GlobalSignal, ReactiveContext, ReadableRef};
|
||||
use crate::{read::*, write::*, CopyValue, GlobalMemo, GlobalSignal, ReadableRef};
|
||||
use crate::{Memo, WritableRef};
|
||||
use dioxus_core::IntoDynNode;
|
||||
use dioxus_core::{prelude::IntoAttributeValue, ScopeId};
|
||||
use dioxus_core::prelude::*;
|
||||
use generational_box::{AnyStorage, Storage, SyncStorage, UnsyncStorage};
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
any::Any,
|
||||
collections::HashSet,
|
||||
@@ -28,7 +28,7 @@ pub type SyncSignal<T> = Signal<T, SyncStorage>;
|
||||
|
||||
/// The data stored for tracking in a signal.
|
||||
pub struct SignalData<T> {
|
||||
pub(crate) subscribers: Mutex<HashSet<ReactiveContext>>,
|
||||
pub(crate) subscribers: Arc<Mutex<HashSet<ReactiveContext>>>,
|
||||
pub(crate) value: T,
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ impl<T, S: Storage<SignalData<T>>> Readable for Signal<T, S> {
|
||||
|
||||
if let Some(reactive_context) = ReactiveContext::current() {
|
||||
tracing::trace!("Subscribing to the reactive context {}", reactive_context);
|
||||
inner.subscribers.lock().unwrap().insert(reactive_context);
|
||||
reactive_context.subscribe(inner.subscribers.clone());
|
||||
}
|
||||
|
||||
Ok(S::map(inner, |v| &v.value))
|
||||
|
||||
Reference in new issue
Block a user