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:v12
We have previously implemented the update process for a single node. This article continues to introduce how to implement updates for multiple nodes.
First, let's look at the begin work stage. The implementation here is similar to the official one. You can refer to the section on multi-node Diff in the previous article. The main difference is that the first round of traversal process has been removed.
It's worth noting that in TypeScript, you can declare a Map type like this:
Map<string | number, Fiber>
However, the key
in WASM is of JsValue
type. When we try to declare the type like this, it will prompt that JsValue
has not implemented Hash
and Eq
traits:
HashMap<JsValue, Rc<RefCell<FiberNode>>>
So, why not just implement it? Due to the "orphan rule" restriction in Rust, we cannot directly implement these traits for JsValue
. We need to create a new struct
to wrap it:
struct Key(JsValue);
impl PartialEq for Key {
fn eq(&self, other: &Self) -> bool {
Object::is(&self.0, &other.0)
}
}
impl Eq for Key {}
impl Hash for Key {
fn hash<H: Hasher>(&self, state: &mut H) {
if self.0.is_string() {
self.0.as_string().unwrap().hash(state)
} else if let Some(n) = self.0.as_f64() {
n.to_bits().hash(state)
} else if self.0.is_null() {
"null".hash(state)
}
}
}
After the multi-node Diff is completed, the FiberNode
that needs to be moved will be marked with Placement
, and the parent node of the FiberNode
that needs to be deleted will be marked with ChildDeletion
. Take the following as an example:
// before
<ul>
<li key={1}>a</li> // key: "1"
<li>b</li> // key: 1
<li>c</li> // key: 2
<li key={4}>d</li> // key: "4"
</ul>
// after
<ul>
<li>b</li> // key: 0
<li>c</li> // key: 1
<li key={1}>d</li> // key: "1"
</ul>
The results after begin work and complete work are shown below:
To briefly explain:
- The input keys are all converted to string types. If no key is passed, the index is used as the key.
- The li node with key 0 cannot find a node with the same key, so a new node needs to be inserted, marked as
Placement
. - The li node with key 1 can be reused, only need to update its child node content from b to c, marked as
Update
. - The li node with key "1" can be reused, but since
oldIndex
is less thanlastPlacedIndex
, it needs to be moved, marked asPlacement
.
Next, we come to the commit phase, which processes the side effects on the nodes in a depth-first traversal manner.
When executing the insertion operation, it will try to find an insertion point before
in siblings
. The difficulty is that this insertion point may not be its sibling at the same level. For example, <div/><B/>
where B is a FunctionComponent
type: function B() {return <div/>}
, here before
is actually B's child
, and the actual level may be deeper. At the same time, if a FiberNode
is marked Placement
, then it is unstable (its corresponding Element
will move in this commit phase), and it cannot be used as before
.
If the insertion point before
can be found, call parent.insertBefore(node, before)
, otherwise call parent.appendChild(node)
.
Using the above example, it will process the side effects on li(0) -> c -> li("1") -> ul
in order, and the results will be as follows:
- commitPlacement
- commitUpdate
- commitPlacement
- commitDeletion
For details on this update, see here. Please kindly give me a star!