ecomfe / efe

EFE guide
20 stars 7 forks source link

一种更纯粹的组件实现方式 #2

Open otakustay opened 8 years ago

otakustay commented 8 years ago

还请 @errorrik 过目……


Inspired by react, redux, mvvm and many many more...

介绍

一个组件分为“数据”和“事件”两部分,其中数据进一步分为“数据的声明”、“数据的初始值”和“当前数据”

class Component {
    data,
    dataTypes,
    initialData,
    events
}

到这一步为止基本就是一个React和Vue的合体,区别在于下面。

当组件实际渲染后,触发了一定事件时,会调用事件对应的函数,而这个函数是一个“纯函数”,它接受当前的数据,返回新的数据(Redux的reducer):

let reduce = async (currentData, fire, ...eventArgs) => newData;

你说为什么偏偏是currentDatafire两个参数,因为一个组件的输入是数据,输出是事件,所以这相当于inputoutput

实际的流程是触发事件后,用函数计算出新的数据,把新的数据补丁到原有的数据上,进一步触发视图的更新。

优势

不需要Model的实现

永别了,我自己写的emc……

更加纯粹

所有的事件处理函数都是纯函数,本身是可单独测着玩的。

更强制地引入Immutable

我不相再赘述Immutable的作用和优势……反正我认为在我的diffy-update的支持下更新Immutable数据并不是那么困难的事。

对绑定同样友好

绑定是发生在data之下的,并不受任何影响,且因为一个reduce返回的新对象可能包含多个属性的修改,相当于默认实现了一定程度上change的合并,不必在纠结所谓的合并不合并的问题。

可组合复用

这点很重要。

在这个模式下,一个组件其实被明确地分为几个部分,除了“当前数据”之外,其它部分其实可以灵活的组合,我的一个组件可以写成这样:

import {set} from 'diffy-update';
import {Types} from 'san-ui/Types';

export let events = {
    async click(data, fire) {
        fire('click');
    }

    async duplicate(data) {
        return set(data, 'text', data.text.repeat(2));
    }
};

export let dataTypes = {
    text: Types.string
};

export let initialData = {
    text: ''
};

export default class Label {
    events = events;
    dataTypes = dataTypes;
    initialData = initialData;
}

此后我有了自己的一个组件,虽然长得不一样,也有一堆逻辑不一样,明显地不能继承Label,但正好也要个duplicate的功能,正好点了以后要触发click事件,那就方便了:

import {events} from 'Label';

export default class BeautifulLady {
    events = events;
    dataTypes...
    initialData...
}

当然我们可以更细化,让Label变成3个文件:

// label/events.js
import {set} from 'diffy-update';

export let click = async (data, fire) => fire('click');

export let duplicate = async data => set(data, 'text', data.text.repeat(2));

// label/Label.js
import * as events from './events';

export default class Label {
    events = events;
    dataTypes...
    initialData...    
}

这样我可以只复用其中的duplicate部分,而不需要click,相当方便。

当然我们更多的时候,面对的是数据结构不尽可同,但是逻辑相似的情况,这时就可以上高阶函数:

// transformer.js
export let withMapping = mapping => reduce => async (data, fire) => {
    let newData = await reduce(data, fire);
    let mappedData = Object.entries(newData).reduce(
        (result, [key, value]) => {
            let mappedKey = mapping[key] || key;
            return {[mappedKey]: value, ...result};
        },
        {}
    );
};

// BeautifulLady.js
import {withMapping} from 'transformer';
import {duplicate} from 'label/events';

let events = {
    duplicate: withMapping({text: name})(duplicate)
};

export default class BeautifulLady {
    events = events;
    dataTypes...
    initialData...
}

以上仅仅是说明了events这一块如何通过组合来达到比继承更强大的复用性,其实dataTypesinitialData同样可以复用,但考虑到这只是普通的数据结构,并不特别适合去复用,就不在此展示。

劣势

很显然,这需要一定的技术素养才能掌握。

过强的复用性可能让代码的组织变乱,各个组件间对events的依赖显得比较奇怪。

events本身是组件内部的一种实现,这样放出去给人复用并不适合大部分情况,通常只是在共享逻辑但定制外观的时候才有用。

errorrik commented 8 years ago

看起来除了一些细节可能要斟酌,复用是很容易。我唯一担心的是,如果采用这个组件构架,组件开发者是否能贯彻这个思路不玩脱

