翻译|Immutable.js,持久化数据结构和结构共享

为什么要用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有每一个属性的拷贝,所以两个对象变得完全分离的.看下图


100,000个属性被(浅)复制到目的地
100,000个属性被(浅)复制到目的地

这就允许你在拷贝以后改变任何对象的属性,对象之间也不会相互影响.甚至在把这些对象处理为不可变(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,根据标记in的路径.可以找到包含5的节点.

那么,怎么修改呢?

让我们思考一下把键tea的值从3改为14.

我们可以创建一个新的trie,尽可能的使用存在的节点.

老的树形结构仍然存在,而且没有变化.在实例中你可以保留一个引用

在上图中如绿色部分所示,我们仅仅只需要更新4个节点来更新这个数.其他的节点是可以重新利用的。

下面这个图展示Immutable.js怎么实施Immutable.Map.创建一个每个节点有32个分支的树.

Immutable.Map的实施
Immutable.Map的实施

当我们更新一个单个项目,仅仅只要一些节点需要被重新创建.

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.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容