Skip to content

不可变数据的实践

musicode edited this page May 31, 2017 · 7 revisions

不可变数据是单向数据流带来的一个概念,它的意思是,数据是不可变的,如果你需要更新数据,那么请创建一份新的数据。

如果没有不可变数据,当我们对比两个引用类型是否相同时,需要层层对比,这对性能的消耗是非常不确定的,简单的对象可能瞬间就对比完了,但是复杂的带有嵌套结构的对象,可能会非常慢。

如果有了不可变数据,这一切就变得非常简单,a !== b 即可判断是否变化。

但不可变数据也并非完美,下面我们从不可变数据的问题说起。

小白先行

先看两段小白的日常代码:

var list = this.get('list');
list.push('new');
this.set('list', list);
var user = this.get('user');
user.name = 'new';
this.set('user', user);

请问,这种情况下框架能知道 list 变了吗?

别问我为啥有人会这么写,反正大家都爱这么写...

作为框架作者来说,碰到这种写法是崩溃的,你得一次一次地说,set 之前请先浅拷贝,否则我不知道你改了它!

浅拷贝

为了方便用户浅拷贝,我们为组件实例增加了 copy 方法,它的方法签名如下:

copy(target: any, deep: boolean): any

于是上面两段代码修改如下:

var list = this.copy(this.get('list'));
list.push('new');
this.set('list', list);
var user = this.copy(this.get('user'));
user.name = 'new';
this.set('user', user);

看起来一切都很正常,这时候性能会给你当头一棒,什么!不是说不可变数据性能很好嘛!!

性能灾难

在一个聊天室的场景,控制人数,控制发送频率,这不会有问题。

当聊天室有一万人在线,每个人都在频繁发消息,这时对于单个用户来说,一秒内可能会接收一万条消息,而我们采用了不可变数据,因此为了更新数据,相当于要浅拷贝一万次,如果拷贝的这个数组非常大,两个因素叠加,性能问题非常恐怖。

我们要保证不可变数据,同时要保证极端场景的性能,于是只好增加一个操作数组的方法,如下:

append(keypath, item)

这个方法要实现以下需求:

  • 确保数组一定会变化,不论是同步或是异步变化
  • 确保同步 get(keypath) 能获取变化后的数组

大致的逻辑就是,set 会往下一个执行周期写入一些操作,这些操作不会立即执行,而等到 nextTick 到来时执行。当调用 get 时,为了保证数据的正确性,会立即执行之前写入的操作。

当我们执行这些批量操作时,需要触发数据对比,有数据变化要通知已注册的 watchers。

确认没有性能问题了吗?

回到刚才的聊天场景,我们知道浏览器中的 DOM 数量是需要控制的,如果聊天列表的长度不受控制,几十分钟的聊天可能产生几万条数据(想想锤子发布会的盛况...),浏览器最后只能崩溃了。

因此每当我们接收到一条消息,肯定需要 get('list.length')。刚才说到,get 需要立即执行变化操作,那么这个场景的浅拷贝频率是否依然过高呢?

为了分析这个问题,我们把场景再具体一些,一秒接收 100 条消息,按照每个 tick 10 毫秒来计算,一秒等于 100 个 tick

如果这 100 条消息是均匀分布,也就是一个 tick 接收一条消息,那么频率是一秒浅拷贝 100 次。

不得不说,这个频率是挺高的,而且仅仅为了“不可变数据”这个名头,在这傻傻的一次一次地浅拷贝。

组件体系

接上,如果用组件方法(如 append)修改数组,不浅拷贝,那么可选的一个方案是数组带一个标记,但是这个标记何时删除是一个问题,尤其是在组件体系中,数据可以一层层的往下传,这种场景用标记法看着就很 hack。

结论

综上,我们还是要求不可变数据,只是提供了一个 forceUpdate 方法,便于在性能要求较高的场景,手动刷新视图,如下:

var list = this.get('list');
list.push('new item');
this.forceUpdate();
Clone this wiki locally