otakustay commented 8 years ago

这正是我在最后一段里想表达的……

在我看来,未来的2-3年间,趋向于纯函数和函数式会是一个大方向,然现阶段在我厂做实施确实有点前卫

但如果现在这个点不做一些激进的事,过了2-3年我们无非又是背着“落后、老旧、跟不上社区理念”这样的抱怨罢了……

otakustay commented 8 years ago

@errorrik 我想的是,我们之前沟通的那个vm核心,是否有可能再提取一个更小的核心出来(估计就是一个模板Parser和data -> repaint的工具),然后我们可以尝试在这个核心的基础上尝试各种不同的方案,哪个死哪个活让TC或者更广泛的大家来评判和选择,之后我们再集中精力于活的那个上面

chriswong commented 8 years ago

看起来跟 react + redux 比较像

otakustay commented 8 years ago

看起来跟 react + redux 比较像

是的,看上去就是把一个redux架构放进了一个组件里面去,但省掉了action creator和action这些概念,用了更简单粗暴直接是了的绑定逻辑

核心的理念更为相似,我认为一个组件无非:

  1. 长什么样(模板)
  2. 现在是什么样(当前的数据)
  3. 会发生什么(事件输入)
  4. 发生什么时的上下文(事件的属性)
  5. 发生什么的时候要变成啥样(新的数据)
  6. 发生什么时要告诉别人什么(触发事件)

这是一个很纯粹的东西,根本没有这么多的别的事参杂在里面,整体就是状态的迁移,也不会产生乱七八糟的的中间状态

顺便说一下,把这个设计出来的组件,在外面再用一个统一的逻辑包一下,就能变成一个真正的VM型的组件……

errorrik commented 8 years ago

其实小朋友们并不care你怎么玩的,能糊业务出来就行了, 至于架构扩展神马的还不是我们这帮人要写代码。

所以我想先做个能run起来的东西,暴露较少application必须接口,然后我们内部结构调整,为扩展暴露更多核心东西。

jinzhubaofu commented 8 years ago

我对「父子组件之间数据交互」有几个困惑

比如 <Tweet><Like/></Tweet> 这样

  1. Like 的 data 中有 props(父组件设置的数据,声明式对外接口) / state(自我持有的数据) 的区分吗?
  2. 当 Tweet 传递给 Like 的数据发生变化时,Like 的数据变化也是通过 reduce 来处理么?对于 Like 来讲,也是个 event ?
  3. Tweet 与 Like 共同使用同一份数据,如何保持一致?比如 A 持有数据 { like: 100, author: 'xxx', 'weibo': 'xxx'},A 将 like 属性传递给 B:B 拥有数据 {like: 100}。那么假设 B 在被 click 时 like++,那么是只修改自己的 like?还是 fire('like-click')?还是先修改自己的like并且fire('like-click')?
otakustay commented 8 years ago
  1. 暂时不想有,其实我是反prop + state的,react现在也是在慢慢强调stateless component,这是未来
  2. Tweet数据变化时,根据绑定关系将部分传给LikeLike拿到数据后看引用是否相同,不同就说明要刷视图,就用vdom还是啥的刷呗。这个过程没有reduceLikereduce用来处理Like上发生的事件,Tweetreduce用来处理Tweet上发生的事件,而datachange不在这里的狭义的“事件”的范围里(不然死循环了啊)
  3. 两种方法,扎实的玩就是B在被点击时,fire('like-click'),因为B知道这个数据是来自外部的,不是自己创建出来的,所以不是这个数据的owner,自然不能改;快捷的玩法就是上双绑
otakustay commented 8 years ago

因为B知道这个数据是来自外部的,不是自己创建出来的

其实这还是stateprop的区别,但我并不采用react的这种明确区分的方案,我的思路是绑定,所以就会变成:

  1. 所有的东西都是prop
  2. 如果你数据保持同步的,自己乖乖双向绑定
  3. 你用单向甚至单次绑定,就说明你根本不想要数据同步,那OK反正我全是immutable也不会修改你的数据,我自己就管理着了

也就是说,一个数据是prop还是state,其实是由使用它的人决定的“是不是有双绑”,而不是这个组件开发者就作了决定

jinzhubaofu commented 8 years ago

对于所有东西都是 prop 这个事情有一个困惑。

