Assume the virtual dom is not rendering while not diffing components (#3406)

* Invert when the rendering flag is set

* Add a playwright test to make sure effects, and web-sys integration is working

* fix playwright test
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-12-20 12:08:57 -08:00
1 parent cf1940cdac
commit 01ebccc269
8 files changed
+79 -15

No files matched your search

Generated
+2
View File
@@ -4051,6 +4051,8 @@ dependencies = [
"serde_json", "serde_json",
"tracing", "tracing",
"tracing-wasm", "tracing-wasm",
"wasm-bindgen",
"web-sys",
] ]
[[package]] [[package]]
+1 -4
View File
@@ -1,5 +1,4 @@
use crate::innerlude::ScopeOrder; use crate::innerlude::ScopeOrder;
use crate::Runtime;
use std::borrow::Borrow; use std::borrow::Borrow;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::VecDeque; use std::collections::VecDeque;
@@ -29,13 +28,11 @@ impl Effect {
self.effect.borrow_mut().push_back(Box::new(f)); self.effect.borrow_mut().push_back(Box::new(f));
} }
pub(crate) fn run(&self, runtime: &Runtime) { pub(crate) fn run(&self) {
runtime.rendering.set(false);
let mut effect = self.effect.borrow_mut(); let mut effect = self.effect.borrow_mut();
while let Some(f) = effect.pop_front() { while let Some(f) = effect.pop_front() {
f(); f();
} }
runtime.rendering.set(true);
} }
} }
+9 -5
View File
@@ -74,7 +74,7 @@ impl Runtime {
Rc::new(Self { Rc::new(Self {
sender, sender,
rendering: Cell::new(true), rendering: Cell::new(false),
scope_states: Default::default(), scope_states: Default::default(),
scope_stack: Default::default(), scope_stack: Default::default(),
suspense_stack: Default::default(), suspense_stack: Default::default(),
@@ -108,6 +108,14 @@ impl Runtime {
} }
} }
/// Run a closure with the rendering flag set to true
pub(crate) fn while_rendering<T>(&self, f: impl FnOnce() -> T) -> T {
self.rendering.set(true);
let result = f();
self.rendering.set(false);
result
}
/// Create a scope context. This slab is synchronized with the scope slab. /// Create a scope context. This slab is synchronized with the scope slab.
pub(crate) fn create_scope(&self, context: Scope) { pub(crate) fn create_scope(&self, context: Scope) {
let id = context.id; let id = context.id;
@@ -380,9 +388,7 @@ impl Runtime {
); );
for listener in listeners.into_iter().rev() { for listener in listeners.into_iter().rev() {
if let AttributeValue::Listener(listener) = listener { if let AttributeValue::Listener(listener) = listener {
self.rendering.set(false);
listener.call(uievent.clone()); listener.call(uievent.clone());
self.rendering.set(true);
let metadata = uievent.metadata.borrow(); let metadata = uievent.metadata.borrow();
if !metadata.propagates { if !metadata.propagates {
@@ -420,9 +426,7 @@ impl Runtime {
// Only call the listener if this is the exact target element. // Only call the listener if this is the exact target element.
if attr.name.get(2..) == Some(name) && target_path == this_path { if attr.name.get(2..) == Some(name) && target_path == this_path {
if let AttributeValue::Listener(listener) = &attr.value { if let AttributeValue::Listener(listener) = &attr.value {
self.rendering.set(false);
listener.call(uievent.clone()); listener.call(uievent.clone());
self.rendering.set(true);
break; break;
} }
} }
-2
View File
@@ -275,7 +275,6 @@ impl Runtime {
// poll the future with the scope on the stack // poll the future with the scope on the stack
let poll_result = self.with_scope_on_stack(task.scope, || { let poll_result = self.with_scope_on_stack(task.scope, || {
self.rendering.set(false);
self.current_task.set(Some(id)); self.current_task.set(Some(id));
let poll_result = task.task.borrow_mut().as_mut().poll(&mut cx); let poll_result = task.task.borrow_mut().as_mut().poll(&mut cx);
@@ -293,7 +292,6 @@ impl Runtime {
poll_result poll_result
}); });
self.rendering.set(true);
self.current_task.set(None); self.current_task.set(None);
poll_result poll_result
+11 -4
View File
@@ -511,7 +511,7 @@ impl VirtualDom {
// At this point, we have finished running all tasks that are pending and we haven't found any scopes to rerun. This means it is safe to run our lowest priority work: effects // At this point, we have finished running all tasks that are pending and we haven't found any scopes to rerun. This means it is safe to run our lowest priority work: effects
while let Some(effect) = self.pop_effect() { while let Some(effect) = self.pop_effect() {
effect.run(&self.runtime); effect.run();
// Check if any new scopes are queued for rerun // Check if any new scopes are queued for rerun
self.queue_events(); self.queue_events();
if self.has_dirty_scopes() { if self.has_dirty_scopes() {
@@ -562,7 +562,10 @@ impl VirtualDom {
#[instrument(skip(self, to), level = "trace", name = "VirtualDom::rebuild")] #[instrument(skip(self, to), level = "trace", name = "VirtualDom::rebuild")]
pub fn rebuild(&mut self, to: &mut impl WriteMutations) { pub fn rebuild(&mut self, to: &mut impl WriteMutations) {
let _runtime = RuntimeGuard::new(self.runtime.clone()); let _runtime = RuntimeGuard::new(self.runtime.clone());
let new_nodes = self.run_scope(ScopeId::ROOT); let new_nodes = self
.runtime
.clone()
.while_rendering(|| self.run_scope(ScopeId::ROOT));
self.scopes[ScopeId::ROOT.0].last_rendered_node = Some(new_nodes.clone()); self.scopes[ScopeId::ROOT.0].last_rendered_node = Some(new_nodes.clone());
@@ -593,7 +596,9 @@ impl VirtualDom {
} }
Work::RerunScope(scope) => { Work::RerunScope(scope) => {
// If the scope is dirty, run the scope and get the mutations // If the scope is dirty, run the scope and get the mutations
self.run_and_diff_scope(Some(to), scope.id); self.runtime.clone().while_rendering(|| {
self.run_and_diff_scope(Some(to), scope.id);
});
} }
} }
} }
@@ -700,7 +705,9 @@ impl VirtualDom {
.is_some(); .is_some();
if run_scope { if run_scope {
// If the scope is dirty, run the scope and get the mutations // If the scope is dirty, run the scope and get the mutations
self.run_and_diff_scope(None::<&mut NoOpMutations>, scope_id); self.runtime.clone().while_rendering(|| {
self.run_and_diff_scope(None::<&mut NoOpMutations>, scope_id);
});
tracing::trace!("Ran scope {:?} during suspense", scope_id); tracing::trace!("Ran scope {:?} during suspense", scope_id);
} else { } else {
+9
View File
@@ -115,3 +115,12 @@ test("onmounted", async ({ page }) => {
const mountedDiv = page.locator("div.onmounted-div"); const mountedDiv = page.locator("div.onmounted-div");
await expect(mountedDiv).toHaveText("onmounted was called 1 times"); await expect(mountedDiv).toHaveText("onmounted was called 1 times");
}); });
test("web-sys closure", async ({ page }) => {
await page.goto("http://localhost:9999");
// wait until the div is mounted
const scrollDiv = page.locator("div#web-sys-closure-div");
await scrollDiv.waitFor({ state: "attached" });
await page.keyboard.press("Enter");
await expect(scrollDiv).toHaveText("the keydown event was triggered");
});
+2
View File
@@ -11,3 +11,5 @@ dioxus = { workspace = true, features = ["web"]}
serde_json = "1.0.96" serde_json = "1.0.96"
tracing.workspace = true tracing.workspace = true
tracing-wasm = "0.2.1" tracing-wasm = "0.2.1"
wasm-bindgen.workspace = true
web-sys = { workspace = true, features = ["Element", "Window"] }
+45
View File
@@ -1,6 +1,7 @@
// This test is used by playwright configured in the root of the repo // This test is used by playwright configured in the root of the repo
use dioxus::prelude::*; use dioxus::prelude::*;
use wasm_bindgen::prelude::*;
fn app() -> Element { fn app() -> Element {
let mut num = use_signal(|| 0); let mut num = use_signal(|| 0);
@@ -53,6 +54,7 @@ fn app() -> Element {
div { class: "eval-result", "{eval_result}" } div { class: "eval-result", "{eval_result}" }
PreventDefault {} PreventDefault {}
OnMounted {} OnMounted {}
WebSysClosure {}
} }
} }
@@ -86,6 +88,49 @@ fn OnMounted() -> Element {
} }
} }
// This component tests attaching an event listener to the document with a web-sys closure
// and effect
#[component]
fn WebSysClosure() -> Element {
static TRIGGERED: GlobalSignal<bool> = GlobalSignal::new(|| false);
use_effect(|| {
let window = web_sys::window().expect("window not available");
// Assert the component contents have been mounted
window
.document()
.unwrap()
.get_element_by_id("web-sys-closure-div")
.expect("Effects should only be run after all contents have bene mounted to the dom");
// Make sure passing the runtime into the closure works
let callback = Callback::new(|_| {
assert!(!dioxus::dioxus_core::vdom_is_rendering());
*TRIGGERED.write() = true;
});
let closure: Closure<dyn Fn()> = Closure::new({
move || {
callback(());
}
});
window
.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
.expect("Failed to add keydown event listener");
closure.forget();
});
rsx! {
div {
id: "web-sys-closure-div",
if TRIGGERED() {
"the keydown event was triggered"
}
}
}
}
fn main() { fn main() {
tracing_wasm::set_as_global_default_with_config( tracing_wasm::set_as_global_default_with_config(
tracing_wasm::WASMLayerConfigBuilder::default() tracing_wasm::WASMLayerConfigBuilder::default()