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:v14
Introduction to Scheduler
Scheduler
is a package in React responsible for task scheduling, and it lays the groundwork for implementing time slicing. The upcoming useEffect
will also utilize it, so in this article, we'll start by implementing a WASM (WebAssembly) version of the Scheduler
.
For an introduction to Scheduler
, you can refer to this article I wrote previously. Below is a brief introduction to its implementation.
Scheduler
maintains two min-heaps: timerQueue
and taskQueue
. Tasks that are ready (startTime
<= currentTime
) are placed into the taskQueue
, while tasks that are not yet ready (with startTime
> currentTime
achieved by passing in a delay
) are placed into the timerQueue
. For instance, in the example below, task1
would be placed into the taskQueue
, while task2
would be placed into the timerQueue
.
const task1 = Scheduler.unstable_scheduleCallback(2, function func1() {
console.log('2')
})
const task2 = Scheduler.unstable_scheduleCallback(
1,
function func2() {
console.log('1')
},
{delay: 100}
)
Later on, a macro task is initiated through MessageChannel
to process the tasks in the taskQueue
. If the processing time exceeds 5ms, it will be interrupted, and then a new macro task will be started to continue processing the remaining tasks. This cycle repeats until all tasks in the heap are completed. Tasks in the timerQueue
are periodically checked to see if they are ready. If they are, they are popped out in sequence and placed into the taskQueue
.
The details of this update can be seen here. Below, I will highlight and explain some key points.
Implementation of Min Heap
To facilitate writing unit tests, a generic version of the min heap was implemented:
...
pub fn push<T: Ord>(heap: &mut Vec<T>, value: T) {
heap.push(value);
sift_up(heap, heap.len() - 1);
}
...
fn sift_up<T: Ord>(heap: &mut Vec<T>, mut index: usize) {
while index != 0 {
let parent = (index - 1) / 2;
if heap[parent] <= heap[index] {
break;
}
heap.swap(parent, index);
index = parent;
}
}
...
However, this generic T
is constrained by Ord
, requiring the implementation of the Ord
trait, like this:
struct Task {
id: u32
}
impl Eq for Task {}
impl PartialEq for Task {
fn eq(&self, other: &Self) -> bool {
self.id.cmp(&other.id) == Ordering::Equal
}
}
impl PartialOrd for Task {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
return self.id.partial_cmp(&other.id);
}
}
impl Ord for Task {
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap_or(Ordering::Equal)
}
}
static mut
The implementation of Scheduler
defines a large number of static mut
, leading to many unsafe code blocks in the code. Clearly, this is not a good practice, but the advantage of doing so is that the implementation is more similar to React's, which facilitates copying code. Moreover, another more important reason is that if we do not use static mut
, but instead define a Scheduler
struct and make these static mut
as its attributes, other problems will be encountered.
For instance, when using perform_work_until_deadline
as the callback function for a macro task, it would need to be changed to self.perform_work_until_deadline
, and such a change would not compile:
pub fn schedule_perform_work_until_deadline() {
let perform_work_closure =
// Will fail to compile if it is changed to self.perform_work_until_deadline
Closure::wrap(Box::new(perform_work_until_deadline) as Box<dyn FnMut()>);
Even changing it to a closure would not work:
pub fn schedule_perform_work_until_deadline() {
let perform_work_closure =
Closure::wrap(Box::new(|| self.perform_work_until_deadline()) as Box<dyn FnMut()>);
Therefore, it seems to be a necessary evil for the time being. However, by using unsafe to bypass Rust's safety checks, some strange behaviors can occur, such as the following example:
static mut MY_V: Vec<Task> = vec![];
#[derive(Debug)]
struct Task {
name: String
}
fn peek<'a>(v: &'a mut Vec<Task>) -> &'a Task {
&v[0]
}
fn pop<'a>(v: &'a mut Vec<Task>) -> Task {
let t = v.swap_remove(0);
t
}
fn main() {
unsafe {
MY_V = vec![Task {
name: "ayou".to_string()
}];
let t = peek(&mut MY_V);
// 1
// pop(&mut MY_V);
// 2
let a = pop(&mut MY_V);
println!("{:?}", t.name);
};
}
Code 1 and Code 2 produce different outputs. Code 1 outputs "\0\0\0\0"
, while Code 2 outputs normally, and the only difference between them is whether the return value is assigned to a variable or not.
As to why there is such a difference, it's not yet very clear. Fortunately, testing has revealed that there are no other issues for now, and next, we can proceed to implement useEffect
.
Please kindly give me a star!