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:v4
The update process in React can be divided into two main phases: Render and Commit. The Render phase can further be divided into two stages: begin work and complete work. This article focuses on implementing the begin work stage.
In the previous article, we discussed that when the render()
method is called, it invokes the update_container
method in the reconciler
:
pub fn update_container(&self, element: Rc<JsValue>, root: Rc<RefCell<FiberRootNode>>) {
let host_root_fiber = Rc::clone(&root).borrow().current.clone();
let update = create_update(element);
enqueue_update(host_root_fiber.borrow(), update);
...
}
After executing the above code, a data structure like the following will be constructed:
The definitions of FiberRootNode
and FiberNode
are as follows:
pub struct FiberRootNode {
pub container: Rc<JsValue>,
pub current: Rc<RefCell<FiberNode>>,
pub finished_work: Option<Rc<RefCell<FiberNode>>>,
}
pub struct FiberNode {
pub tag: WorkTag,
pub pending_props: Option<Rc<JsValue>>,
key: Option<String>,
pub state_node: Option<Rc<StateNode>>,
pub update_queue: Option<Rc<RefCell<UpdateQueue>>>,
pub _return: Option<Rc<RefCell<FiberNode>>>,
pub sibling: Option<Rc<RefCell<FiberNode>>>,
pub child: Option<Rc<RefCell<FiberNode>>>,
pub alternate: Option<Rc<RefCell<FiberNode>>>,
pub _type: Option<Rc<JsValue>>,
pub flags: Flags,
pub subtree_flags: Flags,
pub memoized_props: Option<Rc<JsValue>>,
pub memoized_state: Option<Rc<JsValue>>,
}
Here, the Rc
smart pointer is used to allow a value to have multiple owners, and RefCell
is used for "interior mutability" for certain fields that need to be modified.
Next, we are going to build a FiberNode Tree:
pub fn update_container(&self, element: Rc<JsValue>, root: Rc<RefCell<FiberRootNode>>) {
...
let mut work_loop = WorkLoop::new(self.host_config.clone());
work_loop.schedule_update_on_fiber(host_root_fiber);
}
Because the subsequent code is mostly a translation of the implementation in big-react from JavaScript to Rust, there is no need to go into too much detail. Here are a few differences to note.
Difference 1: workInProgress
In big-react, workInProgress
is a module-level variable. However, Rust does not have the concept of module-level variables, so it has been changed to be an attribute within a struct.
pub struct WorkLoop {
work_in_progress: Option<Rc<RefCell<FiberNode>>>,
}
In Rust, a new struct called WorkLoop
has been introduced, whereas in big-react, it was exported as a function in the work_loop.js
module.
Difference 2: stateNode
In big-react, the stateNode
is of type any
because for the root FiberNode
, its stateNode
is a FiberRootNode
, while for other nodes, the stateNode
is a DOM object in JavaScript. In Rust, an enum is used to represent this:
pub enum StateNode {
FiberRootNode(Rc<RefCell<FiberRootNode>>),
Element(Rc<dyn Any>),
}
It is a bit more cumbersome to use, as it requires the use of match for branching and handling different cases:
match fiber_node.state_node {
None => {}
Some(state_node) => {
match &*state_node {
StateNode::FiberRootNode(fiber_root_node) => {}
StateNode::Element(ele) => {},
};
}
}
Alternatively, it can be done similarly to destructuring assignment in JavaScript:
let Some(StateNode::FiberRootNode(fiber_root_node)) = fiber_node.state_node.clone();
Difference 3: performSyncWorkOnRoot
In big-react, try-catch
is used to catch any errors that occur during the workLoop
process:
do {
try {
workLoop()
break
} catch (e) {
console.error('workLoop error', e)
workInProgress = null
}
} while (true)
Since Rust does not support try-catch
, but instead uses Result
to handle errors, we won't consider it for now and will implement it later:
loop {
self.work_loop();
break;
}
Since we are currently only implementing the begin work phase, we will temporarily comment out complete_unit_of_work
in perform_unit_of_work
. Instead, we will assign None
to work_in_progress
to make the loop exit:
fn work_loop(&mut self) {
while self.work_in_progress.is_some() {
self.perform_unit_of_work(self.work_in_progress.clone().unwrap());
}
}
fn perform_unit_of_work(&mut self, fiber: Rc<RefCell<FiberNode>>) {
let next = begin_work(fiber.clone());
if next.is_none() {
// self.complete_unit_of_work(fiber.clone())
self.work_in_progress = None;
} else {
self.work_in_progress = Some(next.unwrap());
}
}
Next, let's print the generated FiberNode tree in this phase to see if the results are correct:
fn perform_sync_work_on_root(&mut self, root: Rc<RefCell<FiberRootNode>>) {
self.prepare_fresh_stack(Rc::clone(&root));
loop {
self.work_loop();
break;
}
log!("{:?}", *root.clone().borrow());
}
To print the FiberRootNode
, we also need to implement the Debug
trait for it:
impl Debug for FiberRootNode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
}
}
The implementation approach is to use breadth-first traversal. You can refer to the code for more details. Now, let's modify the example in the "hello world" project:
import {createRoot} from 'react-dom'
const comp = (
<div>
<p>
<span>Hello World</span>
</p>
</div>
)
const root = createRoot(document.getElementById('root'))
root.render(comp)
You can see the following output:
Since the reconciliation process for children as an array has not been implemented yet, we can only test the case with a single child for now.
Please kindly give a star.