Implement React v18 from Scratch Using WASM and Rust - [6] Implementation of Commit Process

ayou - Apr 22 - - Dev Community

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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:

Image description

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();
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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
}
...
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .