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:v11
Without an event system, React lacks interactivity, and users are unable to interact with the application. Therefore, in this article, we will use the click
event as an example to demonstrate how to implement it.
Let's take a look at how the official version of React handles events, using the following code as an example.
const App = () => {
innerClick = () => {
console.log('A: react inner click.')
}
outerClick = () => {
console.log('B: react outer click.')
}
return (
<div id='outer' onClickCapture={this.outerClick}>
<button id='inner' onClick={this.innerClick}>
button
</button>
</div>
)
}
When an event is triggered on the root element, we can obtain the native event object NativeEvent
. By accessing the target
property, we can retrieve the current clicked element, which in this case is the button
. Using the property __reactFiber$*****
(where *
represents a random number) on the button, we can access the corresponding FiberNode
. Additionally, React utilizes the NativeEvent
to generate a SyntheticEvent
, which has several important properties worth noting:
-
nativeEvent
: Points to theNativeEvent
. -
_dispatchListeners
: Stores the event listener functions to be executed. -
_dispatchInstances
: Stores theFiberNode
objects to which the event listener functions belong.
Next, we will collect the event listener functions to be executed in both the capturing and bubbling phases.
Finally, the methods in _dispatchListeners
are executed in order, and the corresponding stateNode
is obtained from the FiberNode
in _dispatchInstances
as the currentTarget
on the SyntheticEvent
.
The SyntheticEvent
also has a stopPropagation
method. Once called, the methods following _dispatchListeners
will not be executed, effectively preventing event propagation.
That concludes the introduction to the React event system. For more information, you can refer to this article.
However, the implementation of Big React differs from the official React. Here's how it's done:
During the complete work phase, when creating the stateNode
for a FiberNode
node, the event listener functions on the FiberNode
node are copied to the Element
.
When an event is triggered, the event listener functions are collected by traversing up the tree using the target
property on the NativeEvent
. If it's an onClick
event, the function is pushed
into the bubble
list. If it's an onClickCapture
event, the function is inserted
into the capture
list.
Then, the event listener functions in the capture
list are executed from start to finish, followed by the event listener functions in the bubble
list.
How is event propagation prevented? The answer lies in modifying the method on the NativeEvent
.
fn create_synthetic_event(e: Event) -> Event {
Reflect::set(&*e, &"__stopPropagation".into(), &JsValue::from_bool(false));
let e_cloned = e.clone();
let origin_stop_propagation = derive_from_js_value(&*e, "stopPropagation");
let closure = Closure::wrap(Box::new(move || {
// set __stopPropagation to true
Reflect::set(
&*e_cloned,
&"__stopPropagation".into(),
&JsValue::from_bool(true),
);
if origin_stop_propagation.is_function() {
let origin_stop_propagation = origin_stop_propagation.dyn_ref::<Function>().unwrap();
origin_stop_propagation.call0(&JsValue::null());
}
}) as Box<dyn Fn()>);
let function = closure.as_ref().unchecked_ref::<Function>().clone();
closure.forget();
Reflect::set(&*e.clone(), &"stopPropagation".into(), &function.into());
e
}
fn trigger_event_flow(paths: Vec<Function>, se: &Event) {
for callback in paths {
callback.call1(&JsValue::null(), se);
// If __stopPropagation is true, break
if derive_from_js_value(se, "__stopPropagation")
.as_bool()
.unwrap()
{
break;
}
}
}
However, there is another issue here. The currentTarget
is not corrected as it is in the official version of React. In this case, the currentTarget
always refers to the root element because the event listener functions are bound to that element.
pub fn init_event(container: JsValue, event_type: String) {
...
let element = container
.clone()
.dyn_into::<Element>()
.expect("container is not element");
let on_click = EventListener::new(&element.clone(), event_type.clone(), move |event| {
dispatch_event(&element, event_type.clone(), event)
});
on_click.forget();
}
This update can be found in detail here.
Please kindly give me a star