mirror of https://github.com/yewstack/yew
Component lifecycle scheduler optimizations (#2065)
* yew: partial render deduplication implementation * yew/scheduler: batching and comment fixes * yew/scheduler: clippy fixes * yew/vcomp: add lifecycle debug logging * ci: add release clippy and cargo check passes * ci: fix release check * ci: remove duplicate check calls
This commit is contained in:
parent
4ca68e07ca
commit
a442ce5df5
|
@ -41,6 +41,35 @@ jobs:
|
|||
command: clippy
|
||||
args: --all-targets -- -D warnings
|
||||
|
||||
lint-release:
|
||||
name: Clippy on release profile
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
profile: minimal
|
||||
components: clippy
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ github.job }}-${{ hashFiles('**/Cargo.toml') }}
|
||||
restore-keys: |
|
||||
cargo-${{ runner.os }}-${{ github.job }}-
|
||||
|
||||
- name: Run clippy
|
||||
if: always()
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-targets --release -- -D warnings
|
||||
|
||||
check_examples:
|
||||
name: Check Examples
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -105,7 +134,7 @@ jobs:
|
|||
run: |
|
||||
cd packages/yew
|
||||
cargo test --doc --features "doc_test wasm_test"
|
||||
|
||||
|
||||
- name: Run website code snippet tests
|
||||
run: |
|
||||
cd packages/website-test
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
# public tasks:
|
||||
# * pr-flow
|
||||
# * lint
|
||||
# * lint-release
|
||||
# * tests
|
||||
# * benchmarks
|
||||
#
|
||||
|
@ -25,7 +26,7 @@ namespace = "core"
|
|||
toolchain = "stable"
|
||||
category = "Checks"
|
||||
description = "Lint and test"
|
||||
run_task = { name = ["lint", "tests"], fork = true }
|
||||
run_task = { name = ["lint", "lint-release", "tests"], fork = true }
|
||||
|
||||
[tasks.lint]
|
||||
category = "Checks"
|
||||
|
@ -50,6 +51,13 @@ private = true
|
|||
workspace = true
|
||||
dependencies = ["core::check-format-flow", "core::clippy-flow"]
|
||||
|
||||
# Needed, because we have some code differences between debug and release builds
|
||||
[tasks.lint-release]
|
||||
category = "Checks"
|
||||
workspace = true
|
||||
command = "cargo"
|
||||
args = ["clippy", "--all-targets", "--release", "--", "--deny=warnings"]
|
||||
|
||||
[tasks.tests-setup]
|
||||
private = true
|
||||
script_runner = "@duckscript"
|
||||
|
|
|
@ -16,8 +16,10 @@ pub(crate) struct ComponentState<COMP: Component> {
|
|||
next_sibling: NodeRef,
|
||||
node_ref: NodeRef,
|
||||
has_rendered: bool,
|
||||
pending_root: Option<VNode>,
|
||||
pending_updates: Vec<UpdateEvent<COMP>>,
|
||||
|
||||
// Used for debug logging
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) vcomp_id: u64,
|
||||
}
|
||||
|
||||
impl<COMP: Component> ComponentState<COMP> {
|
||||
|
@ -29,6 +31,12 @@ impl<COMP: Component> ComponentState<COMP> {
|
|||
scope: Scope<COMP>,
|
||||
props: Rc<COMP::Properties>,
|
||||
) -> Self {
|
||||
#[cfg(debug_assertions)]
|
||||
let vcomp_id = {
|
||||
use super::Scoped;
|
||||
|
||||
scope.to_any().vcomp_id
|
||||
};
|
||||
let context = Context { scope, props };
|
||||
|
||||
let component = Box::new(COMP::create(&context));
|
||||
|
@ -40,39 +48,14 @@ impl<COMP: Component> ComponentState<COMP> {
|
|||
next_sibling,
|
||||
node_ref,
|
||||
has_rendered: false,
|
||||
pending_root: None,
|
||||
pending_updates: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_pending_updates(&mut self, state: &Shared<Option<ComponentState<COMP>>>) {
|
||||
if !self.pending_updates.is_empty() {
|
||||
scheduler::push_component_updates(self.pending_updates.drain(..).map(|update| {
|
||||
Box::new(ComponentRunnable {
|
||||
state: state.clone(),
|
||||
event: update.into(),
|
||||
}) as Box<dyn Runnable>
|
||||
}));
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal Component lifecycle event
|
||||
pub(crate) enum ComponentLifecycleEvent<COMP: Component> {
|
||||
Create(CreateEvent<COMP>),
|
||||
Update(UpdateEvent<COMP>),
|
||||
Render,
|
||||
Rendered,
|
||||
Destroy,
|
||||
}
|
||||
|
||||
impl<COMP: Component> From<CreateEvent<COMP>> for ComponentLifecycleEvent<COMP> {
|
||||
fn from(create: CreateEvent<COMP>) -> Self {
|
||||
Self::Create(create)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct CreateEvent<COMP: Component> {
|
||||
pub(crate) struct CreateRunner<COMP: Component> {
|
||||
pub(crate) parent: Element,
|
||||
pub(crate) next_sibling: NodeRef,
|
||||
pub(crate) placeholder: VNode,
|
||||
|
@ -81,15 +64,26 @@ pub(crate) struct CreateEvent<COMP: Component> {
|
|||
pub(crate) scope: Scope<COMP>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> From<UpdateEvent<COMP>> for ComponentLifecycleEvent<COMP> {
|
||||
fn from(update: UpdateEvent<COMP>) -> Self {
|
||||
Self::Update(update)
|
||||
impl<COMP: Component> Runnable for CreateRunner<COMP> {
|
||||
fn run(self: Box<Self>) {
|
||||
let mut current_state = self.scope.state.borrow_mut();
|
||||
if current_state.is_none() {
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(self.scope.vcomp_id, "create");
|
||||
|
||||
*current_state = Some(ComponentState::new(
|
||||
self.parent,
|
||||
self.next_sibling,
|
||||
self.placeholder,
|
||||
self.node_ref,
|
||||
self.scope.clone(),
|
||||
self.props,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum UpdateEvent<COMP: Component> {
|
||||
/// First update
|
||||
First,
|
||||
/// Wraps messages for a component.
|
||||
Message(COMP::Message),
|
||||
/// Wraps batch of messages for a component.
|
||||
|
@ -98,97 +92,110 @@ pub(crate) enum UpdateEvent<COMP: Component> {
|
|||
Properties(Rc<COMP::Properties>, NodeRef, NodeRef),
|
||||
}
|
||||
|
||||
pub(crate) struct ComponentRunnable<COMP: Component> {
|
||||
pub(crate) struct UpdateRunner<COMP: Component> {
|
||||
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
|
||||
pub(crate) event: ComponentLifecycleEvent<COMP>,
|
||||
pub(crate) event: UpdateEvent<COMP>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> Runnable for ComponentRunnable<COMP> {
|
||||
impl<COMP: Component> Runnable for UpdateRunner<COMP> {
|
||||
fn run(self: Box<Self>) {
|
||||
let mut current_state = self.state.borrow_mut();
|
||||
match self.event {
|
||||
ComponentLifecycleEvent::Create(event) => {
|
||||
if current_state.is_none() {
|
||||
*current_state = Some(ComponentState::new(
|
||||
event.parent,
|
||||
event.next_sibling,
|
||||
event.placeholder,
|
||||
event.node_ref,
|
||||
event.scope.clone(),
|
||||
event.props,
|
||||
));
|
||||
if let Some(mut state) = self.state.borrow_mut().as_mut() {
|
||||
let schedule_render = match self.event {
|
||||
UpdateEvent::Message(message) => state.component.update(&state.context, message),
|
||||
UpdateEvent::MessageBatch(messages) => {
|
||||
messages.into_iter().fold(false, |acc, msg| {
|
||||
state.component.update(&state.context, msg) || acc
|
||||
})
|
||||
}
|
||||
}
|
||||
ComponentLifecycleEvent::Update(event) => {
|
||||
if let Some(mut state) = current_state.as_mut() {
|
||||
if state.pending_root.is_some() {
|
||||
state.pending_updates.push(event);
|
||||
return;
|
||||
}
|
||||
|
||||
let should_render = match event {
|
||||
UpdateEvent::First => true,
|
||||
UpdateEvent::Message(message) => {
|
||||
state.component.update(&state.context, message)
|
||||
}
|
||||
UpdateEvent::MessageBatch(messages) => {
|
||||
messages.into_iter().fold(false, |acc, msg| {
|
||||
state.component.update(&state.context, msg) || acc
|
||||
})
|
||||
}
|
||||
UpdateEvent::Properties(props, node_ref, next_sibling) => {
|
||||
// When components are updated, a new node ref could have been passed in
|
||||
state.node_ref = node_ref;
|
||||
// When components are updated, their siblings were likely also updated
|
||||
state.next_sibling = next_sibling;
|
||||
// Only trigger changed if props were changed
|
||||
if state.context.props != props {
|
||||
state.context.props = Rc::clone(&props);
|
||||
state.component.changed(&state.context)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if should_render {
|
||||
state.pending_root = Some(state.component.view(&state.context));
|
||||
state.context.scope.process(ComponentLifecycleEvent::Render);
|
||||
};
|
||||
}
|
||||
}
|
||||
ComponentLifecycleEvent::Render => {
|
||||
if let Some(state) = current_state.as_mut() {
|
||||
if let Some(mut new_root) = state.pending_root.take() {
|
||||
std::mem::swap(&mut new_root, &mut state.root_node);
|
||||
let ancestor = Some(new_root);
|
||||
let new_root = &mut state.root_node;
|
||||
let scope = state.context.scope.clone().into();
|
||||
let next_sibling = state.next_sibling.clone();
|
||||
let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor);
|
||||
state.node_ref.link(node);
|
||||
state
|
||||
.context
|
||||
.scope
|
||||
.process(ComponentLifecycleEvent::Rendered);
|
||||
UpdateEvent::Properties(props, node_ref, next_sibling) => {
|
||||
// When components are updated, a new node ref could have been passed in
|
||||
state.node_ref = node_ref;
|
||||
// When components are updated, their siblings were likely also updated
|
||||
state.next_sibling = next_sibling;
|
||||
// Only trigger changed if props were changed
|
||||
if state.context.props != props {
|
||||
state.context.props = Rc::clone(&props);
|
||||
state.component.changed(&state.context)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(
|
||||
state.vcomp_id,
|
||||
format!("update(schedule_render={})", schedule_render),
|
||||
);
|
||||
|
||||
if schedule_render {
|
||||
scheduler::push_component_render(
|
||||
self.state.as_ptr() as usize,
|
||||
RenderRunner {
|
||||
state: self.state.clone(),
|
||||
},
|
||||
RenderedRunner {
|
||||
state: self.state.clone(),
|
||||
},
|
||||
);
|
||||
// Only run from the scheduler, so no need to call `scheduler::start()`
|
||||
}
|
||||
ComponentLifecycleEvent::Rendered => {
|
||||
if let Some(mut state) = current_state.as_mut() {
|
||||
let first_render = !state.has_rendered;
|
||||
state.component.rendered(&state.context, first_render);
|
||||
state.has_rendered = true;
|
||||
state.drain_pending_updates(&self.state);
|
||||
}
|
||||
}
|
||||
ComponentLifecycleEvent::Destroy => {
|
||||
if let Some(mut state) = current_state.take() {
|
||||
state.component.destroy(&state.context);
|
||||
state.root_node.detach(&state.parent);
|
||||
state.node_ref.set(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DestroyRunner<COMP: Component> {
|
||||
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> Runnable for DestroyRunner<COMP> {
|
||||
fn run(self: Box<Self>) {
|
||||
if let Some(mut state) = self.state.borrow_mut().take() {
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "destroy");
|
||||
|
||||
state.component.destroy(&state.context);
|
||||
state.root_node.detach(&state.parent);
|
||||
state.node_ref.set(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RenderRunner<COMP: Component> {
|
||||
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> Runnable for RenderRunner<COMP> {
|
||||
fn run(self: Box<Self>) {
|
||||
if let Some(state) = self.state.borrow_mut().as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "render");
|
||||
|
||||
let mut new_root = state.component.view(&state.context);
|
||||
std::mem::swap(&mut new_root, &mut state.root_node);
|
||||
let ancestor = Some(new_root);
|
||||
let new_root = &mut state.root_node;
|
||||
let scope = state.context.scope.clone().into();
|
||||
let next_sibling = state.next_sibling.clone();
|
||||
let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor);
|
||||
state.node_ref.link(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RenderedRunner<COMP: Component> {
|
||||
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> Runnable for RenderedRunner<COMP> {
|
||||
fn run(self: Box<Self>) {
|
||||
if let Some(state) = self.state.borrow_mut().as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "rendered");
|
||||
|
||||
let first_render = !state.has_rendered;
|
||||
state.component.rendered(&state.context, first_render);
|
||||
state.has_rendered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -314,7 +321,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn test_lifecycle(props: Props, expected: &[String]) {
|
||||
fn test_lifecycle(props: Props, expected: &[&str]) {
|
||||
let document = crate::utils::document();
|
||||
let scope = Scope::<Comp>::new(None);
|
||||
let el = document.create_element("div").unwrap();
|
||||
|
@ -335,12 +342,7 @@ mod tests {
|
|||
lifecycle: lifecycle.clone(),
|
||||
..Props::default()
|
||||
},
|
||||
&[
|
||||
"create".to_string(),
|
||||
"view".to_string(),
|
||||
"child rendered".to_string(),
|
||||
"rendered(true)".to_string(),
|
||||
],
|
||||
&["create", "view", "child rendered", "rendered(true)"],
|
||||
);
|
||||
|
||||
test_lifecycle(
|
||||
|
@ -351,11 +353,11 @@ mod tests {
|
|||
..Props::default()
|
||||
},
|
||||
&[
|
||||
"create".to_string(),
|
||||
"view".to_string(),
|
||||
"child rendered".to_string(),
|
||||
"rendered(true)".to_string(),
|
||||
"update(false)".to_string(),
|
||||
"create",
|
||||
"view",
|
||||
"child rendered",
|
||||
"rendered(true)",
|
||||
"update(false)",
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -366,13 +368,13 @@ mod tests {
|
|||
..Props::default()
|
||||
},
|
||||
&[
|
||||
"create".to_string(),
|
||||
"view".to_string(),
|
||||
"child rendered".to_string(),
|
||||
"rendered(true)".to_string(),
|
||||
"update(true)".to_string(),
|
||||
"view".to_string(),
|
||||
"rendered(false)".to_string(),
|
||||
"create",
|
||||
"view",
|
||||
"child rendered",
|
||||
"rendered(true)",
|
||||
"update(true)",
|
||||
"view",
|
||||
"rendered(false)",
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -383,11 +385,11 @@ mod tests {
|
|||
..Props::default()
|
||||
},
|
||||
&[
|
||||
"create".to_string(),
|
||||
"view".to_string(),
|
||||
"child rendered".to_string(),
|
||||
"rendered(true)".to_string(),
|
||||
"update(false)".to_string(),
|
||||
"create",
|
||||
"view",
|
||||
"child rendered",
|
||||
"rendered(true)",
|
||||
"update(false)",
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -398,11 +400,11 @@ mod tests {
|
|||
..Props::default()
|
||||
},
|
||||
&[
|
||||
"create".to_string(),
|
||||
"view".to_string(),
|
||||
"child rendered".to_string(),
|
||||
"rendered(true)".to_string(),
|
||||
"update(false)".to_string(),
|
||||
"create",
|
||||
"view",
|
||||
"child rendered",
|
||||
"rendered(true)",
|
||||
"update(false)",
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -413,16 +415,17 @@ mod tests {
|
|||
..Props::default()
|
||||
},
|
||||
&[
|
||||
"create".to_string(),
|
||||
"view".to_string(),
|
||||
"child rendered".to_string(),
|
||||
"rendered(true)".to_string(),
|
||||
"update(true)".to_string(),
|
||||
"view".to_string(),
|
||||
"rendered(false)".to_string(),
|
||||
"create",
|
||||
"view",
|
||||
"child rendered",
|
||||
"rendered(true)",
|
||||
"update(true)",
|
||||
"view",
|
||||
"rendered(false)",
|
||||
],
|
||||
);
|
||||
|
||||
// This also tests render deduplication after the first render
|
||||
test_lifecycle(
|
||||
Props {
|
||||
lifecycle,
|
||||
|
@ -432,16 +435,14 @@ mod tests {
|
|||
..Props::default()
|
||||
},
|
||||
&[
|
||||
"create".to_string(),
|
||||
"view".to_string(),
|
||||
"child rendered".to_string(),
|
||||
"rendered(true)".to_string(),
|
||||
"update(true)".to_string(),
|
||||
"view".to_string(),
|
||||
"rendered(false)".to_string(),
|
||||
"update(true)".to_string(),
|
||||
"view".to_string(),
|
||||
"rendered(false)".to_string(),
|
||||
"create",
|
||||
"view",
|
||||
"child rendered",
|
||||
"rendered(true)",
|
||||
"update(true)",
|
||||
"update(true)",
|
||||
"view",
|
||||
"rendered(false)",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -76,10 +76,18 @@ pub trait Component: Sized + 'static {
|
|||
/// Components define their visual layout using a JSX-style syntax through the use of the
|
||||
/// `html!` procedural macro. The full guide to using the macro can be found in [Yew's
|
||||
/// documentation](https://yew.rs/concepts/html).
|
||||
///
|
||||
/// Note that `view()` calls do not always follow a render request from `update()` or
|
||||
/// `changed()`. Yew may optimize some calls out to reduce virtual DOM tree generation overhead.
|
||||
/// The `create()` call is always followed by a call to `view()`.
|
||||
fn view(&self, ctx: &Context<Self>) -> Html;
|
||||
|
||||
/// The `rendered` method is called after each time a Component is rendered but
|
||||
/// before the browser updates the page.
|
||||
///
|
||||
/// Note that `rendered()` calls do not always follow a render request from `update()` or
|
||||
/// `changed()`. Yew may optimize some calls out to reduce virtual DOM tree generation overhead.
|
||||
/// The `create()` call is always followed by a call to `view()` and later `rendered()`.
|
||||
#[allow(unused_variables)]
|
||||
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {}
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use super::{
|
||||
lifecycle::{
|
||||
ComponentLifecycleEvent, ComponentRunnable, ComponentState, CreateEvent, UpdateEvent,
|
||||
ComponentState, CreateRunner, DestroyRunner, RenderRunner, RenderedRunner, UpdateEvent,
|
||||
UpdateRunner,
|
||||
},
|
||||
Component,
|
||||
};
|
||||
|
@ -27,6 +28,10 @@ pub struct AnyScope {
|
|||
type_id: TypeId,
|
||||
parent: Option<Rc<AnyScope>>,
|
||||
state: Rc<dyn Any>,
|
||||
|
||||
// Used for debug logging
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) vcomp_id: u64,
|
||||
}
|
||||
|
||||
impl<COMP: Component> From<Scope<COMP>> for AnyScope {
|
||||
|
@ -35,6 +40,9 @@ impl<COMP: Component> From<Scope<COMP>> for AnyScope {
|
|||
type_id: TypeId::of::<COMP>(),
|
||||
parent: scope.parent,
|
||||
state: scope.state,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id: scope.vcomp_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +54,9 @@ impl AnyScope {
|
|||
type_id: TypeId::of::<()>(),
|
||||
parent: None,
|
||||
state: Rc::new(()),
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,12 +72,24 @@ impl AnyScope {
|
|||
|
||||
/// Attempts to downcast into a typed scope
|
||||
pub fn downcast<COMP: Component>(self) -> Scope<COMP> {
|
||||
let state = self
|
||||
.state
|
||||
.downcast::<RefCell<Option<ComponentState<COMP>>>>()
|
||||
.expect("unexpected component type");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let vcomp_id = state
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|s| s.vcomp_id)
|
||||
.unwrap_or_default();
|
||||
|
||||
Scope {
|
||||
parent: self.parent,
|
||||
state: self
|
||||
.state
|
||||
.downcast::<RefCell<Option<ComponentState<COMP>>>>()
|
||||
.expect("unexpected component type"),
|
||||
state,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,14 +139,22 @@ impl<COMP: Component> Scoped for Scope<COMP> {
|
|||
|
||||
/// Process an event to destroy a component
|
||||
fn destroy(&mut self) {
|
||||
self.process(ComponentLifecycleEvent::Destroy);
|
||||
scheduler::push_component_destroy(DestroyRunner {
|
||||
state: self.state.clone(),
|
||||
});
|
||||
// Not guaranteed to already have the scheduler started
|
||||
scheduler::start();
|
||||
}
|
||||
}
|
||||
|
||||
/// A context which allows sending messages to a component.
|
||||
pub struct Scope<COMP: Component> {
|
||||
parent: Option<Rc<AnyScope>>,
|
||||
state: Shared<Option<ComponentState<COMP>>>,
|
||||
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
|
||||
|
||||
// Used for debug logging
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) vcomp_id: u64,
|
||||
}
|
||||
|
||||
impl<COMP: Component> fmt::Debug for Scope<COMP> {
|
||||
|
@ -137,6 +168,9 @@ impl<COMP: Component> Clone for Scope<COMP> {
|
|||
Scope {
|
||||
parent: self.parent.clone(),
|
||||
state: self.state.clone(),
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id: self.vcomp_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +194,17 @@ impl<COMP: Component> Scope<COMP> {
|
|||
pub(crate) fn new(parent: Option<AnyScope>) -> Self {
|
||||
let parent = parent.map(Rc::new);
|
||||
let state = Rc::new(RefCell::new(None));
|
||||
Scope { parent, state }
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let vcomp_id = parent.as_ref().map(|p| p.vcomp_id).unwrap_or_default();
|
||||
|
||||
Scope {
|
||||
state,
|
||||
parent,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mounts a component with `props` to the specified `element` in the DOM.
|
||||
|
@ -171,6 +215,8 @@ impl<COMP: Component> Scope<COMP> {
|
|||
node_ref: NodeRef,
|
||||
props: Rc<COMP::Properties>,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(self.vcomp_id, "create placeholder");
|
||||
let placeholder = {
|
||||
let placeholder: Node = document().create_text_node("").into();
|
||||
insert_node(&placeholder, &parent, next_sibling.get().as_ref());
|
||||
|
@ -178,15 +224,24 @@ impl<COMP: Component> Scope<COMP> {
|
|||
VNode::VRef(placeholder)
|
||||
};
|
||||
|
||||
self.schedule(UpdateEvent::First.into());
|
||||
self.process(ComponentLifecycleEvent::Create(CreateEvent {
|
||||
parent,
|
||||
next_sibling,
|
||||
placeholder,
|
||||
node_ref,
|
||||
props,
|
||||
scope: self.clone(),
|
||||
}));
|
||||
scheduler::push_component_create(
|
||||
CreateRunner {
|
||||
parent,
|
||||
next_sibling,
|
||||
placeholder,
|
||||
node_ref,
|
||||
props,
|
||||
scope: self.clone(),
|
||||
},
|
||||
RenderRunner {
|
||||
state: self.state.clone(),
|
||||
},
|
||||
RenderedRunner {
|
||||
state: self.state.clone(),
|
||||
},
|
||||
);
|
||||
// Not guaranteed to already have the scheduler started
|
||||
scheduler::start();
|
||||
}
|
||||
|
||||
pub(crate) fn reuse(
|
||||
|
@ -195,28 +250,19 @@ impl<COMP: Component> Scope<COMP> {
|
|||
node_ref: NodeRef,
|
||||
next_sibling: NodeRef,
|
||||
) {
|
||||
self.process(UpdateEvent::Properties(props, node_ref, next_sibling).into());
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(self.vcomp_id, "reuse");
|
||||
|
||||
self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling));
|
||||
}
|
||||
|
||||
pub(crate) fn process(&self, event: ComponentLifecycleEvent<COMP>) {
|
||||
self.schedule(event);
|
||||
scheduler::start();
|
||||
}
|
||||
|
||||
fn schedule(&self, event: ComponentLifecycleEvent<COMP>) {
|
||||
use ComponentLifecycleEvent::*;
|
||||
|
||||
let push = match &event {
|
||||
Create(_) => scheduler::push_component_create,
|
||||
Update(_) => scheduler::push_component_update,
|
||||
Render => scheduler::push_component_render,
|
||||
Rendered => scheduler::push_component_rendered,
|
||||
Destroy => scheduler::push_component_destroy,
|
||||
};
|
||||
push(Box::new(ComponentRunnable {
|
||||
fn push_update(&self, event: UpdateEvent<COMP>) {
|
||||
scheduler::push_component_update(UpdateRunner {
|
||||
state: self.state.clone(),
|
||||
event,
|
||||
}));
|
||||
});
|
||||
// Not guaranteed to already have the scheduler started
|
||||
scheduler::start();
|
||||
}
|
||||
|
||||
/// Send a message to the component.
|
||||
|
@ -227,7 +273,7 @@ impl<COMP: Component> Scope<COMP> {
|
|||
where
|
||||
T: Into<COMP::Message>,
|
||||
{
|
||||
self.process(UpdateEvent::Message(msg.into()).into());
|
||||
self.push_update(UpdateEvent::Message(msg.into()));
|
||||
}
|
||||
|
||||
/// Send a batch of messages to the component.
|
||||
|
@ -245,7 +291,7 @@ impl<COMP: Component> Scope<COMP> {
|
|||
return;
|
||||
}
|
||||
|
||||
self.process(UpdateEvent::MessageBatch(messages).into());
|
||||
self.push_update(UpdateEvent::MessageBatch(messages));
|
||||
}
|
||||
|
||||
/// Creates a `Callback` which will send a message to the linked
|
||||
|
|
|
@ -1,20 +1,12 @@
|
|||
//! This module contains a scheduler.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{hash_map::Entry, HashMap, VecDeque};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Alias for Rc<RefCell<T>>
|
||||
pub type Shared<T> = Rc<RefCell<T>>;
|
||||
|
||||
thread_local! {
|
||||
/// This is a global scheduler suitable to schedule and run any tasks.
|
||||
///
|
||||
/// Exclusivity of mutable access is controlled by only accessing it through a set of public
|
||||
/// functions.
|
||||
static SCHEDULER: RefCell<Scheduler> = Default::default();
|
||||
}
|
||||
|
||||
/// A routine which could be run.
|
||||
pub trait Runnable {
|
||||
/// Runs a routine with a context instance.
|
||||
|
@ -26,70 +18,78 @@ pub trait Runnable {
|
|||
#[allow(missing_debug_implementations)] // todo
|
||||
struct Scheduler {
|
||||
// Main queue
|
||||
main: VecDeque<Box<dyn Runnable>>,
|
||||
main: Vec<Box<dyn Runnable>>,
|
||||
|
||||
// Component queues
|
||||
destroy: VecDeque<Box<dyn Runnable>>,
|
||||
create: VecDeque<Box<dyn Runnable>>,
|
||||
update: VecDeque<Box<dyn Runnable>>,
|
||||
render: VecDeque<Box<dyn Runnable>>,
|
||||
destroy: Vec<Box<dyn Runnable>>,
|
||||
create: Vec<Box<dyn Runnable>>,
|
||||
update: Vec<Box<dyn Runnable>>,
|
||||
render_first: VecDeque<Box<dyn Runnable>>,
|
||||
render: RenderScheduler,
|
||||
|
||||
// Stack
|
||||
rendered: Vec<Box<dyn Runnable>>,
|
||||
/// Stacks to ensure child calls are always before parent calls
|
||||
rendered_first: Vec<Box<dyn Runnable>>,
|
||||
rendered: RenderedScheduler,
|
||||
}
|
||||
|
||||
/// Execute closure with a mutable reference to the scheduler
|
||||
#[inline]
|
||||
fn with(f: impl FnOnce(&mut Scheduler)) {
|
||||
SCHEDULER.with(|s| f(&mut *s.borrow_mut()));
|
||||
fn with<R>(f: impl FnOnce(&mut Scheduler) -> R) -> R {
|
||||
thread_local! {
|
||||
/// This is a global scheduler suitable to schedule and run any tasks.
|
||||
///
|
||||
/// Exclusivity of mutable access is controlled by only accessing it through a set of public
|
||||
/// functions.
|
||||
static SCHEDULER: RefCell<Scheduler> = Default::default();
|
||||
}
|
||||
|
||||
SCHEDULER.with(|s| f(&mut *s.borrow_mut()))
|
||||
}
|
||||
|
||||
/// Push a generic Runnable to be executed
|
||||
#[inline]
|
||||
/// Push a generic [Runnable] to be executed
|
||||
pub fn push(runnable: Box<dyn Runnable>) {
|
||||
with(|s| s.main.push_back(runnable));
|
||||
with(|s| s.main.push(runnable));
|
||||
// Execute pending immediately. Necessary for runnables added outside the component lifecycle,
|
||||
// which would otherwise be delayed.
|
||||
start();
|
||||
}
|
||||
|
||||
/// Push a component creation Runnable to be executed
|
||||
#[inline]
|
||||
pub(crate) fn push_component_create(runnable: Box<dyn Runnable>) {
|
||||
with(|s| s.create.push_back(runnable));
|
||||
/// Push a component creation, first render and first rendered [Runnable]s to be executed
|
||||
pub(crate) fn push_component_create(
|
||||
create: impl Runnable + 'static,
|
||||
first_render: impl Runnable + 'static,
|
||||
first_rendered: impl Runnable + 'static,
|
||||
) {
|
||||
with(|s| {
|
||||
s.create.push(Box::new(create));
|
||||
s.render_first.push_back(Box::new(first_render));
|
||||
s.rendered_first.push(Box::new(first_rendered));
|
||||
});
|
||||
}
|
||||
|
||||
/// Push a component destruction Runnable to be executed
|
||||
#[inline]
|
||||
pub(crate) fn push_component_destroy(runnable: Box<dyn Runnable>) {
|
||||
with(|s| s.destroy.push_back(runnable));
|
||||
/// Push a component destruction [Runnable] to be executed
|
||||
pub(crate) fn push_component_destroy(runnable: impl Runnable + 'static) {
|
||||
with(|s| s.destroy.push(Box::new(runnable)));
|
||||
}
|
||||
|
||||
/// Push a component render Runnable to be executed
|
||||
#[inline]
|
||||
pub(crate) fn push_component_render(runnable: Box<dyn Runnable>) {
|
||||
with(|s| s.render.push_back(runnable));
|
||||
/// Push a component render and rendered [Runnable]s to be executed
|
||||
pub(crate) fn push_component_render(
|
||||
component_id: usize,
|
||||
render: impl Runnable + 'static,
|
||||
rendered: impl Runnable + 'static,
|
||||
) {
|
||||
with(|s| {
|
||||
s.render.schedule(component_id, Box::new(render));
|
||||
s.rendered.schedule(component_id, Box::new(rendered));
|
||||
});
|
||||
}
|
||||
|
||||
/// Push a component Runnable to be executed after a component is rendered
|
||||
#[inline]
|
||||
pub(crate) fn push_component_rendered(runnable: Box<dyn Runnable>) {
|
||||
with(|s| s.rendered.push(runnable));
|
||||
/// Push a component update [Runnable] to be executed
|
||||
pub(crate) fn push_component_update(runnable: impl Runnable + 'static) {
|
||||
with(|s| s.update.push(Box::new(runnable)));
|
||||
}
|
||||
|
||||
/// Push a component update Runnable to be executed
|
||||
#[inline]
|
||||
pub(crate) fn push_component_update(runnable: Box<dyn Runnable>) {
|
||||
with(|s| s.update.push_back(runnable));
|
||||
}
|
||||
|
||||
/// Push a batch of component updates to be executed
|
||||
#[inline]
|
||||
pub(crate) fn push_component_updates(it: impl IntoIterator<Item = Box<dyn Runnable>>) {
|
||||
with(|s| s.update.extend(it));
|
||||
}
|
||||
|
||||
/// Execute any pending Runnables
|
||||
/// Execute any pending [Runnable]s
|
||||
pub(crate) fn start() {
|
||||
thread_local! {
|
||||
// The lock is used to prevent recursion. If the lock cannot be acquired, it is because the
|
||||
|
@ -99,23 +99,161 @@ pub(crate) fn start() {
|
|||
|
||||
LOCK.with(|l| {
|
||||
if let Ok(_lock) = l.try_borrow_mut() {
|
||||
while let Some(runnable) = SCHEDULER.with(|s| s.borrow_mut().next_runnable()) {
|
||||
runnable.run();
|
||||
let mut queue = vec![];
|
||||
loop {
|
||||
with(|s| s.fill_queue(&mut queue));
|
||||
if queue.is_empty() {
|
||||
break;
|
||||
}
|
||||
for r in queue.drain(..) {
|
||||
r.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
/// Pop next Runnable to be executed according to Runnable type execution priority
|
||||
fn next_runnable(&mut self) -> Option<Box<dyn Runnable>> {
|
||||
self.destroy
|
||||
.pop_front()
|
||||
.or_else(|| self.create.pop_front())
|
||||
.or_else(|| self.update.pop_front())
|
||||
.or_else(|| self.render.pop_front())
|
||||
.or_else(|| self.rendered.pop())
|
||||
.or_else(|| self.main.pop_front())
|
||||
/// Fill vector with tasks to be executed according to Runnable type execution priority
|
||||
///
|
||||
/// This method is optimized for typical usage, where possible, but does not break on
|
||||
/// non-typical usage (like scheduling renders in [crate::Component::create()] or
|
||||
/// [crate::Component::rendered()] calls).
|
||||
fn fill_queue(&mut self, to_run: &mut Vec<Box<dyn Runnable>>) {
|
||||
// Placed first to avoid as much needless work as possible, handling all the other events.
|
||||
// Drained completely, because they are the highest priority events anyway.
|
||||
to_run.append(&mut self.destroy);
|
||||
|
||||
// Create events can be batched, as they are typically just for object creation
|
||||
to_run.append(&mut self.create);
|
||||
|
||||
// First render must never be skipped and takes priority over main, because it may need
|
||||
// to init `NodeRef`s
|
||||
//
|
||||
// Should be processed one at time, because they can spawn more create and rendered events
|
||||
// for their children.
|
||||
if let Some(r) = self.render_first.pop_front() {
|
||||
to_run.push(r);
|
||||
}
|
||||
|
||||
// These typically do nothing and don't spawn any other events - can be batched.
|
||||
// Should be run only after all first renders have finished.
|
||||
if !to_run.is_empty() {
|
||||
return;
|
||||
}
|
||||
to_run.extend(self.rendered_first.drain(..).rev());
|
||||
|
||||
// Updates are after the first render to ensure we always have the entire child tree
|
||||
// rendered, once an update is processed.
|
||||
//
|
||||
// Can be batched, as they can cause only non-first renders.
|
||||
to_run.append(&mut self.update);
|
||||
|
||||
// Likely to cause duplicate renders via component updates, so placed before them
|
||||
to_run.append(&mut self.main);
|
||||
|
||||
// Run after all possible updates to avoid duplicate renders.
|
||||
//
|
||||
// Should be processed one at time, because they can spawn more create and first render
|
||||
// events for their children.
|
||||
if !to_run.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(r) = self.render.pop() {
|
||||
to_run.push(r);
|
||||
}
|
||||
|
||||
// These typically do nothing and don't spawn any other events - can be batched.
|
||||
// Should be run only after all renders have finished.
|
||||
if !to_run.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.rendered.drain_into(to_run);
|
||||
}
|
||||
}
|
||||
|
||||
/// Task to be executed for specific component
|
||||
struct QueueTask {
|
||||
/// Tasks in the queue to skip for this component
|
||||
skip: usize,
|
||||
|
||||
/// Runnable to execute
|
||||
runnable: Box<dyn Runnable>,
|
||||
}
|
||||
|
||||
/// Scheduler for non-first component renders with deduplication
|
||||
#[derive(Default)]
|
||||
struct RenderScheduler {
|
||||
/// Task registry by component ID
|
||||
tasks: HashMap<usize, QueueTask>,
|
||||
|
||||
/// Task queue by component ID
|
||||
queue: VecDeque<usize>,
|
||||
}
|
||||
|
||||
impl RenderScheduler {
|
||||
/// Schedule render task execution
|
||||
fn schedule(&mut self, component_id: usize, runnable: Box<dyn Runnable>) {
|
||||
self.queue.push_back(component_id);
|
||||
match self.tasks.entry(component_id) {
|
||||
Entry::Vacant(e) => {
|
||||
e.insert(QueueTask { skip: 0, runnable });
|
||||
}
|
||||
Entry::Occupied(mut e) => {
|
||||
let v = e.get_mut();
|
||||
v.skip += 1;
|
||||
|
||||
// Technically the 2 runners should be functionally identical, but might as well
|
||||
// overwrite it for good measure, accounting for future changes. We have it here
|
||||
// anyway.
|
||||
v.runnable = runnable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to pop a task from the queue, if any
|
||||
fn pop(&mut self) -> Option<Box<dyn Runnable>> {
|
||||
while let Some(id) = self.queue.pop_front() {
|
||||
match self.tasks.entry(id) {
|
||||
Entry::Occupied(mut e) => {
|
||||
let v = e.get_mut();
|
||||
if v.skip == 0 {
|
||||
return Some(e.remove().runnable);
|
||||
}
|
||||
v.skip -= 1;
|
||||
}
|
||||
Entry::Vacant(_) => (),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Deduplicating scheduler for component rendered calls with deduplication
|
||||
#[derive(Default)]
|
||||
struct RenderedScheduler {
|
||||
/// Task registry by component ID
|
||||
tasks: HashMap<usize, Box<dyn Runnable>>,
|
||||
|
||||
/// Task stack by component ID
|
||||
stack: Vec<usize>,
|
||||
}
|
||||
|
||||
impl RenderedScheduler {
|
||||
/// Schedule rendered task execution
|
||||
fn schedule(&mut self, component_id: usize, runnable: Box<dyn Runnable>) {
|
||||
if self.tasks.insert(component_id, runnable).is_none() {
|
||||
self.stack.push(component_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain all tasks into `dst`, if any
|
||||
fn drain_into(&mut self, dst: &mut Vec<Box<dyn Runnable>>) {
|
||||
for id in self.stack.drain(..).rev() {
|
||||
if let Some(t) = self.tasks.remove(&id) {
|
||||
dst.push(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,34 @@ use std::ops::Deref;
|
|||
use std::rc::Rc;
|
||||
use web_sys::Element;
|
||||
|
||||
thread_local! {
|
||||
#[cfg(debug_assertions)]
|
||||
static EVENT_HISTORY: std::cell::RefCell<std::collections::HashMap<u64, Vec<String>>>
|
||||
= Default::default();
|
||||
}
|
||||
|
||||
/// Push [VComp] event to lifecycle debugging registry
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn log_event(vcomp_id: u64, event: impl ToString) {
|
||||
EVENT_HISTORY.with(|h| {
|
||||
h.borrow_mut()
|
||||
.entry(vcomp_id)
|
||||
.or_default()
|
||||
.push(event.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
/// Get [VComp] event log from lifecycle debugging registry
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn get_event_log(vcomp_id: u64) -> Vec<String> {
|
||||
EVENT_HISTORY.with(|h| {
|
||||
h.borrow()
|
||||
.get(&vcomp_id)
|
||||
.map(|l| (*l).clone())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
/// A virtual component.
|
||||
pub struct VComp {
|
||||
type_id: TypeId,
|
||||
|
@ -16,6 +44,10 @@ pub struct VComp {
|
|||
props: Option<Box<dyn Mountable>>,
|
||||
pub(crate) node_ref: NodeRef,
|
||||
pub(crate) key: Option<Key>,
|
||||
|
||||
/// Used for debug logging
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) id: u64,
|
||||
}
|
||||
|
||||
impl Clone for VComp {
|
||||
|
@ -24,12 +56,18 @@ impl Clone for VComp {
|
|||
panic!("Mounted components are not allowed to be cloned!");
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
log_event(self.id, "clone");
|
||||
|
||||
Self {
|
||||
type_id: self.type_id,
|
||||
scope: None,
|
||||
props: self.props.as_ref().map(|m| m.copy()),
|
||||
node_ref: self.node_ref.clone(),
|
||||
key: self.key.clone(),
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,12 +135,40 @@ impl VComp {
|
|||
props: Some(Box::new(PropsWrapper::<COMP>::new(props))),
|
||||
scope: None,
|
||||
key,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
id: {
|
||||
thread_local! {
|
||||
static ID_COUNTER: std::cell::RefCell<u64> = Default::default();
|
||||
}
|
||||
|
||||
ID_COUNTER.with(|c| {
|
||||
let c = &mut *c.borrow_mut();
|
||||
*c += 1;
|
||||
*c
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn root_vnode(&self) -> Option<impl Deref<Target = VNode> + '_> {
|
||||
self.scope.as_ref().and_then(|scope| scope.root_vnode())
|
||||
}
|
||||
|
||||
/// Take ownership of [Box<dyn Scoped>] or panic with error message, if component is not mounted
|
||||
#[inline]
|
||||
fn take_scope(&mut self) -> Box<dyn Scoped> {
|
||||
self.scope.take().unwrap_or_else(|| {
|
||||
#[cfg(not(debug_assertions))]
|
||||
panic!("no scope; VComp should be mounted");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
panic!(
|
||||
"no scope; VComp should be mounted after: {:?}",
|
||||
get_event_log(self.id)
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
trait Mountable {
|
||||
|
@ -156,7 +222,7 @@ impl<COMP: Component> Mountable for PropsWrapper<COMP> {
|
|||
|
||||
impl VDiff for VComp {
|
||||
fn detach(&mut self, _parent: &Element) {
|
||||
self.scope.take().expect("VComp is not mounted").destroy();
|
||||
self.take_scope().destroy();
|
||||
}
|
||||
|
||||
fn apply(
|
||||
|
@ -173,7 +239,7 @@ impl VDiff for VComp {
|
|||
// If the ancestor is the same type, reuse it and update its properties
|
||||
if self.type_id == vcomp.type_id && self.key == vcomp.key {
|
||||
self.node_ref.reuse(vcomp.node_ref.clone());
|
||||
let scope = vcomp.scope.take().expect("VComp is not mounted");
|
||||
let scope = vcomp.take_scope();
|
||||
mountable.reuse(self.node_ref.clone(), scope.borrow(), next_sibling);
|
||||
self.scope = Some(scope);
|
||||
return vcomp.node_ref.clone();
|
||||
|
|
|
@ -57,7 +57,16 @@ impl VNode {
|
|||
let text_node = vtext.reference.as_ref().expect("VText is not mounted");
|
||||
text_node.clone().into()
|
||||
}
|
||||
VNode::VComp(vcomp) => vcomp.node_ref.get().expect("VComp is not mounted"),
|
||||
VNode::VComp(vcomp) => vcomp.node_ref.get().unwrap_or_else(|| {
|
||||
#[cfg(not(debug_assertions))]
|
||||
panic!("no node_ref; VComp should be mounted");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
panic!(
|
||||
"no node_ref; VComp should be mounted after: {:?}",
|
||||
crate::virtual_dom::vcomp::get_event_log(vcomp.id),
|
||||
);
|
||||
}),
|
||||
VNode::VList(vlist) => vlist.get(0).expect("VList is not mounted").first_node(),
|
||||
VNode::VRef(node) => node.clone(),
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue