Javascript 状态为对象数组 vs 由 id 键控的对象
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/38445006/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
State as array of objects vs object keyed by id
提问by nickcoxdotme
In the chapter on Designing the State Shape, the docs suggest to keep your state in an object keyed by ID:
在Designing the State Shape一章中,文档建议将您的状态保存在一个由 ID 键控的对象中:
Keep every entity in an object stored with an ID as a key, and use IDs to reference it from other entities, or lists.
将每个实体保存在一个以 ID 作为键存储的对象中,并使用 ID 从其他实体或列表中引用它。
They go on to state
他们继续陈述
Think of the app's state as a database.
将应用程序的状态视为数据库。
I'm working on the state shape for a list of filters, some of which will be open (they're displayed in a popup), or have selected options. When I read "Think of the app's state as a database," I thought about thinking of them as a JSON response as it would be returned from an API (itself backed by a database).
我正在处理过滤器列表的状态形状,其中一些将打开(它们显示在弹出窗口中),或者选择了选项。当我读到“将应用程序的状态视为数据库”时,我想到将它们视为 JSON 响应,因为它将从 API(本身由数据库支持)返回。
So I was thinking of it as
所以我认为它是
[{
id: '1',
name: 'View',
open: false,
options: ['10', '11', '12', '13'],
selectedOption: ['10'],
parent: null,
},
{
id: '10',
name: 'Time & Fees',
open: false,
options: ['20', '21', '22', '23', '24'],
selectedOption: null,
parent: '1',
}]
However, the docs suggest a format more like
但是,文档建议的格式更像是
{
1: {
name: 'View',
open: false,
options: ['10', '11', '12', '13'],
selectedOption: ['10'],
parent: null,
},
10: {
name: 'Time & Fees',
open: false,
options: ['20', '21', '22', '23', '24'],
selectedOption: null,
parent: '1',
}
}
In theory, it shouldn't matter as long as the data is serializable (under the heading "State").
从理论上讲,只要数据是可序列化的(在标题“状态”下)就没有关系。
So I went with the array-of-objects approach happily, until I was writing my reducer.
所以我很高兴地使用了对象数组方法,直到我编写了我的减速器。
With the object-keyed-by-id approach (and liberal use of the spread syntax), the OPEN_FILTER
part of the reducer becomes
使用 object-keyed-by-id 方法(以及自由使用扩展语法),OPEN_FILTER
reducer的部分变为
switch (action.type) {
case OPEN_FILTER: {
return { ...state, { ...state[action.id], open: true } }
}
Whereas with the array-of-objects approach, it's the more verbose (and helper function reliant)
而使用对象数组方法,它更冗长(并且依赖于辅助函数)
switch (action.type) {
case OPEN_FILTER: {
// relies on getFilterById helper function
const filter = getFilterById(state, action.id);
const index = state.indexOf(filter);
return state
.slice(0, index)
.concat([{ ...filter, open: true }])
.concat(state.slice(index + 1));
}
...
So my questions are threefold:
所以我的问题有三个:
1) Is the simplicity of the reducer the motivation for going with the object-keyed-by-id approach? Are there other advantages to that state shape?
1)reducer 的简单性是采用 object-keyed-by-id 方法的动机吗?这种状态形状还有其他优点吗?
and
和
2) It seems like the object-keyed-by-id approach makes it harder to deal with standard JSON in/out for an API. (That's why I went with the array of objects in the first place.) So if you go with that approach, do you just use a function to transform it back and forth between JSON format and state shape format? That seems clunky. (Though if you advocate that approach, is part of your reasoning that that's less clunky than the array-of-objects reducer above?)
2) 对象键控 id 方法似乎使处理 API 的标准 JSON 输入/输出变得更加困难。(这就是我首先使用对象数组的原因。)所以如果你采用这种方法,你是否只是使用一个函数在 JSON 格式和状态形状格式之间来回转换它?那看起来很笨重。(虽然如果你提倡这种方法,你的理由是它比上面的对象数组减速器更不笨重吗?)
and
和
3) I know Dan Abramov designed redux to theoretically be state-data-structure agnostic (as suggested by "By convention, the top-level state is an object or some other key-value collection like a Map, but technically it can be any type,"emphasis mine). But given the above, is it just "recommended" to keep it an object keyed by ID, or are there other unforeseen pain points I'm going to run into by using an array of objects that make it such that I should just abort that plan and try to stick with an object keyed by ID?
3) 我知道 Dan Abramov 将 redux 设计为理论上是状态数据结构不可知的(正如“按照惯例,顶级状态是一个对象或其他一些像 Map 这样的键值集合,但从技术上讲,它可以是任何类型,”强调我的)。但是鉴于上述情况,是否只是“建议”将其保留为由 ID 键控的对象,还是存在其他无法预见的痛点,我将通过使用一组对象使其遇到其他无法预料的痛点,从而使我应该中止该对象计划并尝试坚持使用由 ID 键控的对象?
采纳答案by DDS
Q1: The simplicity of the reducer is a result of not having to search through the array to find the right entry. Not having to search through the array is the advantage. Selectors and other data accessors may and often do access these items by id
. Having to search through the array for each access becomes a performance issue. When your arrays get larger, the performance issue worsens steeply. Also, as your app becomes more complex, showing and filtering data in more places, the issue worsens as well. The combination can be detrimental. By accessing the items by id
, the access time changes from O(n)
to O(1)
, which for large n
(here array items) makes a huge difference.
Q1:reducer 的简单性是因为不必搜索数组来找到正确的条目。不必搜索数组是优点。选择器和其他数据访问器可能并且经常通过 访问这些项目id
。每次访问都必须在数组中搜索成为一个性能问题。当您的阵列变大时,性能问题会急剧恶化。此外,随着您的应用程序变得越来越复杂,在更多地方显示和过滤数据,问题也会变得更糟。这种组合可能是有害的。通过通过 访问项目id
,访问时间从O(n)
变为O(1)
,这对于大型n
(此处为数组项目)产生巨大差异。
Q2: You can use normalizr
to help you with the conversion from API to store. As of normalizr V3.1.0 you can use denormalize to go the other way. That said, Apps are often more consumers than producers of data and as such the conversion to store is usually done more frequently.
Q2:您可以使用normalizr
来帮助您从 API 到商店的转换。从 normalizr V3.1.0 开始,您可以使用 denormalize 来反其道而行之。也就是说,应用程序通常是更多的消费者而不是数据的生产者,因此转换到商店的频率通常更高。
Q3: The issues you'll run into by using an array are not so much issues with the storage convention and/or incompatibilities, but more performance issues.
Q3:您在使用阵列时会遇到的问题不是存储约定和/或不兼容性问题,而是更多的性能问题。
回答by Marco Scabbiolo
Think of the app's state as a database.
将应用程序的状态视为数据库。
That's the key idea.
这就是关键思想。
1) Having objects with unique IDs allows you to always use that id when referencing the object, so you have to pass the minimum amount of data between actions and reducers. It is more efficient than using array.find(...). If you use the array approach you have to pass the entire object and that can get messy very soon, you might end up recreating the object on different reducers, actions, or even in the container (you dont want that). Views will always be able to get the full object even if their associated reducer only contains the ID, because when mapping the state you'll get the collection somewhere (the view gets the whole state to map it to the properties). Because of all of what i've said, actions end up having the minimal amount of parameters, and reducers the minimal amount of information, give it a try, try both methods and you'll see the architecture ends up more scalable and clean using IDs if collections do have ID.
1) 拥有具有唯一 ID 的对象允许您在引用对象时始终使用该 ID,因此您必须在操作和减速器之间传递最少量的数据。它比使用 array.find(...) 更有效。如果您使用数组方法,您必须传递整个对象并且很快就会变得混乱,您最终可能会在不同的减速器、动作甚至容器中重新创建对象(您不希望那样)。即使视图关联的reducer 只包含ID,视图也始终能够获取完整对象,因为在映射状态时,您将在某处获取集合(视图获取整个状态以将其映射到属性)。由于我所说的所有内容,动作最终具有最少数量的参数,而减少器具有最少数量的信息,试一试,
2) The connection to the API should not affect the architecture of your storage and reducers, that's why you have actions, to keep the separation of concerns. Just put your conversion logic in and out of the API in a reusable module, import that module in the actions that use the API, and that should be it.
2) 与 API 的连接不应影响您的存储和减速器的架构,这就是为什么您要采取行动来保持关注点分离。只需将您的转换逻辑放入和取出 API 中的可重用模块,在使用 API 的操作中导入该模块,就可以了。
3) I used arrays for structures with IDs, and these are the unforeseen consequences i've suffered:
3) 我将数组用于带有 ID 的结构,这些是我遭受的无法预料的后果:
- Recreating objects constantly throughout the code
- Passing unnecessary information to reducers and actions
- As consequence of that, bad, not clean and not scalable code.
- 在整个代码中不断地重新创建对象
- 将不必要的信息传递给减速器和动作
- 因此,糟糕的、不干净的和不可扩展的代码。
I ended up changing my data structure and rewriting a lot of code. You have been warned, please don't get yourself in trouble.
我最终改变了我的数据结构并重写了很多代码。你已经被警告过,请不要给自己惹麻烦。
Also:
还:
4) Most collections with IDs are meant to use the ID as a reference to the whole object, you should take advantage of that. The API calls will get the ID and thenthe rest of the parameters, so will your actions and reducers.
4) 大多数带有 ID 的集合旨在使用 ID 作为对整个对象的引用,您应该利用这一点。API 调用将获得 ID ,然后是其余参数,您的操作和减速器也是如此。
回答by tobiasandersen
1) Is the simplicity of the reducer the motivation for going with the object-keyed-by-id approach? Are there other advantages to that state shape?
1)reducer 的简单性是采用 object-keyed-by-id 方法的动机吗?这种状态形状还有其他优点吗?
The main reason you want to keep keep entities in objects stored with IDs as keys (also called normalized), is that it's really cumbersome to work with deeply nested objects(which is what you typically get from REST APIs in a more complex app) — both for your components and your reducers.
您希望将实体保留在以 ID 作为键(也称为normalized)存储的对象中的主要原因是,使用深度嵌套的对象确实很麻烦(这是您通常从更复杂的应用程序中的 REST API 获得的内容)—适用于您的组件和减速机。
It's a bit hard to illustrate the benefits of a normalized state with your current example (as you don't have a deeply nested structure). But let's say that the options (in your example) also had a title, and were created by users in your system. That would make the response look something like this instead:
用您当前的示例来说明规范化状态的好处有点困难(因为您没有深度嵌套的结构)。但是假设选项(在您的示例中)也有一个标题,并且是由您系统中的用户创建的。这将使响应看起来像这样:
[{
id: 1,
name: 'View',
open: false,
options: [
{
id: 10,
title: 'Option 10',
created_by: {
id: 1,
username: 'thierry'
}
},
{
id: 11,
title: 'Option 11',
created_by: {
id: 2,
username: 'dennis'
}
},
...
],
selectedOption: ['10'],
parent: null,
},
...
]
Now let's say you wanted to create a component that shows a list of all users that have created options. To do that, you'd first have to request all the items, then iterate over each of their options, and lastly get the created_by.username.
现在假设您要创建一个组件,该组件显示已创建选项的所有用户的列表。为此,您首先必须请求所有项目,然后迭代它们的每个选项,最后获得 created_by.username。
A better solution would be to normalize the response into:
更好的解决方案是将响应标准化为:
results: [1],
entities: {
filterItems: {
1: {
id: 1,
name: 'View',
open: false,
options: [10, 11],
selectedOption: [10],
parent: null
}
},
options: {
10: {
id: 10,
title: 'Option 10',
created_by: 1
},
11: {
id: 11,
title: 'Option 11',
created_by: 2
}
},
optionCreators: {
1: {
id: 1,
username: 'thierry',
},
2: {
id: 2,
username: 'dennis'
}
}
}
With this structure, it's much easier, and more efficient, to list all users that have created options (we have them isolated in entities.optionCreators, so we just have to loop through that list).
使用这种结构,列出所有创建选项的用户会更容易、更有效(我们将它们隔离在 entity.optionCreators 中,所以我们只需要遍历该列表)。
It's also quite simple to show e.g. the usernames of those that have created options for the filter item with ID 1:
显示例如为 ID 为 1 的过滤器项目创建选项的用户名也很简单:
entities
.filterItems[1].options
.map(id => entities.options[id])
.map(option => entities.optionCreators[option.created_by].username)
2) It seems like the object-keyed-by-id approach makes it harder to deal with standard JSON in/out for an API. (That's why I went with the array of objects in the first place.) So if you go with that approach, do you just use a function to transform it back and forth between JSON format and state shape format? That seems clunky. (Though if you advocate that approach, is part of your reasoning that that's less clunky than the array-of-objects reducer above?)
2) 对象键控 id 方法似乎使处理 API 的标准 JSON 输入/输出变得更加困难。(这就是我首先使用对象数组的原因。)所以如果你采用这种方法,你是否只是使用一个函数在 JSON 格式和状态形状格式之间来回转换它?那看起来很笨重。(虽然如果你提倡这种方法,你的理由是它比上面的对象数组减速器更不笨重吗?)
A JSON-response can be normalized using e.g. normalizr.
可以使用例如normalizr对 JSON 响应进行规范化。
3) I know Dan Abramov designed redux to theoretically be state-data-structure agnostic (as suggested by "By convention, the top-level state is an object or some other key-value collection like a Map, but technically it can be any type," emphasis mine). But given the above, is it just "recommended" to keep it an object keyed by ID, or are there other unforeseen pain points I'm going to run into by using an array of objects that make it such that I should just abort that plan and try to stick with an object keyed by ID?
3)我知道 Dan Abramov 设计的 redux 理论上是状态数据结构不可知的(正如“按照惯例,顶级状态是一个对象或其他一些键值集合,如 Map,但从技术上讲,它可以是任何类型,”强调我的)。但是鉴于上述情况,是否只是“建议”将其保留为由 ID 键控的对象,还是存在其他无法预见的痛点,我将通过使用一组对象使其遇到其他无法预料的痛点,从而使我应该中止该对象计划并尝试坚持使用由 ID 键控的对象?
It's probably a recommendation for more complex apps with lots of deeply nested API responses. In your particular example though, it doesn't really matter that much.
对于具有许多深度嵌套 API 响应的更复杂的应用程序,这可能是一个建议。不过,在您的特定示例中,它并没有那么重要。