深拷贝还是浅拷贝 – 以及为什么ngrx的状态应该是不可变的?

huangapple go评论76阅读模式
英文:

To deep copy or not to deep copy - and why should ngrx's state be immutable, anyway?

问题

我是新手使用ngrx(从未使用redux),正在尝试理解一切 - 尤其是是否需要深层复制状态。以下是我到目前为止学到的和仍然令我困惑的内容(在下面用粗体标出)。

ngrx文档状态

每个reducer函数都会获取最新的分发的Action,当前状态,并确定是否返回新修改的状态或原始状态。

他们还指出状态转换需要以不可变方式进行 - 如果您更改状态,它需要在副本上进行:

每个操作都以不可变方式处理状态转换。这意味着状态转换不会修改原始状态,而是使用扩展运算符返回一个新的状态对象。

然而,除了促进引用完整性之外,他们没有解释为什么这是必要的(我不太确定如何确保您的引用指向数据 - 我只知道这个术语是从关系数据库上下文中来的):

扩展语法将当前状态的属性复制到对象中,创建一个新的引用。这确保每次更改都会产生一个新的状态,保持更改的纯度。这还促进引用完整性,确保在状态更改发生时旧引用被丢弃。

然而,这并没有真正解释为什么这是必要的。在StackOverflow的其他地方,我找到了一条评论,建议它允许将Angular的变更检测策略更改为OnPush

(我也对如果一个操作可以触发多个reducer,那么生成的状态副本如何被合并感到有些困惑,但显然是由每个reducer专门负责状态的不同部分以及redux意识到这一点来解释的。)

然而,重要的是,状态的副本 - 浅复制或深复制 - 创建一个新的引用,这意味着ngrx正在向其订阅者推送更改:

如果返回的对象引用已更改,它将触发特定状态片段的任何相关RxJS状态订阅。可以使用一些良好的ngrx选择器来最小化触发的订阅。

一般来说,Redux FAQ列出了为什么不可变性是一件好事以及为什么redux需要它的原因:
提高性能,更简单的调试,更容易的推理,更安全的数据处理,浅相等性检查。

他们还说它

可以简单且廉价地实现复杂的变更检测技术,确保只有在绝对必要时才发生更新DOM的计算密集型过程。

正如刚刚指出的(作为不可变性的要求之一)redux进行浅相等性检查

然而,ngrx的文档建议进行深克隆(而且,如果复制引用旧对象,状态就不是真正的不可变的):

注意:扩展运算符只进行浅层复制,不处理深层嵌套的对象。您需要复制对象中的每个级别以确保不可变性。有一些处理深层复制的库,包括lodash和immer。

然而,深层复制可能会有“不好的”副作用(当在Angular组件中使用克隆的项目时):

这个问题甚至扩展到了Angular的ngFor变更检测的方式
工作方式(使用trackBy函数会使情况更加复杂!):
当我克隆Thing[]中的每个项目并让我的reducer返回一个新的
克隆Thing列表,Angular会认为这是一个全新的列表(事实上也是如此),并对列表中的所有项目运行变更检测:
它们也将是全新的,因此,旧列表项目被删除
并将新的项目添加到DOM中。

假设对于ngFor列表中的每个Thing都有一个ThingComponent。在
该组件中,ngOnChanges将被触发。但这是个问题:the
传递给ngOnChangesSimpleChanges永远不会包含previousValues
因为整个列表都被替换了,所以没有以前的值:
从Angular的角度来看,一切都是全新的。

作者还指出了一个解决方案(trackBy),但我现在在思考:
在ngrx中使用深复制真的是一个好主意吗(如果使库工作所需的只是根/状态对象的新引用,那么您是否真的需要深层复制)?最后一句话听起来有点像只需更换根对象,即状态,以获取然后触发订阅的新引用,而将嵌套对象 - 特别是列表 - 保持不变。

英文:

I'm new to ngrx (and never used redux) and am trying to make sense of it all - especially whether you need deep copies of the state. Here's what I've learned so far and what still confuses me (further down in bold).

The ngrx docs state that
> Each reducer function takes the latest Action dispatched, the current state, and determines whether to return a newly modified state or the original state.

They also point out that state transitions need to happen immutably - if you change state, it needs to be on a copy:
> Each action handles the state transition immutably. This means that the state transitions are not modifying the original state, but are returning a new state object using the spread operator.

They don't say why that is necessary, though, beyond promoting referential integrity (and I'm not quite sure how making sure your references are pointing to data - I only know the term from relational database contexts - plays into it all):
> The spread syntax copies the properties from the current state into the object, creating a new reference. This ensures that a new state is produced with each change, preserving the purity of the change. This also promotes referential integrity, guaranteeing that the old reference was discarded when a state change occurred.

