-
-
Notifications
You must be signed in to change notification settings - Fork 80
不可变数据的实践
不可变数据是单向数据流带来的一个概念,它的意思是,数据是不可变的,如果你需要更新数据,那么请创建一份新的数据。
如果没有不可变数据,当我们对比两个引用类型是否相同时,需要层层对比,这对性能的消耗是非常不确定的,简单的对象可能瞬间就对比完了,但是复杂的带有嵌套结构的对象,可能会非常慢。
如果有了不可变数据,这一切就变得非常简单,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();