比如一个 Calendar 组件,这个组件自己有一个浮层,展开/收起状态一个和业务数据无关的数据,它应该保存在哪里?如果没有 state 只有 prop 那也就只能和业务数据一起、放到的 model / store 里边么?

那我有多个 Calendar 呢? 这样?

另外,这个组件应该如何应对单向绑定、双向绑定、单次绑定,是外部声明时指定的?这几种类型的绑定和 reduce 是什么关系?

otakustay commented 8 years ago

我们抛开react和它的stateprop这些概念,然后让我详细的解释一下这个事情,下文所说的“状态”不指react中的state,是一个纯粹的概念

在UI的领域,其核心始终是一个状态表达一个视图界面,用函数的话来说,就是F(state) => view,这个状态我们称为视图状态

同样的,在业务的领域,我们有一个状态表达业务的上下文,我们称为应用状态

事实上,我们真正关心的,并不是“视图状态分为公开的和私有的”这回事,对于整个应用系统,我们关心的是一件事:视图状态中感兴趣的部分与应用状态是同步的

那么这里就产生了一个问题,这个感兴趣的部分到底是什么,是谁感兴趣,谁来决定?我的论断就是:由实际的业务开发者决定

因此,视图或者说组件并没有资格自己决定哪部分可以与应用状态同步,哪部分不能同步。或者换一个更现实的说法,视图应该能做到状态完全同步的情况下,随时刷新都保持和刷新前像素级的相同。为此,视图不可能有所谓的内部状态,因为持有内部状态意味着“视图自行决定了可保持同步的部分”也意味着“刷新后像素级同步”不再可能

针对上面说的实例:

比如一个 Calendar 组件,这个组件自己有一个浮层,展开/收起状态一个和业务数据无关的数据,它应该保存在哪里?

这个浮层展开/收起的状态恰恰不应该是一个内部状态,因为Calendar完全没有理由去假定外部不想保持这个状态的同步,而在现实中我们也很容易举出一个反例:

作为一个酒店预订系统,当用户填写了半个表单,正在选择离店日期时,不小心手贱按到了刷新按钮。在所有状态保持同步的情况下,刷新后的页面会自动滚动到离店日期这个Calendar所在的位置,Calendar的浮层处于打开状态,让用户可以很自然地回忆起“我刚刚正在填写离店日期”这个事实

显然在这个场景下,Calendar的使用者对“浮层展开/收起”这个状态是有同步的需求的,它希望可以存储这个状态以在一定情况下恢复视图。那么自然Calendar不能把这个状态藏在自己内部硬说你就不能这么干,不爽不要用我……

因此,在一个绑定为基础的环境下,有以下的特点:

  1. 组件所有的状态都是公开的
  2. 随着用户的操作,组件的状态由自己完成修改
  3. 组件的状态同样可以由外部修改,产生视图的变化

这样一个组件是“自治”的,没了谁他都能活。而如果你对这个组件的部分或全部状态感兴趣希望保持同步,就可以使用双向绑定来进行这些状态的同步,如value <-> checkOutDate,那么Calendar的值发生变化时,checkOutDate也相应同步其值

如果你对组件的另一些状态不感兴趣,也不想对其进行操作,那么使用单次绑定、常量绑定甚至不进行绑定(使用组件默认初始值)即可,之后这个组件的状态你无法获知,这种组件自生自灭,用react的话来说,uncontrolled

如果你并不对一个组件的某个状态当前的值感兴趣,但是在需要的时候要命令组件进行这个状态的改变,那么使用单向绑定即可。单向绑定会导致被绑定的属性和绑定源属性在某些时候是不同步的(组件自己修改了状态,因没有双向绑定未进行同步),但这也是使用者自己的选择(事实上很少会出现这情况)

当然用这种全开放的设计,相比react允许开发者保持一些私有状态,一定会增加一些复杂度,比如说多个属性之间有相互的约束,那么它们一部分被绑定一部分未被绑定,就容易产生混乱,这比较考验组件开发者的能力

以上说的是与视图相关的,即决定最终组件的外观的状态。当然我也明白组件会在一些情况下真的想要私有的状态:

  1. 与视图无关的数据,比如一个数组变成一个Set来辅助某些查找逻辑
  2. 某组件死活要让一个状态是私有的,打死不公开出去