That (referential integrity and the section above) doesn't really explain why that is necessary, though.
Elsewhere on SO, I found a comment suggesting it allows to change Angular's change detection strategy to OnPush.

(I was also somewhat baffled by how, if an action can trigger several reducers, the resulting state copies can be consolidated, but that's apparently explained by each reducer exclusively looking after a separate slice of the state and redux being aware of that.)

The important thing seems to be, though, that a copy - shallow or deep - of the state creates a new reference, and that means ngrx is pushing a change to its subscribers:
> If the returned object reference has changed, it will trigger any related RxJS state subscriptions for the particular piece of state in question. Which subscriptions are triggered can be minimised using some good ngrx selectors.

Generally speaking, the Redux FAQ have a list of why immutability is a good thing and why redux requires it:
increased performance, simpler debugging, easier reasoning, safer data handling, shallow equality checking

They also say that it
> enables sophisticated change detection techniques to be implemented simply and cheaply, ensuring the computationally expensive process of updating the DOM occurs only when it absolutely has to

As just pointed out (as one of the requirements for immutability) redux does shallow equality checking.

Nrgx's docs, however, recommend deep cloning (plus, the state isn't truly immutable if the copy references old objects, I suppose):
> Note: The spread operator only does shallow copying and does not handle deeply nested objects. You need to copy each level in the object to ensure immutability. There are libraries that handle deep copying including lodash and immer.

However, deep copies might have "nasty" side effects (when using the cloned items in an Angular component, say):

> This question even extends to the way Angular's ngFor change detection
> works (and using a trackBy function complicates that even further!):
> when I clone every item in Thing[] and have my reducer return a new
> list of cloned Things, Angular will think it's a brand new list (which
> it technically is) and run change-detection for all items in the list:
> They will also be brand new, and as such, old list items get removed
> and new ones get added to the DOM.
>
> Suppose you have a ThingComponent for each Thing in the ngFor list. In
> that component, ngOnChanges will fire. But here's the thing: the
> SimpleChanges passed to ngOnChanges will never contain previousValues,
> because the whole list got replaced, and so there is previous value:
> everything is brand new, from Angulars perspective.

The author also points to a solution (trackBy), but I'm now wondering:
is using deep copy really a good idea with ngrx (and do you really need deep copies if all that is required to make the library work is a new object reference for the root/state object)? The last quote sounds a bit like it would be a better idea to only swap the root object, the state, out, to get a new reference that then triggers subscriptions, but leave the nested objects - lists, especially - alone.

答案1

得分: 2

  1. 深拷贝

我们的目标是尽量保持应用程序的速度,这意味着在不需要或冗余计算时减少计算量。

Angular 有两种变更检测策略:onPush 和 Default。第一种检查传递给输入的变量的指针,第二种进行深层检查,对于大对象会比较耗费资源。但它们有一个共同点,即只在数据发生变化时重新渲染。

深拷贝不好,因为它导致相同的数据在新的对象指针下呈现,这会导致渲染循环,因为数据相同,重新渲染的结果也会相同,除非是与时间有关的应用程序。


  1. 不可变状态

数据流的最佳方式是从顶部到底部,底部可以通知顶部更改数据。

在这种情况下,我们始终可以通过检查父级来查找数据是如何传递到这里的,以及他们对数据做了什么,我们有一个单一的点,数据正在发生变化,我们可以在那里找到是谁引起了变化。

如果我们在需要更改数据的地方改变数据而不通知顶部,随着时间的推移,我们将很难找到是谁做出了这些更改,因为这些地方会分散在代码的各个地方,我们需要检查所有这些地方才能找到问题的根本原因。

英文:
  1. Deep copy

Our goal to keep our apps as fast as possible what means to reduce calculation when it's not needed or redundant.

Angular has 2 change detection strategies onPush and Default, the first one checks pointers of variables passed into inputs, the second one does deep check and quite heavy on heavy objects. But they have one thing - they rerender only when data has been changed.

Deep copy is bad because it causes the same data to be presented under new object pointers, this causes render cycles and because data is the same rerendered result will be the same too, unless it's time dependent app.


  1. Immutable state

The best way for the data flow is from top to bottom, and bottom can notify top to change the data.

In such circumstances we can always find how data came here via checking parents and what they do with the data, and we have a single point where data is changing and we can find there who causes the change.

If we mutate data where we need the change instead of notifying the top, with time we can't easily find who does it anymore because these places will be everywhere in the code and we need to check all of them to find the issue.

huangapple
  • 本文由 发表于 2020年1月3日 19:34:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/59577923.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定