Javascript中的状态管理与监测其实一直都在正解决的路上,语言标准方面,从已废弃的Object.observe API,到进入到ES6标准的Proxy,但都只针对单一对象状态监听,对多层对象实现监听只能借助于外部库,于是产生了我们常见的一些解决方案,如RxJS,提出了新的概念 Reactive Stream,即可被观测的事件流(observable event streams);另一个流行库Mobx则遵循Observer Pattern实现,另外还有一个叫Redux,基于CQRS实现。别说去应用,光是理解这些概念,开发者们都要先掉几根头发。
其实上面几种方案都是围绕着可被观测的对象(Observable Object)这一主题,除了上面几个流行库,在此强烈推荐一个新的更好的解决方案:edata。
以下我们用代码来说明对比这几个状态管理库的核心用法:
RxJS observable
export class DataStore {
public count: BehaviorSubject<number> = new BehaviorSubject(0);
update(newValue: number) { this.count.next(newValue); }
}
export class ButtonComponent {
public count: number;
constructor(private dataStore: DataStore) {
dataStore.count.subscribe(x => this.count = x);
}
onBtnClick() { this.dataStore.update(this.count + 1); }
}
上面的代码,我们在 DataStore
设定了一个可供订阅的 count
,初始值为0。ButtonComponent
在 constructor
添加了一个对 count
的订阅(subscribe
),并将变动会存到 this.count
里。按钮点击后触发 dataStore.update()
动作,让数据源中 count
加一,进而触发ButtonComponent
中 this.count
改变。
Mobx observable
export class DataStore {
@observable public count: number;
@action
update(newValue: number) { this.count = newValue; }
}
export class ButtonComponent {
public count: number;
constructor(private dataStore: DataStore) {
observe(dataStore, 'count', x => this.count = x.newValue);
}
onBtnClick() { this.dataStore.update(this.count + 1); }
}
Mobx
使用 @observable
装饰器使对象可被观测,以及 @action
装饰器来定义反应式方法,非常直观。不过这里注意一下 observe()
这段代码,有两个坑:1)dataStore
必须为对象,若是 primitive value
必须要包在 observable.box()
中,增加了复杂度;2)count
值必须要变化才会触发,若改变的是同值,则不会触发!
为了演示 Mobx
这个问题,我们设定一个情景,我们加入 isDataLoading
变量表明是否数据是否已加载。
export class DataStore {
@observable public data:number;
@action
update(newValue: number) { this.data = newValue; }
}
export class ButtonComponent {
public data: number;
public isDataLoading: boolean;
constructor(private dataStore: DataStore) {
observe(dataStore, 'data', x => {
this.data = x.newValue;
this.isDataLoading = false;
});
}
onBtnClick() {
this.isDataLoading = true;
this.dataStore.update(this.data);
}
}
这里会产生一个BUG,isDataLoading
一直为 true
,因为 this.dataStore.update(this.data)
这句没有改变数值,导致 observe
并没有触发。解决方案就是使用 data = observable.box()
,然后使用 data.set(value)
来进行数值设置。
另外还有一个坑,在 Mobx v4
中数组类型判断要非常注意:
var numbers = observable([1,2,3]);
Array.isArray(numbers) // false ???
Array.isArray(numbers.slice()) // true !!!
简单对比 RxJS
和 Mobx
,可以看出 RxJS
是比较经典的 pub/sub
模式实现,Mobx
更贴近于声明式 + callback风格,但也有不少坑来自于它本身的实现。
重点来了,
快速上手,不想了解太多概念,坑又少,扩展性好,推荐使用edata
我们来看下同样的例子,使用 edata
如何实现:
import {edataProxy} from 'edata'
export const dataStore = edataProxy({
data: 0,
update (newValue: number) {
this.data = newValue
}
})
export class ButtonComponent {
public data: number;
public isDataLoading: boolean;
constructor(private dataStore: EdataProxy) {
dataStore.__watch__('data', x => {
this.data = x.data.value;
this.isDataLoading = false;
})
}
onBtnClick() {
this.isDataLoading = true;
this.dataStore.update(this.dataStore.data);
}
}
上面的代码来类比 Mobx
,可以看到并没有用到非标准的装饰器语法,更贴近原生,并且没有 Mobx
的那些坑。
这里要注意edataProxy()
方法,它使得 dataStore
可以直接使用JS原生方法来存/取对象,但依赖于ES6 Proxy
,若要支持IE浏览器,则尽量使用 edata的低阶API。
更多edata介绍可以参考另一篇文章:JS中反应式数据存储方案对比 RxJS, Mobx 与 edata。
这个库的作者当然也是本文的作者,也需要同学们来一起共建,方式包括但不限于:使用并提出建议,PR,Star,转发等。