wasm-gloo-dom-events
二次封装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
代码片段:
;
// 至此,虽然函数执行结束,但`handle`闭包还驻留在内存中 — 这是事件循环的作用。
// 所以,`button`的`click`事件依旧有响应
相反,RAII Guard
实例会随着【调用函数】的执行结束而被立即析构掉。进而,Rust
端的DOM
事件处理闭包也会被级联地释放掉。请看下面rust
代码片段:
// 在`Trunk`的入口函数`main()`执行结束之后,`button`的`click`处理函数
// 就被立即卸载了。所以,从网页点击`button`组件将不会获得任何的响应。
这明确不是我们想要的。我们想要是
-
RAII Guard
实例常驻内存,和让Rust - WASM
端的【DOM
事件处理闭包】长期有效。但又 -
禁止“人为刻意地”内存泄漏。比如,对
RAII Guard
实例危险地调用std::mem::forget()
— 纯理论备选方案。同时,也 -
避免使用
static mut
变量加unsafe
块,全局缓存RAII Guard
实例 — 这个法子是真管用,但太外行。请看下面代码片段:static mut HANDLE_CACHE: = None;
归纳起来,我们期望由DOM
事件挂载函数gloo::events::EventListener::new(...)
返回的不是“保活守卫Liveness Guard
”,而是“卸载函数Deregistration Function
”。这样才和主流UI
开发框架共同维系的编程习惯一致。目前,register / deregister
事件挂载模式的经典用例就是Angular
框架中的$watch
监听器。比如,
let offHandle;
vm ;
vm ;
工作原理
- 将
DOM
监听器作为“消息源” - 借助“异步、无限(缓存)、多进单出”信道futures::channel::mpsc::unbounded,将被触发的
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 LocalDeferredFuture;
use future;
use ;
use ;
use *;
use ;
use ;
wasm_bindgen_test_configure!;
async
从命令行,执行命令wasm-pack test --chrome --headless --test=case4dom_event
可直接运行此例程。
浏览器【历史栈】变更事件
use LocalDeferredFuture;
use future;
use History;
use ;
use Rc;
use *;
use EventStream;
wasm_bindgen_test_configure!;
async
从命令行,执行命令wasm-pack test --chrome --headless --test=case4history
可直接运行此例程。
浏览器【帧渲染】事件
use LocalDeferredFuture;
use future;
use *;
use EventStream;
wasm_bindgen_test_configure!;
async
从命令行,执行命令wasm-pack test --chrome --headless --test=case4request_animation_frame
可直接运行此例程。
单次计划任务
use LocalDeferredFuture;
use future;
use *;
use EventStream;
wasm_bindgen_test_configure!;
async
从命令行,执行命令可直接运行此例程
- 浏览器:
wasm-pack test --chrome --headless --test=case4timeout
nodejs
:wasm-pack test --node --features=nodejs --test=case4timeout
周期多次计划任务
use LocalDeferredFuture;
use future;
use *;
use EventStream;
wasm_bindgen_test_configure!;
async
从命令行,执行命令可直接运行此例程
- 浏览器:
wasm-pack test --chrome --headless --test=case4interval
nodejs
:wasm-pack test --node --features=nodejs --test=case4interval