Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

组件 #1

Open
otakustay opened this issue Aug 24, 2016 · 5 comments
Open

组件 #1

otakustay opened this issue Aug 24, 2016 · 5 comments

Comments

@otakustay
Copy link
Member

otakustay commented Aug 24, 2016

组件状态

不分state和prop,所有状态可访问性为公开,称为state

组件的状态是不可变的,即每次修改状态都会创建一个新的state

状态更新

对外暴露的方法为patchState(partialState)

另外还有replaceState(newState),用于更新整个状态,且replaceState(newState)还会被用于数据流传递

patchState(partialState) {
    let patch = Object.entries(partialState).reduce(
        (patch, [key, value]) => {
            if (value === this.state[key]) {
                return patch;
            }

            return Object.assign(patch, {[key]: value});
        },
        {}
    );

    if (isEmpty(patch)) {
        return {previousState: this.state, newState: this.state};
    }

    let newState = Object.assign({}, this.state, patch);
    return this.replaceState(newState);
}

replaceState(newState) {
    if (this.state === newState) {
        return {previousState: this.state, newState: this.state};
    }

    let previousState  = this.state;

    this.state = newState;

    let change = {previousState, newState};

    this.pushStateUpdate(change); // 将状态同步到其它组件

    return change;
}

基于Immutable的设计下,组件收到一个新的状态(通过replaceState(newState)调用)时,只要检查引用是否与当前状态相等就行,如果不相等则直接开始进行数据的向下传递,不对状态进行任何深层的判断,这保证了数据流的简单,简单又进一步带动可靠,这是san-view的主要目的之一

组件内部可通过protected的方法notifyStateChange({previousState, newState})对外通知状态的变化,这一方法会触发相关的事件/回调

事件处理

每一个事件处理函数接收当前的状态,并返回一个Promise,该Promise在resolve时提供一个函数(后称为Action),Action接收当前组件的状态并返回状态的补丁(参考上文patchState

同时事件处理函数和Action均接收一个output参数,该参数为一个函数,用于对外发送通知(使用事件或回调的形式,具体由组件控制)

用范式表达为:

async (state, output, ...eventArgs) => (state, output) => newState

一个简单的业务逻辑如下:

async onLike(state, output, event) {
    output('startnetwork');

    let likeCount = await api.updateLikeCount(state.postId);

    output('finishnetwork');

    return (state, output) => {
        output('likeupdate');

        return {like: likeCount};
    };
}

注意事件处理函数得到的状态和Action得到的状态可能是不一样的(因为有异步)

事件处理函数的this为当前组件实例,但非常不建议使用任何带有this访问的代码,尽可能保持事件处理函数是个纯函数

一个事件相当于如下过程:

  1. 调用事件处理函数得到Promise<Action>
  2. 等待Promise进入resolved状态,得到Action
  3. 执行Action,得到state补丁
  4. 调用patchState更新状态
  5. 调用notifyStateChange通知状态变化

需要注意的是通过事件执行的新状态,都会自动通知状态变化,这与外部直接调用patchState不同

注:最初设想一个事件处理函数是直接返回Promise<newState>的,但是考虑到异步会导致状态错乱,所以设计成了Promise<Action>,这会提升一定的复杂性,但更稳健

组件渲染

组件渲染的核心为render(state),接收当前的状态进行处理

组件渲染只会由状态变化触发(暂无其它触发手段),调用patchState后,使用一个所有组件共享的统一调度器进行更新调度

patchState(partialState) {
    // ...前面的代码
    scheduler.scheduleRender(this, change);
}

调度器全局共享一个,通过异步管理,将所有组件的渲染计划收集起来,一个周期后统一处理,一个组件在周期内有多次状态更新,会由调度器进行合并

这会导致在某些时刻状态和界面是不同步的,但这并不重要

在与函数式的配合上,render方法的返回值当前还未决定

组件定义

从上文可以看到,组件中最频繁使用核心功能(状态更新、事件处理)都是纯函数,而不是当前社区中其他框架的依赖this.state等一堆状态

这一特点决定了在san-view体系中,组件事实上是一个“状态的容器”,它本身的逻辑是纯函数,组件实例的存在用来存放状态,而这个存放本身对组件实现者也是透明的

我称这种组件为“纯组件”,与“函数式组件”不同,纯组件的特点是逻辑为纯函数,但本身具备对开发者透明的状态管理,这使得纯组件避开了一般组件状态与生命周期紧耦合难以测试的缺点,也不至于因为实际函数式组件导致数据流过于繁琐

其一个可见的特点为可以针对事件进行测试,可以参考一个简单的 普通组件纯组件 的对比

对于特殊情况,如果一个事件处理函数不能是纯函数,需要用@sideeffect标记,这个标记本身没啥用,但配合一系列工具会有特殊的效果(待定)

@otakustay otakustay mentioned this issue Aug 24, 2016
@hunter2009
Copy link

hunter2009 commented Aug 24, 2016

新的UI体系会引入Virtual Dom,或者是基于容器组件层级引入?

@otakustay
Copy link
Member Author

不会,Server Render暂不在第一个版本考虑范围内,核心是MVVM式的绑定和变化追踪局部刷新,vue已经证实这种方式没有性能上的顾虑,san的特色是依靠强制Immutable来解除变化追踪的复杂性,简化整个逻辑和数据流来追求稳健

@Exodia
Copy link

Exodia commented Aug 25, 2016

我觉得不要 replaceState 这个 api,一个定义良好的组件,具体的state数量应该是确定的,不应该动态增减,如果有增减需求,可以用高阶组合来做。固定的state也方便在开发阶段做类型检测,使得代码更加可预测。

@otakustay
Copy link
Member Author

我觉得不要 replaceState 这个 api

是的。最早的设计里是打算用replaceState做自上而下的数据分发的(当时的直觉是发下去后通过this.state === newState判断要不要继续往下发),后来实际深入以后发现其实数据分发用的也是setState,然后这个忘了删了……正式版本中我会移除,除非有切实需要提Issue再考虑加回来

@otakustay
Copy link
Member Author

经过和 @jinzhubaofu 沟通了一下,我们需要解决一个“发送请求前显示loading,请求结束消失”这样的需求,本质上一个事件处理函数可能需要多次修改状态

为此,除了原有的参数外,我们再添加一个叫resolve的参数(或者叫yield之类的名字,好名字都被标准占了……),用来在整个事件过程中产生不同的状态

但是函数本身还是有return,用来标识事件处理结束,这里涉及到资源清理之类的

    async remove({id}, output, resolve) {
        // 可以在函数return前的任意时候调用`resolve`提供Action更新状态
        resolve(state => ({...state}, loading: true));

        try {
            await service.removeTodo(id);

            // return还是有,用来标识这个事件已经处理完成(这里可能就会涉及和事件处理有关的资源的回收等),
            // 另外未来对于Optimisitc UI来说,只有这个Action可以做
            return (state, output) => {
                // 这个事件等效于`on-item-change`再判断`item.removed`,但我们更推荐对于业务有独立的事件
                output('remove', id);

                // 简单的属性修改可以用Object Spread
                return {...state, removed: true};
            };
        }
        finally {
            resolve(state => ({...state, loading: false}));
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants