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条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL数据库在高并发下的优化方案
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Docker容器配置,解决镜像无法拉取问题
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- 2048小游戏-低调大师作品
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作