Yew框架(一) 应用初始化过程

原创
2020/02/04 19:25
阅读数 1.3W

Yew 框架

Yew 是 Rust 语言生态中最为成熟的前端应用框架。框架的具体介绍及使用方法详见Yew官网https://yew.rs/docs/ ,官网刚做好没有多久。

最简单的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 对象,将所有环境信息集中在一个对象中,由于根组件没有父组件,在Dom中也就没有对应的父节点,这个说法有点不准确,可以这样理解代码的设计,实际上是有父节点的,但目前框架中没有用到,所以环境中的 ancestor 是 None;
  3. 接下来是一个比较神奇的操作 *scope.shared_state.borrow_mut() = ComponentState::Ready(ready_state);,这行代码通过 RefCell 提供的内部可变能力,在当前组件的 Scope 对象正在被使用时,更新了它内部的组件状态为 ReadyState;
  4. 然后创建一个 CreatedComponent 指令对象,并放入 创建 执行队列中;
  5. 然后创建一个 MountedComponent 指令对象,并放入 已经挂载 执行队列中;
  6. 最后返回当前组件的 Scope。

至此,应用的启动过程结束。

为什么返回根组件 Scope ?

在早期的代码中是没有返回根组件 Scope 的,后来变更为返回该 Scope 是为了可以在应用根组件创建之后可以手动给它发一些信息。

调度器

可以看到 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 在 start 方法中根据代码字面意思是防止 start 方法被嵌套调用,但是在单线程中这样做的意义是什么? 什么情况下会出现 start 方法嵌套调用?请知情人指教。

指令

每个指令都需要实现 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, 完成组件的创建并将状态,转换为 Created 状态;
  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. 如果出现其它状态则是程序异常。

下一篇 Yew 框架 (二)子组件的创建和渲染

展开阅读全文
加载中
点击加入讨论🔥(2) 发布并加入讨论🔥
打赏
2 评论
1 收藏
0
分享
返回顶部
顶部