Based on big-react,I am going to implement React v18 core features from scratch using WASM and Rust.
Code Repository:https://github.com/ParadeTo/big-react-wasm
The tag related to this article:v15
The details of this update can be seen here. Let's go through the entire process below.
Like useState
, we first need to export this method from the react
package. It takes two parameters:
#[wasm_bindgen(js_name = useEffect)]
pub unsafe fn use_effect(create: &JsValue, deps: &JsValue) {
let use_effect = &CURRENT_DISPATCHER.current.as_ref().unwrap().use_effect;
use_effect.call2(&JsValue::null(), create, deps);
}
Next, we need to implement mount_effect
and update_effect
for the initial render and updates, respectively. mount_effect
adds a new Hook
node to the linked list of Hooks on the FiberNode
, with its memoized_state
property pointing to an Effect
object. This object is also added to the update_queue
on the FiberNode
, which is a circular queue. Additionally, the FiberNode
is marked with PassiveEffect
:
The work of update_effect
is similar to mount_effect
, updating the Effect
node, but it performs a shallow comparison of the incoming deps
with the previous prev_deps
. If they are all the same, it will not mark the FiberNode
with PassiveEffect
.
The properties included in Effect
are as follows:
pub struct Effect {
pub tag: Flags,
pub create: Function,
pub destroy: JsValue,
pub deps: JsValue,
pub next: Option<Rc<RefCell<Effect>>>,
}
During the Render phase, no changes are needed. In the Commit phase, we need to add logic to handle useEffect
before commit_mutation_effects
:
// useEffect
let root_cloned = root.clone();
let passive_mask = get_passive_mask();
if flags.clone() & passive_mask.clone() != Flags::NoFlags
|| subtree_flags.clone() & passive_mask != Flags::NoFlags
{
if unsafe { !ROOT_DOES_HAVE_PASSIVE_EFFECTS } {
unsafe { ROOT_DOES_HAVE_PASSIVE_EFFECTS = true }
let closure = Closure::wrap(Box::new(move || {
flush_passive_effects(root_cloned.borrow().pending_passive_effects.clone());
}) as Box<dyn Fn()>);
let function = closure.as_ref().unchecked_ref::<Function>().clone();
closure.forget();
unstable_schedule_callback_no_delay(Priority::NormalPriority, function);
}
}
Here, we use the scheduler
implemented in the previous article to schedule a task to execute the flush_passive_effects
method:
fn flush_passive_effects(pending_passive_effects: Rc<RefCell<PendingPassiveEffects>>) {
unsafe {
if EXECUTION_CONTEXT
.contains(ExecutionContext::RenderContext | ExecutionContext::CommitContext)
{
log!("Cannot execute useEffect callback in React work loop")
}
for effect in &pending_passive_effects.borrow().unmount {
CommitWork::commit_hook_effect_list_destroy(Flags::Passive, effect.clone());
}
pending_passive_effects.borrow_mut().unmount = vec![];
for effect in &pending_passive_effects.borrow().update {
CommitWork::commit_hook_effect_list_unmount(
Flags::Passive | Flags::HookHasEffect,
effect.clone(),
);
}
for effect in &pending_passive_effects.borrow().update {
CommitWork::commit_hook_effect_list_mount(
Flags::Passive | Flags::HookHasEffect,
effect.clone(),
);
}
pending_passive_effects.borrow_mut().update = vec![];
}
}
The pending_passive_effects
here is a property on the FiberRootNode
, used to store the Effect
that needs to be executed this time:
pub struct PendingPassiveEffects {
pub unmount: Vec<Rc<RefCell<Effect>>>,
pub update: Vec<Rc<RefCell<Effect>>>,
}
Among them, the Effect
that needs to be handled due to component unmounting is saved in unmount
, and the Effect
that needs to be handled due to updates is saved in update
. From the code, we can see that the Effect
due to component unmounting is handled first, even if the component is later in the sequence, like in this example:
function App() {
const [num, updateNum] = useState(0)
return (
<ul
onClick={(e) => {
updateNum((num: number) => num + 1)
}}>
<Child1 num={num} />
{num === 1 ? null : <Child2 num={num} />}
</ul>
)
}
function Child1({num}: {num: number}) {
useEffect(() => {
console.log('child1 create')
return () => {
console.log('child1 destroy')
}
}, [num])
return <div>child1 {num}</div>
}
function Child2({num}: {num: number}) {
useEffect(() => {
console.log('child2 create')
return () => {
console.log('child2 destroy')
}
}, [num])
return <div>child2 {num}</div>
}
After clicking, the destroy
of Child2
's useEffect
will be executed first, printing child2 destroy
. But if it's changed to this:
function App() {
const [num, updateNum] = useState(0)
return (
<ul
onClick={(e) => {
updateNum((num: number) => num + 1)
}}>
<Child1 num={num} />
<Child2 num={num} />
</ul>
)
}
After clicking, the destroy
of Child1
's useEffect
will be executed first, printing child1 destroy
.
So when are the Effect
in pending_passive_effects
added? The answer is in commit_mutation_effects
, there are two situations:
- If the
FiberNode
node is marked for deletion and is of theFunctionComponent
type, then theEffect
in theupdate_queue
needs to be added to theunmount
list inpending_passive_effects
.
fn commit_deletion(
&self,
child_to_delete: Rc<RefCell<FiberNode>>,
root: Rc<RefCell<FiberRootNode>>,
) {
let first_host_fiber: Rc<RefCell<Option<Rc<RefCell<FiberNode>>>>> =
Rc::new(RefCell::new(None));
self.commit_nested_unmounts(child_to_delete.clone(), |unmount_fiber| {
let cloned = first_host_fiber.clone();
match unmount_fiber.borrow().tag {
WorkTag::FunctionComponent => {
CommitWork::commit_passive_effect(
unmount_fiber.clone(),
root.clone(),
"unmount",
);
}
...
}
}
}
- If the
FiberNode
node is marked withPassiveEffect
, then theEffect
in theupdate_queue
needs to be added to theupdate
list inpending_passive_effects
.
if flags & Flags::PassiveEffect != Flags::NoFlags {
CommitWork::commit_passive_effect(finished_work.clone(), root, "update");
finished_work.borrow_mut().flags -= Flags::PassiveEffect;
}
The general process is now complete, for more details please refer to here.