这种情况下,我给的解决方案是:使用Symbol,绑定并不会读取放在状态上的各个Symbol所以可以创造出私有的内容来,但也同时要注意这样做的代价是:

  1. 组件可能不再是完全可恢复的
  2. 使用到Symbolreduce函数不能被复用(除非你把Symbol也一起分享出去)

再来说reduce这东西,我想是我没解释清楚reduce是个啥……

用最直观的话来说,reduce是event handler,用于处理模板上写的on-click="updateName",此时就对应一个叫updateNamereduce,这个元素被点击时就调用这个函数,计算出新的data,用新的data刷新视图

reduce和绑定没有任何关系(除非作死写on-data-change="theFuckingReducer",请务必不要作死)

绑定则是在模板中声明的,这个和vue或者ng很相似,我也不作特别详细的描述了,对于组件来说,绑定是透明的,它不需要去应对或者实现绑定,当reduce产生一个新的data时,绑定层会自动将新的和老的data进行对比(因为是immutable,对比非常快速),取出相应的变化,通过从模板上解析到的绑定关系将这些变化分发出去

双向绑定是标准的一个单向绑定加一个匿名的reduce(目标组件发生data变化事件时更新自己的data

jinzhubaofu commented 8 years ago

在之前的概念中组件自身是个状态机,自身维护其状态;但是在新的框架下组件并不是一个状态机,而是一个渲染函数F(state) => view。状态的维护通过 vm/store 来完成,vm/store 的状态迁移是通过 function/immutable 完成的。这个我理解了。

对于各种绑定,我梳理一下,不知道是不是正确:

  1. 如果是双绑,那么 Tweet.like 与 Like.like 有绑定关系,Like 中 reduce 掉 like 的值,那么 Tweet.like 也会被更新。
  2. 如果是单向绑定,那么 Like.like 自己使用 reduce 修改时,Tweet.like 不会变化;Like fire('like-click') 事件 Tweet 执行 reduce 改掉 Tweet.like 那么 Like.like 也会变化;
  3. 如果是单次绑定,那么 Tweet.like 和 Like.like 没有绑定,各玩各的。

如果是上边这样描述的情况,Like 在点击的时候为了保证自己正确,应该怎么处理?类似这样?

function reduce(data, fire) {

  // 单向绑定
  if (this.vm.isDanbang(data.like)) {
    fire('like-click');
    // 自己不+1,坐等 Tweet 同步我
    return data;
  }

  // 双绑 or 单次 or uncontrolled
  return set(data, 'like', data.like + 1);

}

或者乐观更新?

function reduce(data, fire) {

  // 告诉上边被赞了
  fire('like-click');

  // 我先更新了啊,上边你要是觉得不对,你再 reduce 一下吧
  return set('data', 'like', data.like + 1);
}
otakustay commented 8 years ago

是乐观更新,且仅仅要同步数据的话,不需要触发任何事件,绑定层会帮你做掉:

Like确保自己正确始终是一个标准的reduce

import {set} from 'diffy-update';
class Like {
    events = {
        likeIt(data) {
            set(data, 'like', data.like + 1)
        }
    }
}

模板里:

<button type="button" on-click="likeIt">👍</button>

然后Tweet的模板:

<!-- :foo 表示双绑 -->
<Like ui-like=":likeCount" />

之后绑定性会有这个逻辑:

  1. 调用reduce后得到新的数据
  2. 新数据与原来的比对,发现like值有变化,从100变成了101
  3. 更新Like的视图,这里应该是vdom或者其它机制
  4. 发现Like组件的likeTweet组件的likeCount是双绑关系
  5. 自动将Tweet的数据的likeCount变为101(Immutable更新,变成一个新的数据对象)
  6. 调用TweetupdateData(类似react的setState吧)给新的数据更新视图
  7. 更新视图时,会又把likeCount这个101送给Likelike属性,但Like会判断值是一样的所以就不会再更新自己了
  8. 如果TweetlikeCount也有更外层的双绑,回第4步继续走

所以说数据的绑定同步是隐式的,并不需要任何的事件的触发和监听(内部其实有一个类似data-change之类的事件吧,应该)

事件只是用来干类似clickdrag啊这种不会影响自身数据变化的事情的

otakustay commented 8 years ago

简单来说,一个组件始终保持自己的状态和视图是同步的,组件外面的同步就交给绑定来走了,有双向绑定的情况下,这个状态会一路向上同步直到应用状态里,没有双向绑定那就脱节了也没办法,但组件自己还是和视图一致的

jinzhubaofu commented 8 years ago

那么我是不是可以这样理解:组件状态中的所有数据,组件不必关注是不是与应用状态绑定的。如果与应用状态脱节的,那么组件不需要去管『脱节数据』要不要与上层组件or应用状态的同步。这些『脱节数据』就相当于是一个『私有』的状态,自己玩好就行了。

还有另外一个问题,子组件得到的数据是 computed,那么 reduce 时应当如何处理?

例如:<Like ui-like=":likeCount * 1000" />

otakustay commented 8 years ago

那么我是不是可以这样理解:组件状态中的所有数据,组件不必关注是不是与应用状态绑定的。如果与应用状态脱节的,那么组件不需要去管『脱节数据』要不要与上层组件or应用状态的同步。这些『脱节数据』就相当于是一个『私有』的状态,自己玩好就行了

是的,是不是“私有”的就是决定于外部怎么看你

还有另外一个问题,子组件得到的数据是 computed,那么 reduce 时应当如何处理?

:likeCount * 1000是个绑定表达式,解析这个表达式后,绑定层会关心likeCount的变化,发生变化时调用like.updateData({like: likeCount * 1000})

有表达式的属性是不能双向绑定的,我可不负责给你从乘法变成除法反着算……

jinzhubaofu commented 8 years ago

这个 computed 一定是很常用的。那对于 Like 来讲,它是没有办法知道这个 like 是不是 computed 出来的。那它还怎么 reduce 呢?

otakustay commented 8 years ago

对于Like来说,它根本不知道,它只知道自己有like这个属性,外面给的值是23000(其实外面是通过23 * 1000算的,但它不知道)

这个compute逻辑是在绑定层做的,任何一个组件都不感知这个逻辑

对于Like来说,他只知道自己现在是23000,点击后变成23001,所以他的reduce逻辑还是一样的,依旧是like = like + 1

然后23000变成23001后,绑定层发现这个值变了,再去看这个值是怎么绑定的,发现绑定的关系是like -> :likeCount * 1000,然后这是个单向绑定,所以就不同步给上面的Tweet组件了,此时Tweet组件依旧是23000,这就是状态脱节了,没办法

如果你既要这个compute功能,又要双向绑定保证不脱节怎么办呢,那偷懒的可以自己定义get和set了,在Tweet里定义

{
    get likeCount() { return this.realLikeCount * 1000; }
    set likeCount(value) { this.realLikeCount = value / 1000; }
}

因为有get和set,所以对其它流程也是透明的(看上去都是普通属性而已)

不过这里引入了几个问题:

  1. 在IE8-下完蛋了
  2. 有get和set的data在传给reduce更新的时候,还能不能保持get和set,确实是个问题

总的来说我就不大推荐这样做,另一个复杂点但稳健的办法是自己管这个双向绑定。因为双向绑定其实是一个单向绑定+一个事件绑定,所以还是能直接手写的,把Tweet这么写

import {set} from 'diffy-update';

class Tweet {
    events = {
        syncLike(data, fire, newLike)  {
            return set(data, 'likeCount', newLike / 1000);
        }
    }
}

而模板里这么写:

<Like ui-like=":likeCount * 1000" on-like-change="syncLike($event.newValue)" />

模板定义了一个属性的单向绑定,一个事件like-change的绑定,js用一个reduce更新自己的数据进行同步

这里的on-like-change将是框架提供的一个语法糖,效果等于on-data-change加一个if判断修改了like并计算出newValue

这个双绑其实和ng2一样,ng2的[(ngModel)]其实就是ngModel的单向绑定和onNgModelChange的事件绑定

errorrik commented 8 years ago

个人喜好来说:

  1. 我并不喜欢区分prop和state,原因灰大讲的太清楚了
  2. 我也不喜欢reduce这个词,原因是对ui的使用者来说会难以理解
otakustay commented 8 years ago

reduce也就我们内部说说了,真对外的话就叫event handler得了

errorrik notifications@github.com于2016年6月24日 周五下午5:55写道:

个人喜好来说:

  1. 我并不喜欢区分prop和state,原因灰大讲的太清楚了
  2. 我也不喜欢reduce这个词,原因是对ui的使用者来说会难以理解

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/ecomfe/efe/issues/2#issuecomment-228305943, or mute the thread https://github.com/notifications/unsubscribe/AAnCPQFQcZjS6KI_2qwUGy08K-JoZuRXks5qO6mCgaJpZM4I7t1g .

jinzhubaofu commented 8 years ago

<Like ui-like=":likeCount * 1000" on-like-change="syncLike($event.newValue)" />

表达式这样处理没问题,就是有点麻烦。

OK 我的困惑都搞清楚了,多谢灰大。

otakustay commented 8 years ago

基于 https://www.zhihu.com/question/48099761/answer/109486302 的内容,我觉得引入Optimistic UI也是很好的,在我的这个体系下,每个event函数的返回从Promise<newState>修改为[Promise<newState>, optimisticState]就行了,其中optimisticState可选(即可以返回长度只有1的数组)

比较头疼的就是返回个数组看着难受就是了……

当然也可以从Action的角度入手,一个event应该返回一个Action,这个Action才是生成新的newState的实际函数,这就是Action Creator -> Action -> State的路子,是不是有实际比较大的收益(如newStateoptimisticState真的可以是同一个Action弄出来的,我很怀疑这点)得实际实施的时候仔细看看

ustccjw commented 8 years ago

看起来和这个有点像 https://github.com/reactjs/react-future/blob/master/09%20-%20Reduce%20State/01%20-%20Declarative%20Component%20Module.js

otakustay commented 8 years ago

咦怎么就future了……我有这么牛逼吗- -

这一套有不少地方是借(抄)鉴(袭)react的,其实是把redux这个应用结构放到了组件里面来,不过整个组件的模式还是MVVM式绑定而不是react的那种模式

ustccjw commented 8 years ago

就是这么牛逼...

ustccjw commented 8 years ago

@otakustay 我觉得 redux 也没多少意义来着。实际开发中,我觉得数据同步才是重点,实际上我们需要一套智能的数据模型(缓存和同步)。抛开性能问题不说,目前在项目中有尝试使用 falcor(智能的数据模型) 和 async-props(基于 react-router 异步更新视图)来实现前端的 M(falcor)V(view)C(react-router)。对于 restful API 接口,我们可以在前端实现一套抽象的 falcor 路由(data-model),然后再抽象出一个 ui-model。这样就很简单啦,action 实际上就是执行 ui-model set 和 data-model call 这样的操作,然后刷新整个视图。路由变化和 action 最终的动作都是刷新视图,有了 async-props,我们可以根据 router 从 model 中异步获取数据(自动缓存和同步)。

9ab3cd51-510b-4a08-8c1f-89c40c0b0276

otakustay commented 8 years ago

@ustccjw 我厂毕竟有不少奇怪的应用场景,比如各种广告要兼容IE6,所以不少东西还是自己折腾做新的

你的这个图其实我蛮理解的,不过model要再过controller送到view(感觉类似MVP了)有没有可能相对复杂

另一个问题是,其实一个架构真正麻烦的可能在view的内部这棵component tree怎么组织,外部反而还好

ustccjw commented 8 years ago

一次把 router 对应 view 需要的数据都从 model 中取出(router component 挂载 loadProps)。这样虽然暴力,但传统 MVC是实际上也是以 router 为粒度加载页面的。这样做的另一个好处是能够真正做到声明式构建页面,因为不存在命令式获取数据的问题。

otakustay commented 8 years ago

我又细化思考了一下,现在觉得把event做成一个纯函数并不是很现实,我们不得不承认一部分event是需要side effect的,比如拖拽的当前位置保存(如果完全走数据 => 视图更新的逻辑,可能会掉帧),又比如一些特定情况下才注册的事件(scroll这种很耗性能的)

所以我觉得可以这样:

  1. event还是作为类的方法,不单独放在一个events对象里面
  2. 每个函数还是接收data, fire, ...eventArgs,返回一个data,但允许使用this放一些额外的私有state(比如当前鼠标位置之类的每次都更新性能吃不消的),但推荐尽量少放这种东西
  3. 如果一个函数有副作用,必须添加@sideeffect标记(暂时我也不知道这个标记啥用,但总之先加着)
  4. 有些event是不会修改数据的,那就直接return this.data,绑定系统会判断没变化就退出