Created
October 26, 2023 12:19
-
-
Save safx/9af2efa78e0a34d3144c268eb5a72dac to your computer and use it in GitHub Desktop.
Fine-grained reactive system: https://www.youtube.com/watch?v=GWB3vTWeLd4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::any::Any; | |
use std::cell::{Cell, RefCell}; | |
use std::collections::{HashMap, HashSet}; | |
use std::marker::PhantomData; | |
use web_sys::{console, window}; | |
use wasm_bindgen::prelude::{Closure, JsCast}; | |
#[derive(Default)] | |
struct Runtime { | |
signal_values: RefCell<Vec<Box<RefCell<dyn Any>>>>, | |
running_effect: Cell<Option<EffectId>>, | |
signal_subscribers: RefCell<HashMap<SignalId, HashSet<EffectId>>>, | |
effects: RefCell<Vec<Box<dyn Fn()>>>, | |
} | |
impl Runtime { | |
fn create_signal<T>(&'static self, value: T) -> Signal<T> where T: Clone + 'static { | |
let len = self.signal_values.borrow().len(); | |
self.signal_values.borrow_mut().push(Box::new(RefCell::new(value))); | |
Signal { cx: self, id: SignalId(len), ty: PhantomData } | |
} | |
fn create_effect(&'static self, f: impl Fn() + 'static) { | |
let len = self.effects.borrow().len(); | |
self.effects.borrow_mut().push(Box::new(f)); | |
self.run_effect(EffectId(len)) | |
} | |
fn run_effect(&self, effect_id: EffectId) { | |
// push effect onto stack | |
let prev_running_effect = self.running_effect.take(); | |
self.running_effect.set(Some(effect_id)); | |
// run effect | |
let effect = &self.effects.borrow()[effect_id.0]; | |
effect(); | |
// pop effect off stack | |
self.running_effect.set(prev_running_effect); | |
} | |
} | |
#[derive(Copy, Clone, Eq, Hash, PartialEq)] | |
struct SignalId(usize); | |
#[derive(Copy, Clone, Eq, Hash, PartialEq)] | |
struct EffectId(usize); | |
#[derive(Copy, Clone)] | |
struct Signal<T> where T: Clone + 'static { | |
cx: &'static Runtime, | |
id: SignalId, | |
ty: PhantomData<T>, | |
} | |
impl<T> Signal<T> where T: Clone + 'static { | |
fn get(&self) -> T { | |
// get value | |
let value = &self.cx.signal_values.borrow()[self.id.0]; | |
let value = value.borrow(); | |
let value = value.downcast_ref::<T>().unwrap(); | |
// add subscribers | |
if let Some(running_effect) = self.cx.running_effect.get() { | |
let mut subs = self.cx.signal_subscribers.borrow_mut(); | |
let subs = subs.entry(self.id).or_default(); | |
subs.insert(running_effect); | |
} | |
// return value | |
value.clone() | |
} | |
fn set(&self, new_value: T) { | |
// set value | |
{ | |
let value = &self.cx.signal_values.borrow_mut()[self.id.0]; | |
let mut value = value.borrow_mut(); | |
let value: &mut T = value.downcast_mut::<T>().unwrap(); | |
*value = new_value; | |
} | |
// notify subscribers | |
let subs = { | |
let subs = self.cx.signal_subscribers.borrow(); | |
subs.get(&self.id).cloned() | |
}; | |
if let Some(subs) = subs { | |
for sub in subs { | |
self.cx.run_effect(sub); | |
} | |
} | |
} | |
} | |
fn main() { | |
console_error_panic_hook::set_once(); | |
let window = window().unwrap(); | |
let document = window.document().unwrap(); | |
let body = document.body().unwrap(); | |
let dec = document.create_element("button").unwrap(); | |
let p = document.create_element("p").unwrap(); | |
let p2 = document.create_element("p").unwrap(); | |
let inc = document.create_element("button").unwrap(); | |
dec.set_text_content(Some("-1")); | |
p.set_text_content(Some("Some value")); | |
p2.set_text_content(Some("Some value")); | |
inc.set_text_content(Some("+1")); | |
let _ = body.append_child(&dec); | |
let _ = body.append_child(&p); | |
let _ = body.append_child(&p2); | |
let _ = body.append_child(&inc); | |
// set up reactive system | |
let cx: &'static Runtime = Box::leak(Box::default()); | |
let count = cx.create_signal(0); | |
cx.create_effect(move || { | |
p.set_text_content(Some(&count.get().to_string())); | |
}); | |
let double_count = move || count.get() * 2; | |
cx.create_effect(move || { | |
p2.set_text_content(Some(&double_count().to_string())); | |
}); | |
let closure = Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| { | |
console::log_1(&"-1".into()); | |
count.set(count.get() - 1); | |
}) as Box<dyn FnMut(_)>); | |
dec.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()).unwrap(); | |
closure.forget(); | |
let closure = Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| { | |
console::log_1(&"+1".into()); | |
count.set(count.get() + 1); | |
}) as Box<dyn FnMut(_)>); | |
inc.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()).unwrap(); | |
closure.forget(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment