Life of xhu

About

Thinking in State Structure Design of Redux

Feb 21, 2016

  |   #Redux

在我之前看过的一片关于Redux的入门文章里, 曾经提到过, 对于store里的state对象, 建议使用范式化的数据结构, 当时我并不理解为什么要这么做,因为在我看来这样无疑多使用了不少的存储空间而并没有什么显而易见的好处, 日常的使用使用简单的数组应该就足够了.

理想总是很丰满, 直到在真正的开发中, 我终于因为使用数组存储数据而踩了坑.

应用场景是这样的, 在我编写MySnippets这个应用的时候, 有一个需求, 就是要把输入的snippets都存到db里, 并且要对记录进行CRUD操作, 从db里获取数据后我都存到了Redux的state里, 然后在UI上进行操作.

于是关于state里数据结构的折腾就开始了...

数组

第一眼看到这样的需求, 我相信很多人包括我, 第一反应肯定是通过数组来存储state里的snippets, 具体代码如下:

[{
  id: 1,
  content: 'test1'
}, {
  id: 2,
  content: 'test2'
}]

这样的存储方式在一开始编写左侧的list的时候都没有问题, 但是在加上右侧的editor之后, 问题来了.

在右侧的编辑框里, 每次改变代码, 左侧list的改动也是实时进行的, 这时使用数组存储的一个缺点就暴露出来了, 就是无法通过一个id快速的找到数组中响应的项, 基本上每次都进行O(n)的遍历, 对于实时保存来说, 数据量小没感觉, 一旦数据量一大, 肯定会有性能损失.

那么有没有能通过id快速找到对应记录的方式呢, 当然有了.

对象

聪明的你肯定已经想到了, 既然每次编辑都要通过id来找到相应的snippets, 而id又是唯一的, 那么建立以id为key的对象就可以快速找到snippet了:

{
  1: {
    id: 1,
    content: 'test1'
  },
  2: {
    id: 2,
    content: 'test2'
  }
}

这样看来, update和delete这样需要对特定snippet操作的性能问题就解决了, 然而这种方式是最好的吗? 当然不是了, 坑马上就来了.

这种对象存储有一个最大的弊端就是, 这样存的结果, 是无顺序的!

JavaScript规范里并没有规定一个对象里键值对的排列, 在这个应用里我有一个需求, 就是在编辑一个snippet之后, 把它放到列表的最顶端, 这个用对象几乎是不可能实现的, 对于需要排序的需求, 最好的做法就是额外维护一个主键构成的数组, 也就是接下来要提到的范式化存储了.

范式存储

这时就得用上前文里提到的范式存储了, 在这个应用里我们使用的范式并不复杂, 仅仅是实现了第二范式(2NF)

第二范式(2NF)要求数据库表中的每个实例或行必须可以被唯一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的唯一标识。

简单地说就是把唯一的健和内容相分离, 这样存储的记录是这样的:

{
  ids: [1, 2],
  entities: {
    1: {
      id: 1,
      content: 'test1'
    },
    2: {
      id: 2,
      content: 'test2'
    }
  }
}

乍看上去就是把前两个综合起来, 实际上也的确是, 不过这样存储数据的时候, 就同时拥有了数组和对象的优点了.

首先, 在进行常规的CRUD操作的时候, 可以根据id快速的在entities里找到操作的对象, 也就是对于一个数据的集合, 当我们需要操作一个单独项的时候, 可以通过主键在state里迅速找到这条记录.

而当进行排序相关的操作的时候, 只需要改变ids里的顺序即可. 而且后来加上的filter功能也很容易实现, 因为不需要去改变entities里的值, filter的结果反映在ids这个数组里, 只要保证list的内容和顺序与ids数组保持一致就行.

这样一来, 使用范式化的存储之后, 对于数据的操作都可以满足了. 今后在编写比较大型的程序的时候, 数据结构一定是要好好设计的, 避免再有踩坑的情况出现.

Ref: