Crates.io | wasm-gloo-dom-events |
lib.rs | wasm-gloo-dom-events |
version | 0.2.0 |
source | src |
created_at | 2023-10-22 11:16:49.637008 |
updated_at | 2023-10-31 15:50:21.610127 |
description | 二次封装`gloo crate`,将`Cpp - RAII`风格的`DOM`事件处理函数挂载方式封装为`Javascript - Angular`风格的`register / deregister`模式。 |
homepage | |
repository | https://github.com/stuartZhang/wasm-gloo-dom-events |
max_upload_size | |
id | 1010513 |
size | 42,435 |
二次封装gloo crate,将Cpp - RAII
风格的DOM
事件处理函数挂载方式封装变形为Typescript - Angular
风格的register / deregister
模式。
就DOM
事件处理函数的【挂/卸载】操作而言,gloo crate
已经做了非常完善的RAII with Guard
设计模式封装。这包括:
Rust
事件处理闭包封装成wasm_bindgen::closure::Closure
。再,wasm_bindgen::closure::Closure
类型转换为js_sys::Function
。接着,js_sys::Function
注入DOM
元素web_sys::EventTarget::add_event_listener_with_callback(&self, ...)
— 至此,完成了DOM
事件处理函数的挂载工作。然后,RAII Guard
实例”给【调用端】。这就DOM
事件处理函数的卸载工作委托给rustc
的Drop Checker
来完成。后续,#4
的)RAII Guard
实例被释放,RAII Guard
的析构函数Drop::drop(self)
就会卸载在#3
挂载的DOM
事件处理函数。很完美!它
DOM
事件处理函数的挂载操作委托给RAII Guard
的构造器EventListener::new(...);同时,DOM
事件处理函数的卸载操作委托给RAII Guard
的析构器。这实在太Thinking in Rust
了。而且,能完全落实这套RAII
编程范式的Cpp
程序员也必定是老司机了。但,
RAII Guard
是纯【系统编程】概念RAII Guard
实例是WebAssembly
线性内存对象,却不在**JS
堆**上RAII Guard
实例与JS
事件循环没有直接的“物理”联系所以,RAII Guard
实例不会因为事件挂载操作而常驻内存(— 这是拥有GC
加持的js
程序才具备的“超能力”)。请看下面js
代码片段:
(() => {
let handle = event => console.log(event.type);
button.addEventListener('click', handle);
})();
// 至此,虽然函数执行结束,但`handle`闭包还驻留在内存中 — 这是事件循环的作用。
// 所以,`button`的`click`事件依旧有响应
相反,RAII Guard
实例会随着【调用函数】的执行结束而被立即析构掉。进而,Rust
端的DOM
事件处理闭包也会被级联地释放掉。请看下面rust
代码片段:
fn main() {
let handle = EventListener::new(&button, "click", move |event| {
info!("按钮点击事件2", event);
});
}
// 在`Trunk`的入口函数`main()`执行结束之后,`button`的`click`处理函数
// 就被立即卸载了。所以,从网页点击`button`组件将不会获得任何的响应。
这明确不是我们想要的。我们想要是
RAII Guard
实例常驻内存,和让Rust - WASM
端的【DOM
事件处理闭包】长期有效。但又
禁止“人为刻意地”内存泄漏。比如,对RAII Guard
实例危险地调用std::mem::forget()
— 纯理论备选方案。同时,也
避免使用static mut
变量加unsafe
块,全局缓存RAII Guard
实例 — 这个法子是真管用,但太外行。请看下面代码片段:
static mut HANDLE_CACHE: Option<EventListener> = None;
fn main() {
let handle = EventListener::new(&button, "click", move |event| {
info!("按钮点击事件2", event);
});
unsafe { // 我想吐槽:“能写出这样代码的‘货’也真没谁了!”。
HANDLE_CACHE.replace(handle);
}
}
归纳起来,我们期望由DOM
事件挂载函数gloo::events::EventListener::new(...)
返回的不是“保活守卫Liveness Guard
”,而是“卸载函数Deregistration Function
”。这样才和主流UI
开发框架共同维系的编程习惯一致。目前,register / deregister
事件挂载模式的经典用例就是Angular
框架中的$watch
监听器。比如,
let offHandle;
vm.$onInit = () => {
// 监听器挂载函数返回的是“卸载函数”。
offHandle = $rootScope.$watch('some_property', () => {/* do something */});
};
vm.$onDestroy = () => {
offHandle(); // 执行“卸载函数”注销掉监听器。
};
DOM
监听器作为“消息源”DOM
事件序列转换成【异步流futures::stream::Stream<Item = web_sys::Event>】。
DOM
事件对象web_sys::Event
自身。wasm_bindgen_futures::spawn_local()
执行器,将【异步流】实例挂到js vm
的事件循环上。进而,确保【异步流】实例在WebAssembly
线性内存中的常驻,除非我们显式地卸载它。futures::stream::StreamExt::for_each
(甚至,并发for_each
)该【异步流】实例,就能在
Trunk
的入口函数main
执行结束之后,DOM
元素发起的事件了。【异步编程】真是前端的技术关键路线,无论是Typescript
前端,还是WEB
汇编前端。
首先,该crate
分别对
DOM
元素触发事件window.addEventListener('popstate',...)
requestAnimationFrame()
setTimeout()
setInterval()
的处理函数【挂/卸载】操作做了register / deregister
封装。
其次,对非常活跃事件源的事件处理函数,基于【异步流】底层技术,提供两种执行方式:
.await
状态,剩余事件处理函数就立即开始执行或继续执行。至于,如何传参配置执行方式,请见程序的【文档注释】。
cargo add wasm-gloo-dom-events
一共分成五个场景与五类套路
DOM
元素响应事件use ::deferred_future::LocalDeferredFuture;
use ::futures::future;
use ::gloo::{timers::future::TimeoutFuture, utils};
use ::wasm_bindgen::{JsCast, UnwrapThrowExt};
use ::wasm_bindgen_test::*;
use ::wasm_gloo_dom_events::{EventStream, Options};
use ::web_sys::{Document, HtmlBodyElement, HtmlButtonElement, PointerEvent};
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn dom_event() {
//
// 创建一个按钮`DOM`元素,和将其添加至文档`DOM`流中。
//
let document = utils::document();
let body = utils::body().dyn_into::<HtmlBodyElement>().unwrap_throw();
let button = create_element::<HtmlButtonElement>(&document, "button");
body.append_child(&button).unwrap_throw();
let deferred_future = LocalDeferredFuture::default();
let defer = deferred_future.defer();
//
// 给按钮`DOM`元素挂载鼠标点击事件处理函数。
// 1. 回调函数唯一形参是`DOM`事件自身的事件对象。
//
let off = EventStream::on(&button, "click", Options::enable_prevent_default(true), move |_event| {
// 异步的事件处理函数
defer.borrow_mut().complete("12".to_string());
future::ready(Ok(()))
});
//
// 模拟稍后点击按钮`DOM`元素。
//
wasm_bindgen_futures::spawn_local(async move {
TimeoutFuture::new(500).await;
let event = PointerEvent::new("click").unwrap_throw();
button.dispatch_event(&event).unwrap_throw();
});
let result = deferred_future.await;
assert_eq!(result, "12");
//
// 卸载事件处理函数
//
off();
}
fn create_element<T: JsCast>(document: &Document, tag_name: &str) -> T {
document.create_element(tag_name).unwrap_throw().dyn_into::<T>().unwrap_throw()
}
从命令行,执行命令wasm-pack test --chrome --headless --test=case4dom_event
可直接运行此例程。
use ::deferred_future::LocalDeferredFuture;
use ::futures::future;
use gloo::history::History;
use ::gloo::{history::BrowserHistory, timers::future::TimeoutFuture};
use ::std::rc::Rc;
use ::wasm_bindgen_test::*;
use ::wasm_gloo_dom_events::EventStream;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn history() {
//
// 从主窗体拾取出`history`实例
//
let browser_history = Rc::new(BrowserHistory::new());
let deferred_future: LocalDeferredFuture<String> = LocalDeferredFuture::default();
let defer = deferred_future.defer();
let off = {
let browser_history = Rc::clone(&browser_history);
//
// 给`history`挂载历史栈更新事件处理函数。
// 1. 回调函数第一个形参是`CustomEvent`。其`type`属性值呼应于`EventStream::on_history(..)`的第二个实参值。
// 2. 回调函数第二个形参是`history`的最新状态数据。
//
EventStream::on_history(Rc::clone(&browser_history), "测试".to_string(), true, move |_event, state: Option<Rc<&str>>| {
// 异步的事件处理函数
defer.borrow_mut().complete(state.unwrap().to_string());
future::ready(Ok(()))
})
};
{
let browser_history = Rc::clone(&browser_history);
//
// 模拟稍后`TAB`签路由变更 — 浏览器地址栏内容发生变化。
//
wasm_bindgen_futures::spawn_local(async move {
TimeoutFuture::new(500).await;
// 修改地址栏`url`,和压栈新历史状态数据。在本例中,
// 1. 修改浏览器地址栏为`/route1`
// 2. 填入历史状态数据"12"字符串
browser_history.push_with_state("route1", "12");
});
}
let result = deferred_future.await;
assert_eq!(result, "12");
//
// 卸载事件处理函数
//
off();
}
从命令行,执行命令wasm-pack test --chrome --headless --test=case4history
可直接运行此例程。
use ::deferred_future::LocalDeferredFuture;
use ::futures::future;
use ::wasm_bindgen_test::*;
use ::wasm_gloo_dom_events::EventStream;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn request_animation_frame() {
let deferred_future = LocalDeferredFuture::default();
let defer = deferred_future.defer();
//
// 给浏览器【帧渲染】挂载事件。回调函数唯一形参是`CustomEvent`。
// 1. 其`type`属性值呼应于`EventStream::on_request_animation_frame(..)`的第一个实参值。
// 2. 其`detail.timestamp`属性值是`js - requestAnimationFrame(timestamp => {...})`中的`timestamp`回调函数实参值。
//
let off = EventStream::on_request_animation_frame("requestAnimationFrame".to_string(), true, move |_event| {
// 异步的事件处理函数
defer.borrow_mut().complete("12".to_string());
future::ready(Ok(()))
});
let result = deferred_future.await;
assert_eq!(result, "12");
//
// 卸载事件处理函数
//
off();
}
从命令行,执行命令wasm-pack test --chrome --headless --test=case4request_animation_frame
可直接运行此例程。
use ::deferred_future::LocalDeferredFuture;
use ::futures::future;
use ::wasm_bindgen_test::*;
use ::wasm_gloo_dom_events::EventStream;
#[cfg(not(feature = "nodejs"))]
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn timeout() {
let deferred_future = LocalDeferredFuture::default();
let defer = deferred_future.defer();
//
// 给`window.setTimeout()`挂载回调函数。回调函数唯一形参是`CustomEvent`。
// 1. 其`type`属性值呼应于`EventStream::on_timeout(..)`的第一个实参值。
//
let off = EventStream::on_timeout("timeout".to_string(), 1000, move |_event| {
// 异步的事件处理函数
defer.borrow_mut().complete("12".to_string());
future::ready(Ok(()))
});
let result = deferred_future.await;
assert_eq!(result, "12");
//
// 卸载事件处理函数
//
off();
}
从命令行,执行命令可直接运行此例程
wasm-pack test --chrome --headless --test=case4timeout
nodejs
:wasm-pack test --node --features=nodejs --test=case4timeout
use ::deferred_future::LocalDeferredFuture;
use ::futures::future;
use ::wasm_bindgen_test::*;
use ::wasm_gloo_dom_events::EventStream;
#[cfg(not(feature = "nodejs"))]
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn timeout() {
let deferred_future = LocalDeferredFuture::default();
let defer = deferred_future.defer();
let mut count = 0_u8;
//
// 给`window.setInterval()`挂载回调函数。回调函数唯一形参是`CustomEvent`。
// 1. 其`type`属性值呼应于`EventStream::on_interval(..)`的第一个实参值。
//
let off = EventStream::on_interval("interval".to_string(), 1000, true, move |_event| {
// 异步的事件处理函数
count += 1;
if count > 5 {
defer.borrow_mut().complete("12".to_string());
}
future::ready(Ok(()))
});
let result = deferred_future.await;
assert_eq!(result, "12");
//
// 卸载事件处理函数
//
off();
}
从命令行,执行命令可直接运行此例程
wasm-pack test --chrome --headless --test=case4interval
nodejs
:wasm-pack test --node --features=nodejs --test=case4interval