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:v2
Implementation of ReactElement
In the previous article, we set up the development debugging environment. This time, we will implement the react library.
Without further ado, let's take a look at the compiled code:
For now, we are only concerned with the first three parameters passed into jsxDEV
, which are:
type
, representing the type ofReactElement
. If it's anHTMLElement
, this would be its corresponding tag (string
). If it's a user-defined component, this would be afunction
.props
, the parameters passed to theReactElement
, includingchildren
.key
, this does not need much explanation, everyone knows what it is.
Following this order, let's define our jsx_dev
function:
#[wasm_bindgen(js_name = jsxDEV)]
pub fn jsx_dev(_type: &JsValue, config: &JsValue,key: &JsValue) -> JsValue {
}
Here are a few points to note:
What is JsValue, and why is the type JsValue?
JsValue internally contains a u32 type index, which can be used to access objects in JS. More details can be found at the end of the document.Why isn't the return a
ReactElement
object?
Because thisReactElement
object will eventually be passed to react-dom, it will still only be defined asJsValue
. Therefore, there's no need to define it asReactElement
here.
Implementing this method is also quite simple, just convert the incoming parameters into an object as shown below:
{
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
}
The code is as follows:
use js_sys::{Object, Reflect};
use wasm_bindgen::prelude::*;
use shared::REACT_ELEMENT_TYPE;
#[wasm_bindgen(js_name = jsxDEV)]
pub fn jsx_dev(_type: &JsValue, config: &JsValue, key: &JsValue) -> JsValue {
// Initialize an empty object
let react_element = Object::new();
// Set properties of react_element using Reflect::set
Reflect::set(
&react_element,
&"&&typeof".into(),
&JsValue::from_str(REACT_ELEMENT_TYPE),
)
.expect("$$typeof panic");
Reflect::set(&react_element, &"type".into(), _type).expect("_type panic");
Reflect::set(&react_element, &"key".into(), key).expect("key panic");
// Iterate config and copy every property to props except ref.
// The ref property will be set to react_element
let conf = config.dyn_ref::<Object>().unwrap();
let props = Object::new();
for prop in Object::keys(conf) {
let val = Reflect::get(conf, &prop);
match prop.as_string() {
None => {}
Some(k) => {
if k == "ref" && val.is_ok() {
Reflect::set(&react_element, &"ref".into(), &val.unwrap()).expect("ref panic");
} else if val.is_ok() {
Reflect::set(&props, &JsValue::from(k), &val.unwrap()).expect("props panic");
}
}
}
}
// Set props of react_element using Reflect::set
Reflect::set(&react_element, &"props".into(), &props).expect("props panic");
// Convert Object into JsValue
react_element.into()
}
For simplicity, we did not define REACT_ELEMENT_TYPE
as Symbol
, but &str
:
pub static REACT_ELEMENT_TYPE: &str = "react.element";
It is defined in the shared project, so the Cargo.toml
file in the react project also needs to add the code as below:
[dependencies]
shared = { path = "../shared" }
Rebuild and run the previous example, you can see the following output, that means the react library is completed:
This article has touched on the implementation of the react library of the React18 WASM, which is relatively simple. The difficulty will increase from now on. We know that a single update process in React is divided into two major phases: render and commit. So in the next article, we will implement the render phase.
Supplement: Exploring the Principles of JsValue
Previously, we briefly discussed JsValue. Now, let's delve deeper into its principles. First, let's look at the code in the jsx-dev-runtime_bg.js
file after packaging with wasm-pack
. We find the jsxDEV
function:
export function jsxDEV(_type, config, key) {
try {
const ret = wasm.jsxDEV(
addBorrowedObject(_type),
addBorrowedObject(config),
addBorrowedObject(key)
)
return takeObject(ret)
} finally {
heap[stack_pointer++] = undefined
heap[stack_pointer++] = undefined
heap[stack_pointer++] = undefined
}
}
The parameters passed in are all processed by the addBorrowedObject
method, so let's continue to look into it:
const heap = new Array(128).fill(undefined);
heap.push(undefined, null, true, false);
let stack_pointer = 128;
...
function addBorrowedObject(obj) {
if (stack_pointer == 1) throw new Error('out of js stack')
heap[--stack_pointer] = obj
return stack_pointer
}
Oh, it turns out that on the JS side, an array is used to simulate a heap structure, and the parameters are all stored on this heap. The three parameters are stored in the following way:
And what's actually passed into wasm.jsxDEV is just the index of the array. So, how does the WASM side obtain the actual object through this index? For example, how does this code Reflect::get(conf, &prop); work?
If you think about it carefully, since the data is still on the JS side and only the index is passed to WASM, it's necessary that the JS side must also provide some interfaces for the WASM side to call. Continuing to look at the code in jsx-dev-runtime_bg.js, we find a method getObject(idx), which is used to retrieve data from the heap through an index:
function getObject(idx) {
return heap[idx]
}
So, let's set a breakpoint in this function and continue stepping through until we reach a call stack like this:
In WASM, it shows that the method __wbg_get_e3c254076557e348
was called:
The method __wbg_get_e3c254076557e348
can be found in jsx-dev-runtime_bg.js
:
export function __wbg_get_e3c254076557e348() {
return handleError(function (arg0, arg1) {
const ret = Reflect.get(getObject(arg0), getObject(arg1))
return addHeapObject(ret)
}, arguments)
}
At this point, the related data is as shown in the figure:
This corresponds to the execution of this step in the Rust code:
let val = Reflect::get(conf, &prop); // prop 为 children
At this point, the truth is revealed.