From 4b3332907bba5d269cd2096c1e2921c7b0417025 Mon Sep 17 00:00:00 2001 From: youxingzhi Date: Fri, 13 Sep 2024 11:30:58 +0800 Subject: [PATCH 1/3] skip bailout for suspense --- packages/react-reconciler/src/begin_work.rs | 72 ++++++++++---------- packages/react-reconciler/src/fiber_throw.rs | 25 +++++-- packages/react-reconciler/src/work_loop.rs | 7 +- 3 files changed, 63 insertions(+), 41 deletions(-) diff --git a/packages/react-reconciler/src/begin_work.rs b/packages/react-reconciler/src/begin_work.rs index aba2024..b9706aa 100644 --- a/packages/react-reconciler/src/begin_work.rs +++ b/packages/react-reconciler/src/begin_work.rs @@ -57,42 +57,44 @@ pub fn begin_work( }; // TODO work with suspense - // let current = { work_in_progress.borrow().alternate.clone() }; + let current = { work_in_progress.borrow().alternate.clone() }; - // if current.is_some() { - // let current = current.clone().unwrap(); - // let old_props = current.borrow().memoized_props.clone(); - // let old_type = current.borrow()._type.clone(); - // let new_props = work_in_progress.borrow().pending_props.clone(); - // let new_type = work_in_progress.borrow()._type.clone(); - // if !Object::is(&old_props, &new_props) || !Object::is(&old_type, &new_type) { - // unsafe { DID_RECEIVE_UPDATE = true } - // } else { - // let has_scheduled_update_or_context = - // check_scheduled_update_or_context(current.clone(), render_lane.clone()); - // // The current fiber lane is not included in render_lane - // // TODO context - // if !has_scheduled_update_or_context { - // unsafe { DID_RECEIVE_UPDATE = false } - // match work_in_progress.borrow().tag { - // WorkTag::ContextProvider => { - // let new_value = derive_from_js_value( - // &work_in_progress.borrow().memoized_props, - // "value", - // ); - // let context = - // derive_from_js_value(&work_in_progress.borrow()._type, "_context"); - // push_provider(&context, new_value); - // } - // _ => {} - // } - // return Ok(bailout_on_already_finished_work( - // work_in_progress, - // render_lane, - // )); - // } - // } - // } + if current.is_some() { + let current = current.clone().unwrap(); + let old_props = current.borrow().memoized_props.clone(); + let old_type = current.borrow()._type.clone(); + let new_props = work_in_progress.borrow().pending_props.clone(); + let new_type = work_in_progress.borrow()._type.clone(); + if !Object::is(&old_props, &new_props) || !Object::is(&old_type, &new_type) { + unsafe { DID_RECEIVE_UPDATE = true } + } else { + let has_scheduled_update_or_context = + check_scheduled_update_or_context(current.clone(), render_lane.clone()); + // The current fiber lane is not included in render_lane + // TODO context + if !has_scheduled_update_or_context + && current.borrow().tag != WorkTag::SuspenseComponent + { + unsafe { DID_RECEIVE_UPDATE = false } + match work_in_progress.borrow().tag { + WorkTag::ContextProvider => { + let new_value = derive_from_js_value( + &work_in_progress.borrow().memoized_props, + "value", + ); + let context = + derive_from_js_value(&work_in_progress.borrow()._type, "_context"); + push_provider(&context, new_value); + } + _ => {} + } + return Ok(bailout_on_already_finished_work( + work_in_progress, + render_lane, + )); + } + } + } work_in_progress.borrow_mut().lanes = Lane::NoLane; // if current.is_some() { diff --git a/packages/react-reconciler/src/fiber_throw.rs b/packages/react-reconciler/src/fiber_throw.rs index 0b4cfdb..c7aa569 100644 --- a/packages/react-reconciler/src/fiber_throw.rs +++ b/packages/react-reconciler/src/fiber_throw.rs @@ -9,11 +9,20 @@ use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; use web_sys::js_sys::Function; use crate::{ - fiber::FiberRootNode, fiber_flags::Flags, fiber_lanes::Lane, - suspense_context::get_suspense_handler, work_loop::ensure_root_is_scheduled, JsValueKey, + fiber::{FiberNode, FiberRootNode}, + fiber_flags::Flags, + fiber_lanes::Lane, + suspense_context::get_suspense_handler, + work_loop::{ensure_root_is_scheduled, mark_update_lane_from_fiber_to_root}, + JsValueKey, }; -fn attach_ping_listener(root: Rc>, wakeable: JsValue, lane: Lane) { +fn attach_ping_listener( + root: Rc>, + source_fiber: Rc>, + wakeable: JsValue, + lane: Lane, +) { let mut ping_cache_option: Option>>>> = root.borrow().ping_cache.clone(); let mut ping_cache: HashMap>>>; @@ -54,6 +63,7 @@ fn attach_ping_listener(root: Rc>, wakeable: JsValue, lan } root.clone().borrow_mut().mark_root_updated(lane.clone()); root.clone().borrow_mut().mark_root_pinged(lane.clone()); + mark_update_lane_from_fiber_to_root(source_fiber.clone(), lane.clone()); ensure_root_is_scheduled(root.clone()); }) as Box); let ping = closure.as_ref().unchecked_ref::().clone(); @@ -63,7 +73,12 @@ fn attach_ping_listener(root: Rc>, wakeable: JsValue, lan } } -pub fn throw_exception(root: Rc>, value: JsValue, lane: Lane) { +pub fn throw_exception( + root: Rc>, + source_fiber: Rc>, + value: JsValue, + lane: Lane, +) { if !value.is_null() && type_of(&value, "object") && derive_from_js_value(&value, "then").is_function() @@ -74,6 +89,6 @@ pub fn throw_exception(root: Rc>, value: JsValue, lane: L suspense_boundary.borrow_mut().flags |= Flags::ShouldCapture; } - attach_ping_listener(root, value, lane) + attach_ping_listener(root, source_fiber, value, lane) } } diff --git a/packages/react-reconciler/src/work_loop.rs b/packages/react-reconciler/src/work_loop.rs index 3450f69..c9c14ec 100644 --- a/packages/react-reconciler/src/work_loop.rs +++ b/packages/react-reconciler/src/work_loop.rs @@ -520,7 +520,12 @@ fn throw_and_unwind_work_loop( lane: Lane, ) { reset_hooks_on_unwind(unit_of_work.clone()); - throw_exception(root.clone(), thrown_value, lane.clone()); + throw_exception( + root.clone(), + unit_of_work.clone(), + thrown_value, + lane.clone(), + ); unwind_unit_of_work(unit_of_work); } From 6c16637fe23fa09784af33b7b26c1364450380b7 Mon Sep 17 00:00:00 2001 From: youxingzhi Date: Fri, 13 Sep 2024 11:45:37 +0800 Subject: [PATCH 2/3] multi suspense demo --- examples/hello-world/src/suspense/index.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/hello-world/src/suspense/index.tsx b/examples/hello-world/src/suspense/index.tsx index 6ce2e81..17b424d 100644 --- a/examples/hello-world/src/suspense/index.tsx +++ b/examples/hello-world/src/suspense/index.tsx @@ -19,14 +19,21 @@ function fetchData(id, timeout) { export default function App() { return ( - loading}> - + Out loading}> + + Inner loading}> + + ) } -function Child() { - const {data} = use(fetchData(1, 1000)) +function Child({id, timeout}) { + const {data} = use(fetchData(id, timeout)) - return {data} + return ( +
+ {id}:{data} +
+ ) } From 82b3ba9d4841e603f7cf21bfd76af403fe663cfa Mon Sep 17 00:00:00 2001 From: youxingzhi Date: Thu, 19 Sep 2024 17:07:13 +0800 Subject: [PATCH 3/3] add lazy --- examples/hello-world/src/App.tsx | 2 +- examples/hello-world/src/lazy/Cpn.tsx | 3 ++ examples/hello-world/src/lazy/index.tsx | 19 ++++++++ packages/react-reconciler/src/begin_work.rs | 16 +++++++ packages/react-reconciler/src/fiber.rs | 5 +- packages/react-reconciler/src/work_loop.rs | 17 +++++-- packages/react-reconciler/src/work_tags.rs | 1 + packages/react/src/lazy.rs | 52 +++++++++++++++++++++ packages/react/src/lib.rs | 29 +++++++++++- packages/shared/src/lib.rs | 1 + 10 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 examples/hello-world/src/lazy/Cpn.tsx create mode 100644 examples/hello-world/src/lazy/index.tsx create mode 100644 packages/react/src/lazy.rs diff --git a/examples/hello-world/src/App.tsx b/examples/hello-world/src/App.tsx index a8a8fb1..eb18dd9 100644 --- a/examples/hello-world/src/App.tsx +++ b/examples/hello-world/src/App.tsx @@ -1 +1 @@ -export {default} from './suspense' +export {default} from './lazy' diff --git a/examples/hello-world/src/lazy/Cpn.tsx b/examples/hello-world/src/lazy/Cpn.tsx new file mode 100644 index 0000000..d525148 --- /dev/null +++ b/examples/hello-world/src/lazy/Cpn.tsx @@ -0,0 +1,3 @@ +export default function Cpn() { + return
Cpn
+} diff --git a/examples/hello-world/src/lazy/index.tsx b/examples/hello-world/src/lazy/index.tsx new file mode 100644 index 0000000..971267c --- /dev/null +++ b/examples/hello-world/src/lazy/index.tsx @@ -0,0 +1,19 @@ +import {Suspense, lazy} from 'react' + +function delay(promise) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(promise) + }, 2000) + }) +} + +const Cpn = lazy(() => import('./Cpn').then((res) => delay(res))) + +export default function App() { + return ( + loading}> + + + ) +} diff --git a/packages/react-reconciler/src/begin_work.rs b/packages/react-reconciler/src/begin_work.rs index b9706aa..4d32fd0 100644 --- a/packages/react-reconciler/src/begin_work.rs +++ b/packages/react-reconciler/src/begin_work.rs @@ -119,9 +119,25 @@ pub fn begin_work( WorkTag::Fragment => Ok(update_fragment(work_in_progress.clone())), WorkTag::SuspenseComponent => Ok(update_suspense_component(work_in_progress.clone())), WorkTag::OffscreenComponent => Ok(update_offscreen_component(work_in_progress.clone())), + WorkTag::LazyComponent => update_lazy_component(work_in_progress.clone(), render_lane), }; } +fn update_lazy_component( + work_in_progress: Rc>, + render_lane: Lane, +) -> Result>>, JsValue> { + let lazy_type = { work_in_progress.borrow()._type.clone() }; + let payload = derive_from_js_value(&lazy_type, "_payload"); + let init_jsvalue = derive_from_js_value(&lazy_type, "_init"); + let init = init_jsvalue.dyn_ref::().unwrap(); + let Component = init.call1(&JsValue::null(), &payload)?; + work_in_progress.borrow_mut()._type = Component.clone(); + work_in_progress.borrow_mut().tag = WorkTag::FunctionComponent; + let child = update_function_component(work_in_progress, Component.clone(), render_lane); + child +} + fn mount_suspense_fallback_children( work_in_progress: Rc>, primary_children: JsValue, diff --git a/packages/react-reconciler/src/fiber.rs b/packages/react-reconciler/src/fiber.rs index 09a72c5..664c47d 100644 --- a/packages/react-reconciler/src/fiber.rs +++ b/packages/react-reconciler/src/fiber.rs @@ -10,7 +10,8 @@ use wasm_bindgen::JsValue; use web_sys::js_sys::Reflect; use shared::{ - derive_from_js_value, log, type_of, REACT_MEMO_TYPE, REACT_PROVIDER_TYPE, REACT_SUSPENSE_TYPE, + derive_from_js_value, log, type_of, REACT_LAZY_TYPE, REACT_MEMO_TYPE, REACT_PROVIDER_TYPE, + REACT_SUSPENSE_TYPE, }; use crate::fiber_context::ContextItem; @@ -179,6 +180,8 @@ impl FiberNode { fiber_tag = WorkTag::ContextProvider; } else if _typeof == REACT_MEMO_TYPE { fiber_tag = WorkTag::MemoComponent; + } else if _typeof == REACT_LAZY_TYPE { + fiber_tag = WorkTag::LazyComponent; } else { log!("Unsupported type {:?}", _type); } diff --git a/packages/react-reconciler/src/work_loop.rs b/packages/react-reconciler/src/work_loop.rs index c9c14ec..5a965ee 100644 --- a/packages/react-reconciler/src/work_loop.rs +++ b/packages/react-reconciler/src/work_loop.rs @@ -9,7 +9,7 @@ use scheduler::{ unstable_cancel_callback, unstable_schedule_callback_no_delay, unstable_should_yield_to_host, Priority, }; -use shared::{is_dev, log}; +use shared::{derive_from_js_value, is_dev, log, type_of}; use crate::begin_work::begin_work; use crate::commit_work::{ @@ -42,7 +42,9 @@ static ROOT_COMPLETED: u8 = 2; static ROOT_DID_NOT_COMPLETE: u8 = 3; static NOT_SUSPENDED: u8 = 0; -static SUSPENDED_ON_DATA: u8 = 6; +static SUSPENDED_ON_ERROR: u8 = 1; +static SUSPENDED_ON_DATA: u8 = 2; +static SUSPENDED_ON_DEPRECATED_THROW_PROMISE: u8 = 4; pub fn schedule_update_on_fiber(fiber: Rc>, lane: Lane) { if is_dev() { @@ -505,7 +507,16 @@ fn handle_throw(root: Rc>, mut thrown_value: JsValue) { unsafe { WORK_IN_PROGRESS_SUSPENDED_REASON = SUSPENDED_ON_DATA }; thrown_value = get_suspense_thenable(); } else { - // TODO + let is_wakeable = !thrown_value.is_null() + && type_of(&thrown_value, "object") + && derive_from_js_value(&thrown_value, "then").is_function(); + unsafe { + WORK_IN_PROGRESS_SUSPENDED_REASON = if is_wakeable { + SUSPENDED_ON_DEPRECATED_THROW_PROMISE + } else { + SUSPENDED_ON_ERROR + }; + }; } unsafe { diff --git a/packages/react-reconciler/src/work_tags.rs b/packages/react-reconciler/src/work_tags.rs index 1246990..9302b80 100644 --- a/packages/react-reconciler/src/work_tags.rs +++ b/packages/react-reconciler/src/work_tags.rs @@ -9,4 +9,5 @@ pub enum WorkTag { SuspenseComponent = 13, OffscreenComponent = 14, MemoComponent = 15, + LazyComponent = 16, } diff --git a/packages/react/src/lazy.rs b/packages/react/src/lazy.rs new file mode 100644 index 0000000..7638fc4 --- /dev/null +++ b/packages/react/src/lazy.rs @@ -0,0 +1,52 @@ +use js_sys::{Function, Reflect}; +use shared::derive_from_js_value; +use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; + +pub static UNINITIALIZED: i8 = -1; +static PENDING: i8 = 0; +static RESOLVED: i8 = 1; +static REJECTED: i8 = 2; + +pub fn lazy_initializer(payload: JsValue) -> Result { + let status = derive_from_js_value(&payload, "_status"); + if status == UNINITIALIZED { + let ctor = derive_from_js_value(&payload, "_result"); + let ctor_fn = ctor.dyn_ref::().unwrap(); + let thenable = ctor_fn.call0(ctor_fn).unwrap(); + let then_jsvalue = derive_from_js_value(&thenable, "then"); + let then = then_jsvalue.dyn_ref::().unwrap(); + + let payload1 = payload.clone(); + let on_resolve_closure = Closure::wrap(Box::new(move |module: JsValue| { + Reflect::set(&payload1, &"_status".into(), &JsValue::from(RESOLVED)); + Reflect::set(&payload1, &"_result".into(), &module); + }) as Box ()>); + let on_resolve = on_resolve_closure + .as_ref() + .unchecked_ref::() + .clone(); + on_resolve_closure.forget(); + + let payload2 = payload.clone(); + let on_reject_closure = Closure::wrap(Box::new(move |err: JsValue| { + Reflect::set(&payload2, &"_status".into(), &JsValue::from(REJECTED)); + Reflect::set(&payload2, &"_result".into(), &err); + }) as Box ()>); + let on_reject = on_reject_closure + .as_ref() + .unchecked_ref::() + .clone(); + + then.call2(&thenable, &on_resolve, &on_reject); + + Reflect::set(&payload, &"_status".into(), &JsValue::from(PENDING)); + Reflect::set(&payload, &"_result".into(), &thenable); + } + + if status == RESOLVED { + let module = derive_from_js_value(&payload, "_result"); + return Ok(derive_from_js_value(&module, "default")); + } else { + return Err(derive_from_js_value(&payload, "_result")); + } +} diff --git a/packages/react/src/lib.rs b/packages/react/src/lib.rs index 387816b..de09020 100644 --- a/packages/react/src/lib.rs +++ b/packages/react/src/lib.rs @@ -1,14 +1,16 @@ -use js_sys::{Array, Object, Reflect, JSON}; +use js_sys::{Array, Function, Object, Reflect, JSON}; +use lazy::{lazy_initializer, UNINITIALIZED}; use wasm_bindgen::prelude::*; use shared::{ - derive_from_js_value, REACT_CONTEXT_TYPE, REACT_ELEMENT_TYPE, REACT_MEMO_TYPE, + derive_from_js_value, REACT_CONTEXT_TYPE, REACT_ELEMENT_TYPE, REACT_LAZY_TYPE, REACT_MEMO_TYPE, REACT_PROVIDER_TYPE, }; use crate::current_dispatcher::CURRENT_DISPATCHER; pub mod current_dispatcher; +mod lazy; fn resolve_key(val: &JsValue) -> JsValue { if val.is_undefined() { @@ -199,3 +201,26 @@ pub fn memo(_type: &JsValue, compare: &JsValue) -> JsValue { ); fiber_type.into() } + +#[wasm_bindgen] +pub fn lazy(ctor: &JsValue) -> JsValue { + let payload = Object::new(); + Reflect::set(&payload, &"_status".into(), &JsValue::from(UNINITIALIZED)); + Reflect::set(&payload, &"_result".into(), ctor); + + let lazy_type = Object::new(); + + Reflect::set( + &lazy_type, + &"$$typeof".into(), + &JsValue::from_str(REACT_LAZY_TYPE), + ); + Reflect::set(&lazy_type, &"_payload".into(), &payload); + let closure = Closure::wrap( + Box::new(lazy_initializer) as Box Result> + ); + let f = closure.as_ref().unchecked_ref::().clone(); + closure.forget(); + Reflect::set(&lazy_type, &"_init".into(), &f); + lazy_type.into() +} diff --git a/packages/shared/src/lib.rs b/packages/shared/src/lib.rs index 5c67af1..c121773 100644 --- a/packages/shared/src/lib.rs +++ b/packages/shared/src/lib.rs @@ -5,6 +5,7 @@ use web_sys::wasm_bindgen::{JsCast, JsValue}; pub static REACT_ELEMENT_TYPE: &str = "react.element"; pub static REACT_CONTEXT_TYPE: &str = "react.context"; pub static REACT_PROVIDER_TYPE: &str = "react.provider"; +pub static REACT_LAZY_TYPE: &str = "react.lazy"; pub static REACT_MEMO_TYPE: &str = "react.memo"; pub static REACT_SUSPENSE_TYPE: &str = "react.suspense"; pub static REACT_FRAGMENT_TYPE: &str = "react.fragment";