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:v6
The previous article has already implemented the render process. In this article, we will implement the final step, which is the commit phase.
Firstly, add the commit
step to the work_loop
function:
fn perform_sync_work_on_root(&mut self, root: Rc<RefCell<FiberRootNode>>) {
...
root.clone().borrow_mut().finished_work = finished_work;
self.commit_root(root);
}
The finished_work
here refers to the root FiberNode
.
The complete commit process includes steps like commitBeforeMutationEffects
, commitMutationEffects
, commitLayoutEffects
, and so on. For simplicity, let's start by implementing only the commitMutationEffects
step. Of course, we can first check if the root node or its child nodes have any side effects ahead:
fn commit_root(&self, root: Rc<RefCell<FiberRootNode>>) {
...
if subtree_has_effect || root_has_effect {
commit_work.commit_mutation_effects(finished_work.clone());
cloned.borrow_mut().current = finished_work.clone();
} else {
cloned.borrow_mut().current = finished_work.clone();
}
}
In the commit_mutation_effects
step, the Fiber tree is traversed in depth-first way. For example, the traversal order for the following example is BEFCDA:
And the traversal of the subtree is determined based on the value of subtree_flags
. For example, in the following example, EF is skipped, and the final order is BCDA:
The subtree_flags
are updated in the bubble_properties
of CompleteWork
. This means that after completing a node, the flags
of all its child nodes are merged:
fn bubble_properties(&self, complete_work: Rc<RefCell<FiberNode>>) {
let mut subtree_flags = Flags::NoFlags;
{
let mut child = complete_work.clone().borrow().child.clone();
while child.is_some() {
let child_rc = child.clone().unwrap().clone();
{
let child_borrowed = child_rc.borrow();
subtree_flags |= child_borrowed.subtree_flags.clone();
subtree_flags |= child_borrowed.flags.clone();
}
{
child_rc.borrow_mut()._return = Some(complete_work.clone());
}
child = child_rc.borrow().sibling.clone();
}
}
complete_work.clone().borrow_mut().subtree_flags |= subtree_flags.clone();
}
Each node submits different operations based on its own flags
. For now, we will only handle the Placement
operation.
fn commit_mutation_effects_on_fiber(&self, finished_work: Rc<RefCell<FiberNode>>) {
let flags = finished_work.clone().borrow().flags.clone();
if flags.contains(Flags::Placement) {
self.commit_placement(finished_work.clone());
finished_work.clone().borrow_mut().flags -= Flags::Placement
}
}
In commit_placement
, the nearest HostComponent
or HostRoot
to the currently processed FiberNode
is first obtained as the target parent node for insertion:
fn commit_placement(&self, finished_work: Rc<RefCell<FiberNode>>) {
let host_parent = self.get_host_parent(finished_work.clone());
if host_parent.is_none() {
return;
}
let parent_state_node = FiberNode::derive_state_node(host_parent.unwrap());
if parent_state_node.is_some() {
self.append_placement_node_into_container(
finished_work.clone(),
parent_state_node.unwrap(),
);
}
}
fn get_host_parent(&self, fiber: Rc<RefCell<FiberNode>>) -> Option<Rc<RefCell<FiberNode>>> {
let mut parent = fiber.clone().borrow()._return.clone();
while parent.is_some() {
let p = parent.clone().unwrap();
let parent_tag = p.borrow().tag.clone();
if parent_tag == WorkTag::HostComponent || parent_tag == WorkTag::HostRoot {
return Some(p);
}
parent = p.borrow()._return.clone();
}
None
}
In the commit_placement
function, the nearest HostComponent
or HostRoot
to the currently processed FiberNode
is obtained as the target parent node where it will be inserted.
fn append_placement_node_into_container(
&self,
fiber: Rc<RefCell<FiberNode>>,
parent: Rc<dyn Any>,
) {
let fiber = fiber.clone();
let tag = fiber.borrow().tag.clone();
if tag == WorkTag::HostComponent || tag == WorkTag::HostText {
let state_node = fiber.clone().borrow().state_node.clone().unwrap();
self.host_config.append_child_to_container(
self.get_element_from_state_node(state_node),
parent.clone(),
);
return;
}
let child = fiber.borrow().child.clone();
if child.is_some() {
self.append_placement_node_into_container(child.clone().unwrap(), parent.clone());
let mut sibling = child.unwrap().clone().borrow().sibling.clone();
while sibling.is_some() {
self.append_placement_node_into_container(sibling.clone().unwrap(), parent.clone());
sibling = sibling.clone().unwrap().clone().borrow().sibling.clone();
}
}
}
You can refer to this link for detailed code changes in this update.
So far, we have replicated the big react v2 version, which can perform initial rendering of a single node and supports HostComponent
and HostText
.
After rebuilding and installing dependencies, running the example from the previous article, the final result will look like the following image:
From the result, we can also see that some insertion operations are completed before the Commit phase. Specifically in the code, this is done in the complete_work
function:
...
WorkTag::HostComponent => {
let instance = self.host_config.create_instance(
work_in_progress
.clone()
.borrow()
._type
.clone()
.unwrap()
.clone()
.as_string()
.unwrap(),
);
self.append_all_children(instance.clone(), work_in_progress.clone());
work_in_progress.clone().borrow_mut().state_node =
Some(Rc::new(StateNode::Element(instance.clone())));
self.bubble_properties(work_in_progress.clone());
None
}
...
This way, during the Commit phase, we only need to insert an off-screen DOM tree once, which can be considered a small optimization.
Please kindly give me a star.