我目前正在开发React JS和React Native框架。在半路上,当我读到Facebook的Flux和Redux实现时,我遇到了Immutability或Immutable-JS库。
问题是,为什么不变性如此重要?改变对象有什么错?这不是让事情变得简单了吗?
举个例子,让我们考虑一个简单的新闻阅读器应用程序,它的打开屏幕是一个新闻标题的列表视图。
如果我设置一个带有值的对象数组,一开始我不能操作它。这就是不可变原理,对吧?(如果我说错了请指正。)
但是,如果我有一个新的News对象需要更新怎么办?通常情况下,我可以将对象添加到数组中。
在这种情况下我该如何实现呢?删除存储并重新创建它?
向数组中添加对象难道不是一种成本较低的操作吗?
举个例子:
const userMessage = {
user: "userId",
topic: "topicId"
content: {}
}
validateMessage(userMessage)
saveMessage(userMessage)
sendMessageViaEmail(userMessage)
**sendMessageViaMobilePush(userMessage)**
console.log(userMessage) // => ?
现在回答一些问题:
在可变代码中的sendMessageViaMobilePush(userMessage))下的userMessage是什么?
{
id: "xxx-xxx-xxx-xxx", //set by ..(Answer for question 3)
user:"John Tribe", //set by sendMessageViaEmail
topic: "Email title", //set by sendMessageViaEmail
status: FINAL, //set by saveMessage or could be set by sendMessageViaEmail
from: "..", //set by sendMessageViaEmail
to:"...", //set by sendMessageViaEmail
valid:true, //set by validateMessage
state: SENT //set by sendMessageViaEmail
}
Surprised?? Me too :d. But this is normal with mutability in javascript.
(in Java too but a bit in different way. When You expect null but get some object).
What is under userMessage on same line in immutable code?
const userMessage = {
user: "userId",
topic: "topicId",
content: {}
}
Easy right ?
Can You guess by which method "id" is updated in mutable code in Snippet 1 ??
By sendMessageViaEmail.
Why?
Why not?
Well it was at first updated by saveMessage,
but then overridden by sendMessageViaEmail.
In mutable code people didn't received push messages (sendMessageViaMobilePush). Can You guess why ??
because I am amazing developer :D and I put safety check in method sendMessageViaMobilePush(userMessage)
function sendMessageViaMobilePush(userMessage) {
if (userMessage.state != SENT) { //was set to SENT by sendMessageViaEmail
send(userMessage)
}
}
Even if You saw this method before,
was this possible for You to predict this behavior in mutable code ?
For me it wasn't.
希望这能帮助你理解在javascript中使用可变对象的主要问题。
请注意,当复杂性上升时,很难检查设置的内容和位置,特别是当您与他人一起工作时。
我认为支持不可变对象的主要原因是保持对象的状态有效。
假设我们有一个叫arr的物体。当所有项都是相同的字母时,该节点有效。
// this function will change the letter in all the array
function fillWithZ(arr) {
for (var i = 0; i < arr.length; ++i) {
if (i === 4) // rare condition
return arr; // some error here
arr[i] = "Z";
}
return arr;
}
console.log(fillWithZ(["A","A","A"])) // ok, valid state
console.log(fillWithZ(["A","A","A","A","A","A"])) // bad, invalid state
如果arr成为一个不可变对象,那么我们将确保arr始终处于有效状态。
虽然其他答案都很好,但为了解决您关于实际用例的问题(来自其他答案的评论),让我们暂时离开您的运行代码,看看您眼前无处不在的答案:git。如果每次提交都会覆盖存储库中的数据,会发生什么情况?
现在我们遇到了不可变集合面临的一个问题:内存膨胀。Git非常聪明,它不会在每次更改时简单地复制文件,而是简单地跟踪差异。
虽然我不太了解git的内部工作原理,但我只能假设它使用了与您所参考的库类似的策略:结构化共享。在底层,库使用try或其他树只跟踪不同的节点。
对于内存中的数据结构,这种策略的性能也相当好,因为有一些知名的树操作算法在对数时间内运行。
另一个用例:假设你想在你的webapp上有一个撤销按钮。对于数据的不可变表示,实现这一点相对简单。但是如果您依赖于突变,这意味着您必须担心缓存世界的状态并进行原子更新。
简而言之,运行时性能和学习曲线的不可变性是要付出代价的。但是任何有经验的程序员都会告诉您,调试时间比代码编写时间要长一个数量级。运行时性能受到的轻微影响可能被用户不必忍受的与状态相关的错误所抵消。
我已经为可变(或不可变)状态创建了一个框架不可知的开源(MIT)库,它可以取代所有那些不可变的存储,如库(redux, vuex等…)
对我来说,不可变状态是丑陋的,因为有太多的工作要做(很多简单的读/写操作),代码可读性较差,大数据集的性能不能接受(整个组件重新渲染:/)。
对于深度状态观察者,我只能用点表示法更新一个节点并使用通配符。我还可以创建状态的历史记录(撤销/重做/时间旅行),只保留那些已经改变的具体值{path:value} =较少的内存使用。
使用深度状态观测器,我可以对事情进行微调,并对组件行为进行粒度控制,因此性能可以大幅提高。代码可读性更强,重构也更容易——只需要搜索和替换路径字符串(不需要更改代码/逻辑)。
从前,线程之间的数据同步出现了一个问题。这个问题非常麻烦,有10多个解决方案。有些人试图从根本上解决这个问题。它是函数式编程诞生的地方。这就像马克思主义。我不明白Dan Abramov是怎么把这个想法卖给JS的,因为它是单线程的。他是个天才。
我可以举个小例子。在gcc中有一个属性__attribute__((pure))。编译器试图解决你的函数是否是纯的,如果你不特别声明它。你的函数可以是纯的,即使你的状态是可变的。不可变性只是保证你的函数是纯粹的100多种方法中的一种。实际上95%的函数都是纯函数。
如果你真的没有明确的理由,你就不应该使用任何限制(比如不可变性)。如果你想“撤销”某些状态,你可以创建事务。如果你想简化通信,你可以发送带有不可变数据的事件。这取决于你。
我写这条信息来自后马克思主义共和国。我相信任何思想的激进化都是错误的。