Yew 框架 (一)
Yew 框架
Yew 是 Rust 语言生态中最为成熟的前端应用框架。
最简单的Yew应用
最简单的Yew应用只包含一个组件,即根组件,和一个main()方法。
use yew::{html, Component, ComponentLink, Html, ShouldRender};
pub struct Model {
link: ComponentLink<Self>,
}
pub enum Msg {
Click,
}
impl Component for Model {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
Model { link }
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Click => {}
}
true
}
fn view(&self) -> Html {
html! {
<div>
<button onclick=self.link.callback(|_| Msg::Click)>{ "Click" }</button>
</div>
}
}
}
fn main() {
yew::start_app::<minimal::Model>();
}
应用启动
应用启动调用框架的方法,传入根组件的类型。
/// Starts an app mounted to a body of the document.
pub fn start_app<COMP>()
where
COMP: Component,
COMP::Properties: Default,
{
initialize();
App::<COMP>::new().mount_to_body();
run_loop();
}
该方法中 initialize() 方法和 run_loop() 两人个方法先不看,是 stdweb 用来做一些环境配置的。如果你没有使用 stdweb,这两行代码都可以去掉。
这个方法只做了两人件事情,一是创建App对象,二是将根节点挂载到index.html的body标签中。
值得注意的是用这个方法来启动应用对该根组件有一个要求:组件的的属性需要实现 Default 特性,因为除些之外,框架不知道该如何创建根组件的属性对象。如果需要使用特定的属性对象,可以调用另一个方法来传入在外部初始化的根组件属性对象。
pub fn start_app_with_props<COMP>(props: COMP::Properties)
where
COMP: Component,
{
initialize();
App::<COMP>::new().mount_to_body_with_props(props);
run_loop();
}
创建App对象
App是一个结构体,声明如下:
#[derive(Debug)]
pub struct App<COMP: Component> {
/// `Scope` holder
scope: Scope<COMP>,
}
只包含一个组件域 Scope 对象。创建方法如下:
pub fn new() -> Self {
let scope = Scope::new();
App { scope }
}
没有什么好说的,直接创建一个 Scope 对象。
Scope 是什么?
Scope 代表一个组件的域,通过组件的域可以与组件进行全生命周期的管理。声明如下:
pub struct Scope<COMP: Component> {
shared_state: Shared<ComponentState<COMP>>,
}
pub(crate) type Shared<T> = Rc<RefCell<T>>;
enum ComponentState<COMP: Component> {
Empty,
Ready(ReadyState<COMP>),
Created(CreatedState<COMP>),
Processing,
Destroyed,
}
pub fn new() -> Self {
let shared_state = Rc::new(RefCell::new(ComponentState::Empty));
Scope { shared_state }
}
Scope 由一个可以共享的组件状态构成,组件状态中记录组件在不同时期的相关信息,可以看出组件有五个状态:
- 空状态是指组件的创建环境还没有建立起来;
- Ready 状态是组件的创建环境建立起来了,但组件还没有被创建;
- Created 状态是组件已经创建;
- Processing 状态是组件正在处理相关内部逻辑;
- Destroyed 是指组件已经被销毁。
为什么要有 Empty 这个状态呢?
通常设计中不会这个状态,要明白为什么要有这个状态,看看组件状态设计就明白了:
struct ReadyState<COMP: Component> {
element: Element,
node_ref: NodeRef,
props: COMP::Properties,
link: ComponentLink<COMP>,
ancestor: Option<VNode>,
}
/// Create link for a scope.
fn connect(scope: &Scope<COMP>) -> Self {
ComponentLink {
scope: scope.clone(),
}
}
组件的 ReadyState 中包含一个 ComponentLink<COMP> 对象 link,而该对象的创建需要一个 Scope 对象,也就是说创建Scope对象需要先创建 ComponentState 对象,而创建 ComponentState 对象又需要先创建 Scope 对象,形成一个循环,如果没有办法来打破这个循环,Scope 对象就无法被创建出来。引入 Empty 状态就是来用打破这个循环的。
这个设计在初次看代码的时候会看来头晕所以这里特别备注一下。
首次为一个组件创建域时,这个域只包含一个空状态。
App的创建实际上没有做太多的事情,只是为接下来创建=组件准备一个空的 Scope 对象。
挂载到 body 节点
将组件挂载到 body 节点就相对复杂了,需要创建组件对象,根据组件对象定义渲染出 dom 节点,获取 dom 中的 body 节点,将对象挂载到 body 之下。
pub fn mount_to_body(self) -> Scope<COMP> {
// Bootstrap the component for `Window` environment only (not for `Worker`)
let element = document()
.query_selector("body")
.expect("can't get body node for rendering")
.expect("can't unwrap body node");
self.mount(element)
}
pub fn mount(self, element: Element) -> Scope<COMP> {
clear_element(&element);
self.scope.mount_in_place(
element,
None,
NodeRef::default(),
COMP::Properties::default(),
)
}
这两个方法本来可以写成一个的,考虑代码复用,拆成了两个方法。
- 调用 stdweb 的方法获取到body节点对象 element;
- 清除该节点下的所有节点,让body变成一个干净的节点;
- 创建一个默认的 NodeRef 对象,默认没有指向任何节点,这个对象的目的是为了给 Component 提供一个可以操作其对应的 Dom 节点的手段,但目前组件对应的 Dom 节点尚未创建,所以只是一个暂时未指向任何节点的空对象,后续 Dom 节点创建之后会更新该对象,将其指向对应的 Dom 节点;
- 为组件创建默认属性。
之后调用 Scope 的 mount_to_place 方法:
/// Mounts a component with `props` to the specified `element` in the DOM.
pub(crate) fn mount_in_place(
self,
element: Element,
ancestor: Option<VNode>,
node_ref: NodeRef,
props: COMP::Properties,
) -> Scope<COMP> {
let mut scope = self;
let link = ComponentLink::connect(&scope);
let ready_state = ReadyState {
element,
node_ref,
link,
props,
ancestor,
};
*scope.shared_state.borrow_mut() = ComponentState::Ready(ready_state);
scope.create();
scope.mounted();
scope
}
pub(crate) fn mounted(&mut self) {
let shared_state = self.shared_state.clone();
let mounted = MountedComponent { shared_state };
scheduler().push_mount(Box::new(mounted));
}
/// Schedules a task to create and render a component and then mount it to the DOM
pub(crate) fn create(&mut self) {
let shared_state = self.shared_state.clone();
let create = CreateComponent { shared_state };
scheduler().push_create(Box::new(create));
}
- 创建 ComponentLink 对象;
- 创建 ReadyState 对象,将所有环境信息集中在一个对象中,由于根节点没有祖先节点,所以环境中的 ancestor 是 None;
- 接下来是一个比较神奇的操作
*scope.shared_state.borrow_mut() = ComponentState::Ready(ready_state);,这行代码通过 RefCell 提供的内部可变能力,在当前组件的 Scope 对象正在被使用时,更新了它内部的组件状态为 ReadyState; - 然后创建一个 CreatedComponent 指令对象,并放入 创建 执行队列中;
- 然后创建一个 MountedComponent 指令对象,并放入 已经挂载 执行队列中;
- 最后返回当前组件的 Scope。
可以看到 Scope 没有等待创建指令完成就发出了挂载指令,同样没有等待挂载指令完成就返回组件 Scope 对象到应用中。这样的做法不会导致状态出问题吗?
调度器
这里的执行队列是被放在一个进程级的全局调度器 Scheduler 中的。
thread_local! {
static SCHEDULER: Rc<Scheduler> =
Rc::new(Scheduler::new());
}
pub(crate) struct Scheduler {
lock: Rc<RefCell<()>>,
main: Shared<VecDeque<Box<dyn Runnable>>>,
create_component: Shared<VecDeque<Box<dyn Runnable>>>,
mount_component: Shared<Vec<Box<dyn Runnable>>>,
}
全局调度器中有一把锁和三个执行队列,分别是主消息队列 main,创建消息队列 create_component 和 挂载组件消息队列 mount_component。每次向执行队列中放入新的指令,都会调用调度器的 start 方法:
pub(crate) fn start(&self) {
let lock = self.lock.try_borrow_mut();
if lock.is_err() {
return;
}
loop {
let do_next = self
.create_component
.borrow_mut()
.pop_front()
.or_else(|| self.mount_component.borrow_mut().pop())
.or_else(|| self.main.borrow_mut().pop_front());
if let Some(runnable) = do_next {
runnable.run();
} else {
break;
}
}
}
由于在线程内部,所以不会有并发执行的情况发生,但不管是在哪个线程中,总是先执行 创建 指令,再执行 挂载 指令,最后执行其它指令。
lock 在线程中的意义是什么? 请知情人指教。
指令
每个指令都需要实现 Runnable 特性,来决定如何执行该指令。
pub(crate) trait Runnable {
/// Runs a routine with a context instance.
fn run(self: Box<Self>);
}
一共有 4 个指令,分别是创建组件、挂载组件、更新组件、销毁组件:
创建组件指令
struct CreateComponent<COMP>
where
COMP: Component,
{
shared_state: Shared<ComponentState<COMP>>,
}
impl<COMP> Runnable for CreateComponent<COMP>
where
COMP: Component,
{
fn run(self: Box<Self>) {
let current_state = self.shared_state.replace(ComponentState::Processing);
self.shared_state.replace(match current_state {
ComponentState::Ready(state) => ComponentState::Created(state.create().update()),
ComponentState::Created(_) | ComponentState::Destroyed => current_state,
ComponentState::Empty | ComponentState::Processing => {
panic!("unexpected component state: {}", current_state);
}
});
}
}
- 将组件状态标记为处理中;
- 如果当前状态是 Ready, 完成组件的创建并将状态,转换为 Craterd 状态;
- 如果是已经创建或已经销毁,保持状态不变;
- 如果出现其它状态则是程序异常。
挂载组件指令
struct MountedComponent<COMP>
where
COMP: Component,
{
shared_state: Shared<ComponentState<COMP>>,
}
impl<COMP> Runnable for MountedComponent<COMP>
where
COMP: Component,
{
fn run(self: Box<Self>) {
let current_state = self.shared_state.replace(ComponentState::Processing);
self.shared_state.replace(match current_state {
ComponentState::Created(state) => ComponentState::Created(state.mounted()),
ComponentState::Destroyed => current_state,
ComponentState::Empty | ComponentState::Processing | ComponentState::Ready(_) => {
panic!("unexpected component state: {}", current_state);
}
});
}
}
- 将组件状态标记为处理中;
- 如果当前状态是 Created 调用状态的 mounted() 方法,并保持当前状态为 Created 状态;
- 如果是已销毁状态,保持不变; 4.如果出现其它状态则是程序异常。
更新组件指令
struct UpdateComponent<COMP>
where
COMP: Component,
{
shared_state: Shared<ComponentState<COMP>>,
update: ComponentUpdate<COMP>,
}
impl<COMP> Runnable for UpdateComponent<COMP>
where
COMP: Component,
{
fn run(self: Box<Self>) {
let current_state = self.shared_state.replace(ComponentState::Processing);
self.shared_state.replace(match current_state {
ComponentState::Created(mut this) => {
let should_update = match self.update {
ComponentUpdate::Message(message) => this.component.update(message),
ComponentUpdate::MessageBatch(messages) => messages
.into_iter()
.fold(false, |acc, msg| this.component.update(msg) || acc),
ComponentUpdate::Properties(props) => this.component.change(props),
};
let next_state = if should_update { this.update() } else { this };
ComponentState::Created(next_state)
}
ComponentState::Destroyed => current_state,
ComponentState::Processing | ComponentState::Ready(_) | ComponentState::Empty => {
panic!("unexpected component state: {}", current_state);
}
});
}
}
- 将组件状态标记为处理中;
- 如果当前状态是 Created,根据更新组件更新消息类型做调用组件对应方法处理消息,更新消息有三种:单个自定义消息、批量自定义消息和组件属性改变消息,如果组件更新消息执行完成后返回值为 应该 更新组件,则更新 Dom ,并更新组件状态;
- 如果是已销毁状态,保持不变;
- 如果出现其它状态则是程序异常。
销毁组件指令
struct DestroyComponent<COMP>
where
COMP: Component,
{
shared_state: Shared<ComponentState<COMP>>,
}
impl<COMP> Runnable for DestroyComponent<COMP>
where
COMP: Component,
{
fn run(self: Box<Self>) {
match self.shared_state.replace(ComponentState::Destroyed) {
ComponentState::Created(mut this) => {
this.component.destroy();
if let Some(last_frame) = &mut this.last_frame {
last_frame.detach(&this.element);
}
}
ComponentState::Ready(mut this) => {
if let Some(ancestor) = &mut this.ancestor {
ancestor.detach(&this.element);
}
}
ComponentState::Empty | ComponentState::Destroyed => {}
s @ ComponentState::Processing => panic!("unexpected component state: {}", s),
};
}
}
- 将组件状态标记为已经销毁;
- 如果组件前一个状态是 Created, 则调用组件的 destroy() 方法清理资源,如果是已经挂载到 Dom 节点上,将该组件对应的节点从 Dom 中移除;
- 如果组件前一个状态是 Ready,并且该组件有父组件,则将该组件从父节点中移除;
- 如果出现其它状态则是程序异常。