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,并且该组件有父组件,则将该组件从父节点中移除;
- 如果出现其它状态则是程序异常。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
电子书合集 | 62 篇技术文章,探索阿里巴巴云原生实践之路
关注“阿里巴巴云原生”公众号,回复关键词电子书,即可下载 3 本电子书!从 2011 年起,阿里巴巴开始通过容器实践云原生技术体系,在整个业界都还没有任何范例可供参考的大背境下,从最初独自摸索到拥抱开源回馈社区,积累了丰富和宝贵的实践经验。 本文整理了 2019 年发布的3 本电子书,包含技术文章 62 篇,深度解读阿里巴巴在云原生实践路上遇到的问题及经验。 《不一样的 双11 技术:阿里巴巴经济体云原生实践》 《阿里巴巴云原生实践 15 讲》 《Knative 云原生应用开发指南》 《不一样的 双11 技术:阿里巴巴经济体云原生实践》 2019 双11,订单创新峰值达到 54.4 万笔/秒,单日数据处理量达到 970PB,面对世界级流量洪峰,并实现了100% 核心应用以云原生的方式上云。本书深挖云原生技术在 2019 双11 的实践细节
- 下一篇
听说云办公的第一天,被小学生弄“崩”了?
春节假期已经结束,今天是节后的第一个工作日,许多小伙伴也已经返程回到自己工作的城市。为了防止疫情扩散,大部分企业都延长了到岗时间,选择让员工居家隔离“云办公”。 什么是“云办公”?云办公可以简单理解为基于云计算应用模式的办公平台。存在的初衷有三点:第一,降低办公成本;第二,提高办公效率;第三,低碳减排。从 2015 年起,协同办公开始走入大众的视线,代表性产品包括企业微信、钉钉、石墨文档等。 目前“云办公”协同软件主要分为四种主要类别:沟通 IM、文档协作、视频会议/面试、任务管理。在举国共防疫情的情况下,云办公平台起到了极其重要的作用。众多协同办公软件也纷纷推出全新功能,并且免费开放。据不完全统计,从 1 月 24 日至今,企业微信、华为旗下的 WeLink、阿里钉钉、石墨文档等至少 17 家云协同办公厂商免费开放了部分能力,包括音视频会议、协同办公、远程诊断、健康管理等。 企业微信:疫情期间,会议人数上限升级至 300 人,支持随时随地发起音视频会议。主持人可管理与会人员;可投屏演示文档或屏幕,支持实时标注演示内容。 钉钉:支持“在家办公”的全套免费解决方案,包括远程视频会议、保障...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Hadoop3单机部署,实现最简伪集群
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Windows10,CentOS7,CentOS8安装Nodejs环境