Implement React v18 from Scratch Using WASM and Rust - [2] Implementation of ReactElement

ayou - Apr 7 - - 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: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:

Image description

For now, we are only concerned with the first three parameters passed into jsxDEV, which are:

  • type, representing the type of ReactElement. If it's an HTMLElement, this would be its corresponding tag (string). If it's a user-defined component, this would be a function.

  • props, the parameters passed to the ReactElement, including children.

  • 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: &JsValuekey: &JsValue) -> JsValue {

}


Enter fullscreen mode Exit fullscreen mode

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 this ReactElement object will eventually be passed to react-dom, it will still only be defined as JsValue. Therefore, there's no need to define it as ReactElement 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,
}


Enter fullscreen mode Exit fullscreen mode

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()
}



Enter fullscreen mode Exit fullscreen mode

For simplicity, we did not define REACT_ELEMENT_TYPE as Symbol, but &str:



pub static REACT_ELEMENT_TYPE: &str = "react.element";


Enter fullscreen mode Exit fullscreen mode

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" }


Enter fullscreen mode Exit fullscreen mode

Rebuild and run the previous example, you can see the following output, that means the react library is completed:

Image description

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
  }
}


Enter fullscreen mode Exit fullscreen mode

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
}


Enter fullscreen mode Exit fullscreen mode

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:

Image description

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]
}


Enter fullscreen mode Exit fullscreen mode

So, let's set a breakpoint in this function and continue stepping through until we reach a call stack like this:

Image description

In WASM, it shows that the method __wbg_get_e3c254076557e348 was called:

Image description

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)
}


Enter fullscreen mode Exit fullscreen mode

At this point, the related data is as shown in the figure:

Image description

This corresponds to the execution of this step in the Rust code:



let val = Reflect::get(conf, &prop); // prop 为 children


Enter fullscreen mode Exit fullscreen mode

At this point, the truth is revealed.

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