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

Yew 框架 (一)

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

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条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章