为什么要用Immutable.js来代替Javascript的对象
翻译版本,原文请见
把你的数据看成是不可变的会带来很多的好处.实际上在React背后有个原则:React的元素是不可变的.你可能也会对学习不可变App构架有很大兴趣.
但是使用Immutable.js的好处是:
function toggleTodo (todos, id) {
return todos.update(id,
(todo) => todo.update('completed',//update是immutable的方法
(completed) => !completed
)
)
}
跳过使用普通的Javascript对象(把他们看作为immutable,可以选则使用例如seamless-immutable之类的助手函数),像这样?:
function toggleTodo (todos, id) {
return Object.assign({ }, todos, {
[id]: Object.assign({ }, todos[id], {
completed: !todos[id].completed
})
})
}
// Using updeep
function toggleTodo (todos, id) {
return u({
[id]: {
completed: (completed) => !completed
}
}, todos)
}
一个非常大的对象...
让我们假设todo list 里面有100,00个任务:
var todos = {
⋮
t79444dae: { title: 'Task 50001', completed: false },
t7eaf70c3: { title: 'Task 50002', completed: false },
t2fd2ffa0: { title: 'Task 50003', completed: false },
t6321775c: { title: 'Task 50004', completed: false },
t2148bf88: { title: 'Task 50005', completed: false },
t9e37b9b6: { title: 'Task 50006', completed: false },
tb5b1b6ae: { title: 'Task 50007', completed: false },
tfe88b26d: { title: 'Task 50008', completed: false },
⋮
(100,000 items)
}
我刚刚完成第50005件任务.
现在我想把它标记位完成.
使用普通Javascript 对象
var nextState=toggleTodo(todos,'t2148bf88')
这个单一的操作哟花费134ms来运行.
为什么?因为当你使用Object.assign
,Javascript的浅复制拷贝每一个源的每个属性到目的地.一次一个.
我们有100,000个todos,所以意味着有100,000个属性要拷贝.
这就是为什么要花这么长的时间.
为什么要这么做?
在Javascript中,对象默认是可以突变(mutable)的.
当你克隆一个对象,Javascript有每一个属性的拷贝,所以两个对象变得完全分离的.看下图
这就允许你在拷贝以后改变任何对象的属性,对象之间也不会相互影响.甚至在把这些对象处理为不可变(immutable),Javascript也还是按照mutable来处理.
使用Immutable.js
var todos = Immutable.fromJS({
⋮
t79444dae: { title: 'Task 50001', completed: false },
t7eaf70c3: { title: 'Task 50002', completed: false },
t2fd2ffa0: { title: 'Task 50003', completed: false },
t6321775c: { title: 'Task 50004', completed: false },
t2148bf88: { title: 'Task 50005', completed: false },
t9e37b9b6: { title: 'Task 50006', completed: false },
tb5b1b6ae: { title: 'Task 50007', completed: false },
tfe88b26d: { title: 'Task 50008', completed: false },
⋮
(100,000 items)
})
使用Immutable.Map
来代表我们的数据,更新第50005条任务
var nextState=toggleTodo(todos,'t2148bf88')
这个操作仅花费1.2ms时间去运行.速度提升了100倍以上!
为什么会这么快?
持久数据结构
持久数据结构(Persistent data structures)强力限制所有的操作都要返回数据结构的新版本,保持原数据结构的完整性,不能更改原数据结构.
这一点暗示所有的持久化数据都是不可变的.
在这个给定的限制下,实现持久化数据结构的库可以进行很好的优化,因为这些库知道我们不会改变我们的数据.
让我们看一个优化
使用tries来优化
为了直观一点,试一个小例子
想象存储一个键-值映射:
我们可以把这个数据结构存储到单一的Javascirpt对象中:
const data = {
to: 7,
tea: 3,
ted: 4,
ten: 12,
A: 15,
i: 11,
in: 5,
inn: 9
}
但是我们怎么才能创建一个trie来代替js的对象呢?他的结构看起来是这个样子的:
基本上你可根据图上的路径从root开始获取到你需要的值.
如果你从root开始找data.in
,根据标记i
和n
的路径.可以找到包含5
的节点.
那么,怎么修改呢?
让我们思考一下把键tea
的值从3
改为14
.
我们可以创建一个新的trie,尽可能的使用存在的节点.
老的树形结构仍然存在,而且没有变化.在实例中你可以保留一个引用
在上图中如绿色部分所示,我们仅仅只需要更新4个节点来更新这个数.其他的节点是可以重新利用的。
下面这个图展示Immutable.js怎么实施Immutable.Map
.创建一个每个节点有32个分支的树.
当我们更新一个单个项目,仅仅只要一些节点需要被重新创建.
Immutable.js借助crazy advanced techniques保持树形结构的紧凑,根据各种子树的各种属性来创建多种类型的节点.
并不总是如此...
不要把本问的本意理解为“你总是需要Immutable.js“.不是这个意思,我只是想强调一下他的好处.解释一下为什么推荐要使用他.
数据结构是很重要的,但是当我编写软件的时候,我首先要尝试最简单的方式.我过去使用数组和对象,之后当我需要速度提升的时候,我使用Immutable.js,或者是在我遇到到我需要他的时候.在只要少数的条目,还有小的对象和集合的时候,我就不会使用Immutable.js.
是不是意思是我可能会返回去并且在后面在改变?
对!非常好!如果你的数据接入是通过单一,组织良好的模块.例如:
// -- Todos.js --
export const empty = { }
export function add (todos, id, todo) {
return Object.assign({ }, todos, { [id]: todo })
}
export function getById (todos, id) {
return todos[id]
}
估计所有的应用代码总是要使用这个模块来获取数据.当你想改变内含的数据结构时,你仅仅需要更新这个文件.
这个我们叫做”实体模块“,封装了代表整个软件系统的所有内容.这个概念来自”Clean Architecture“.我计划以后来写写这个问题.
不要把应用的逻辑和数据结构耦合在一起
我很了解应用逻辑和数据结构不耦合在一起的艰难之处.这是因为我们不知道在未来数据怎么来获取.
例如:我们的todo app现在管理着100,000任务.我们改为使用Immytable.js.现在每个部分都足够好和足够快.
突然需求来了:”任务要有一个安排者”.(类似老师布置作业给学生),”用户应该可以看到任务是谁给安排的”.
function findByAssigneeAsArray (todos, assigneeId) {
return todos.filter(
(task) => Task.containsAssignee(task, assigneeId)
).toArray()
}
用户开始抱怨app变慢了,分析揭示上面这个函数是个大问题.
使用上面这段代码需要序列搜索100,000任务.这样做怎么能快的起来?
要优化这个案例,我们需要改变内在的数据结构
这需要保持一个反向的查询表,连接任务安排人和任务列表的TaksID.这个优化的修改实例来自于[Taskworld](https://taskworld.com/)
.
如果我们的reducer/selector/view代码和数据结构直接连系在一起,要做出这样的改变非常难.
所以,如果我们想快速迭代,我们需要确保很容易做出修改.从开始就保持代码整洁,书写测试,建立持续集成.
感谢阅读!
更多的讨论在Reddit.