您现在的位置是:首页 > 文章详情

Yew 框架 (一)

日期:2020-02-04点击:585

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(), ) } 

这两个方法本来可以写成一个的,考虑代码复用,拆成了两个方法。

  1. 调用 stdweb 的方法获取到body节点对象 element;
  2. 清除该节点下的所有节点,让body变成一个干净的节点;
  3. 创建一个默认的 NodeRef 对象,默认没有指向任何节点,这个对象的目的是为了给 Component 提供一个可以操作其对应的 Dom 节点的手段,但目前组件对应的 Dom 节点尚未创建,所以只是一个暂时未指向任何节点的空对象,后续 Dom 节点创建之后会更新该对象,将其指向对应的 Dom 节点;
  4. 为组件创建默认属性。

之后调用 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)); } 
  1. 创建 ComponentLink 对象;
  2. 创建 ReadyState 对象,将所有环境信息集中在一个对象中,由于根节点没有祖先节点,所以环境中的 ancestor 是 None;
  3. 接下来是一个比较神奇的操作 *scope.shared_state.borrow_mut() = ComponentState::Ready(ready_state);,这行代码通过 RefCell 提供的内部可变能力,在当前组件的 Scope 对象正在被使用时,更新了它内部的组件状态为 ReadyState;
  4. 然后创建一个 CreatedComponent 指令对象,并放入 创建 执行队列中;
  5. 然后创建一个 MountedComponent 指令对象,并放入 已经挂载 执行队列中;
  6. 最后返回当前组件的 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); } }); } } 
  1. 将组件状态标记为处理中;
  2. 如果当前状态是 Ready, 完成组件的创建并将状态,转换为 Craterd 状态;
  3. 如果是已经创建或已经销毁,保持状态不变;
  4. 如果出现其它状态则是程序异常。
挂载组件指令
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); } }); } } 
  1. 将组件状态标记为处理中;
  2. 如果当前状态是 Created 调用状态的 mounted() 方法,并保持当前状态为 Created 状态;
  3. 如果是已销毁状态,保持不变; 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); } }); } } 
  1. 将组件状态标记为处理中;
  2. 如果当前状态是 Created,根据更新组件更新消息类型做调用组件对应方法处理消息,更新消息有三种:单个自定义消息、批量自定义消息和组件属性改变消息,如果组件更新消息执行完成后返回值为 应该 更新组件,则更新 Dom ,并更新组件状态;
  3. 如果是已销毁状态,保持不变;
  4. 如果出现其它状态则是程序异常。
销毁组件指令
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), }; } } 
  1. 将组件状态标记为已经销毁;
  2. 如果组件前一个状态是 Created, 则调用组件的 destroy() 方法清理资源,如果是已经挂载到 Dom 节点上,将该组件对应的节点从 Dom 中移除;
  3. 如果组件前一个状态是 Ready,并且该组件有父组件,则将该组件从父节点中移除;
  4. 如果出现其它状态则是程序异常。
原文链接:https://my.oschina.net/zengsai/blog/3163260
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章