为什么JavaScript中的不变性如此重要(或需要)?
我目前正在研究React JS和React Native框架。 当我读到Facebook的Flux实现和Redux实现时,我遇到了Immutability或Immutable-JS库 。
问题是,为什么不变性如此重要? 什么是错误的变异对象? 它不是简单吗?
举一个例子,让我们考虑一个简单的新闻阅读器应用程序。 开幕式是新闻头条列表视图。
如果我最初设置一个带有值的对象数组 。 我无法操纵它。 这就是不变性原则所说的,对吗?(如果我错了,请纠正我)。 但是,如果我有一个新的新闻对象,必须更新? 通常情况下,我可以刚刚添加对象到数组中。 我在这种情况下如何实现? 删除商店并重新创build它? 是不是将一个对象添加到数组中一个更便宜的操作?
PS:如果这个例子不是解释不变性的正确方法,请让我知道什么是正确的实际例子。
我正在努力学习这里的东西。 请赐教:)
我最近一直在研究同一个话题。 我会尽我所能来回答你的问题,并尝试分享我迄今为止学到的东西。
问题是,为什么不变性如此重要? 什么是错误的变异对象? 它不是简单吗?
基本上归结为不变性增加透明度,性能(间接),并允许突变跟踪。
可预测性
突变隐藏的变化,这会产生(意外的)副作用,这可能会导致讨厌的错误。 当你执行不变性时,你可以保持你的应用程序架构和心智模型简单,这使得更容易推理你的应用程序。
性能
即使将值添加到不可变对象中,也意味着需要在需要复制现有值的情况下创build新实例,并且需要将新值添加到耗费内存的新对象中。 不可变对象可以利用结构共享来减less内存开销。
所有的更新都会返回新的值,但是内部的结构是共享的,可以大大减less内存使用量(和GC抖动)。 这意味着如果你追加到1000个元素的向量,它实际上并没有创build一个新的向量1001-元素长。 内部很可能只分配了几个小对象。
你可以在这里阅读更多。
突变跟踪
除了减less内存使用量之外,不变性还允许您通过引用和值相等来优化应用程序。 这使得很容易看到是否有任何改变。 例如一个反应组件的状态改变。 您可以使用shouldComponentUpdate
通过比较状态对象来检查状态是否相同,并防止不必要的呈现。 你可以在这里阅读更多。
其他资源:
- 不变性之道
- 不可变数据结构和JavaScript
- JavaScript中的不变性
如果我最初设置一个带有值的对象数组。 我无法操纵它。 这就是不变性原则所说的,对吗?(如果我错了,请纠正我)。 但是,如果我有一个新的新闻对象,必须更新? 通常情况下,我可以刚刚添加对象到数组中。 我在这种情况下如何实现? 删除商店并重新创build它? 是不是将一个对象添加到数组中一个更便宜的操作?
是的,这是正确的。 如果你对如何在你的应用程序中实现这个问题感到困惑,我会build议你看看REDX如何做到这一点,以熟悉核心概念,这对我有很大的帮助。
我喜欢用Redux作为例子,因为它包含了不变性。 它有一个单一的不可变状态树(被称为store
),其中所有的状态变化都是通过调度由一个接受前一个状态的reducer所执行的动作(一次一个)来显式的,并返回你的下一个状态应用。 你可以在这里阅读更多关于它的核心原则。
在egghead.io上有一个很好的redux课程,redux的作者Dan Abramov解释这些原则如下(为了更好地适应场景,我修改了一下代码):
import React from 'react'; import ReactDOM from 'react-dom'; // Reducer. const news = (state=[], action) => { switch(action.type) { case 'ADD_NEWS_ITEM': { return [ ...state, action.newsItem ]; } default: { return state; } } }; // Store. const createStore = (reducer) => { let state; let listeners = []; const subscribe = (listener) => { listeners.push(listener); return () => { listeners = listeners.filter(cb => cb !== listener); }; }; const getState = () => state; const dispatch = (action) => { state = reducer(state, action); listeners.forEach( cb => cb() ); }; dispatch({}); return { subscribe, getState, dispatch }; }; // Initialize store with reducer. const store = createStore(news); // Component. const News = React.createClass({ onAddNewsItem() { const { newsTitle } = this.refs; store.dispatch({ type: 'ADD_NEWS_ITEM', newsItem: { title: newsTitle.value } }); }, render() { const { news } = this.props; return ( <div> <input ref="newsTitle" /> <button onClick={ this.onAddNewsItem }>add</button> <ul> { news.map( ({ title }) => <li>{ title }</li>) } </ul> </div> ); } }); // Handler that will execute when the store dispatches. const render = () => { ReactDOM.render( <News news={ store.getState() } />, document.getElementById('news') ); }; // Entry point. store.subscribe(render); render();
此外,这些video更详细地展示了如何实现以下方面的不变性:
- 数组
- 对象
问题是,为什么不变性如此重要? 什么是错误的变异对象? 它不是简单吗?
事实上,情况正好相反:可变性使事情变得更加复杂,至less从长远来看。 是的,这使得您的初始编码更容易,因为您可以随意更改任何内容,但是当程序变得更大时,就会成为一个问题 – 如果值发生了变化,变更了什么?
当你把所有东西都变成不变的时候,这意味着数据不能再被意外的改变了。 您肯定知道,如果您将值传递给函数,则不能在该函数中进行更改。
简单地说:如果你使用不可变的值,就可以很容易地推断出你的代码:每个人都得到一个唯一的数据拷贝,所以它不能与它一起解决并破坏你的代码的其他部分。 想象一下,在multithreading环境下工作多么容易!
注1:取决于你在做什么,有一个潜在的性能成本,但像Immutable.js这样的东西尽可能地优化。
注2:在不太可能发生的情况下,您不能确定,Immutable.js和ES6 const
意味着完全不同的东西。
通常情况下,我可以刚刚添加对象到数组中。 我在这种情况下如何实现? 删除商店并重新创build它? 是不是将一个对象添加到数组中一个更便宜的操作? PS:如果这个例子不是解释不变性的正确方法,请让我知道什么是正确的实际例子。
是的,你的新闻例子是非常好的,你的推理是完全正确的:你不能只修改你现有的列表,所以你需要创build一个新的:
var originalItems = Immutable.List.of(1, 2, 3); var newItems = originalItems.push(4, 5, 6);
尽pipe其他的答案没有问题,但是为了解决你对于实际用例的问题(来自其他答案的评论),让我们暂时在你的运行代码之外跳一下,看看你的鼻子下面的无处不在的答案: git 。 如果每次推送提交时都会覆盖存储库中的数据,会发生什么情况?
现在我们遇到了不可变集合面临的一个问题:内存膨胀。 Git足够聪明,每当你做出改变的时候,不要简单地创build新的文件副本, 它只是跟踪差异 。
虽然我对git的内部工作知之甚less,但我只能假设它使用了与您所引用的库类似的策略:结构共享。 底层的库使用尝试或其他树来只跟踪不同的节点。
这种策略对于内存数据结构也是合理的,因为在对数时间内有众所周知的树操作algorithm。
另一个用例:假设你想在你的web应用上使用一个撤消button。 用你的数据的不可变的表示,实现这种做法相对来说是微不足道的。 但是如果你依赖于变异,这意味着你不得不担心caching世界的状态并进行primefaces更新。
总而言之,在运行时间性能和学习曲线上不变的代价是值得的。 但是任何有经验的程序员都会告诉你,debugging时间要比代码写入时间大一个数量级。 对运行时性能的轻微影响可能超过了用户不必忍受的与状态相关的错误。
逆向的不变性观
简而言之:在JavaScript中,不变性更像是一种时尚潮stream。 有一些狭隘的情况会变得有用(主要是React / Redux),尽pipe通常是出于错误的原因。
长答案:阅读下面。
为什么JavaScript中的不变性如此重要(或需要)?
那么,我很高兴你问!
前段时间,一个非常有才华的人Dan Abramov写了一个叫做Redux的JavaScript状态pipe理库,它使用了纯粹的函数和不变性。 他还制作了一些非常酷的video ,使得这个主意很容易理解(和销售)。
时机是完美的。 Angular的新颖性正在消失,JavaScript世界已经准备好去关注那些具有正确程度的最新的东西了。这个图书馆不仅具有创新性,而且完全与另一个硅谷动力公司正在兜售的React分享。
在JavaScript的世界里,时尚是可悲的。 现在阿布拉莫夫被称为半神人,我们所有的凡人都必须服从于不变的道,不pipe有没有道理。
什么是错误的变异对象?
没有!
实际上,程序员一直在为对象进行变异,只要有对象发生变异。 换句话说, 50多年的应用开发。
为什么复杂的东西? 当你有对象cat
,它死了,你真的需要第二cat
跟踪改变? 大多数人只会说cat.isDead = true
并完成它。
不(变异对象)使事情变得简单?
是! 当然是的!
特别是在JavaScript中,它实际上是最有用的,用于呈现其他地方维护的某个状态的视图(如在数据库中)。
如果我有一个新的新闻对象,必须更新? …在这种情况下我如何实现? 删除商店并重新创build它? 是不是将一个对象添加到数组中一个更便宜的操作?
那么,你可以使用传统的方法来更新News
对象,这样你的内存表示就会改变(并且视图会显示给用户,或者所希望的)。
或者…
您可以尝试性感的FP / Immutability方法,并将您的更改添加到News
对象中,以跟踪每个历史变化 ,然后您可以遍历该数组并找出正确的状态表示应该是什么(phew!)。
我正在努力学习这里的东西。 请赐教:)
时尚来来往往。 有10种方法来剥皮猫。
我很抱歉,你必须承受一个不断变化的编程范例的混淆。 但是,嘿,欢迎来到俱乐部!
关于不变性,现在需要记住几个要点,你会以这种只有天真才能产生的狂热的强度来抛弃你。
1)不变性对于避免multithreading环境中的竞争条件是很棒的 。
multithreading环境(如C ++,Java和C#)在多个线程想要更改它们的时候会犯下locking对象的做法。 这对性能不利,但比数据损坏的select更好。 但还不如一切不可改变(主赞美哈斯克尔!)。
可惜! 在JavaScript中,你总是在一个线程上运行 。 即使是networking工作者(每个运行在一个单独的上下文 )。 因此,由于在执行上下文(所有这些可爱的全局variables和闭包)内不能存在与线程有关的竞争条件,所以支持不变性的主要观点就是窗口。
(话虽如此,在web worker中使用纯函数有一个好处,那就是你不会在主线程上摆弄对象)。
2)不变性可以(以某种方式)避免您的应用程序状态的竞争条件。
这是事情的真正关键,大多数(React)开发人员会告诉你,不变性和FP可以以某种方式工作,这使得你的应用程序的状态变得可预测。
当然,这并不意味着您可以避免数据库中的竞争条件 ,为了避免在所有浏览器中协调所有用户 ,您需要一个像WebSockets这样的后端推送技术(更多在这下面),将广播更改运行应用程序的每个人。
这个相当混乱的说法只是意味着分配给状态对象的最新值(由在浏览器中运行的单个用户定义)变得可预测。 根本没有任何进展。 因为你可以使用一个简单的旧的变异variables来跟踪你的状态,并且每当你访问它时,你都会知道你正在处理的是最新的信息,而这个只能用于React / Redux。
为什么? 因为React是特殊的,组件状态是通过一系列你无法控制的事件来pipe理的,并且依赖于你记住不直接改变状态 。 从PR的angular度来看,这已经得到了令人惊讶的处理,因为React的炒作已经把缺点变成了一种性感的方式。 除了时尚之外,我宁愿看到它是不可变的,也就是说,当你select的框架不能直观地处理状态时,它就是一个缩小差距的工具。
3)比赛条件绝对不好。
那么,他们可能是如果你使用React。 但是如果你select了一个不同的框架,它们是很less见的。
此外,你通常有更大的问题来处理…像依赖地狱的问题。 像一个臃肿的代码库。 像你的CSS没有得到加载。 就像一个缓慢的构build过程,或者被困在一个单一的后端,使迭代几乎不可能。 就像没有经验的开发人员不理解正在发生什么事情,弄得一团糟。
你懂。 现实。 但是,嘿,谁在乎呢?
4)不可变性使用引用types来减less跟踪每个状态变化的性能影响。
因为严重的是,如果每当你的状态改变的时候你要复制东西的话,你最好确保你很聪明。
5)不变性允许你UNDO的东西 。
因为呃..这是你的项目经理要求的头号function,对吧?
6)不可变状态与WebSocket结合起来有很多很酷的潜力
最后但并非最不重要的一点是,状态变化的积累与WebSocket结合在一起是一个相当引人注目的情况,它允许将状态作为不可变事件的stream程轻松地消费。
一旦这个概念落入这个概念之下 (状态是事件的stream动 – 而不是一组代表最新观点的logging),这个不变的世界变成了一个神奇的居住地。 一个事件来源的奇迹和可能性, 超越时间本身的土地。 如果做得对,这绝对可以使实时应用程序更容易完成,您只需向所有感兴趣的人广播事件stream程,以便他们可以构build自己的现在表示forms,并将自己的更改写回公共stream程。
但是在某个时刻,你醒悟过来,意识到所有这些奇迹和魔法都不是免费的,更难以编写不可变的代码,而且更容易打破它,再加上如果你不愿意使用不可变的前端,没有后端来支持它。 当你终于说服你的 同事 你应该通过像WebSockets这样的推技术来发布和使用事件的利益相关者 ,你会发现扩大生产的痛苦 。
现在有一些build议,你应该select接受它。
使用FP / Immutability编写JavaScript的select也是使您的应用程序代码库变得更大,更复杂,更难以pipe理的一个select。 我强烈主张将这种方法限制在Redux减速器中,除非你知道你在做什么。
现在,如果你有幸能够在你的工作中做出select,那么试着用你的智慧(或不要),付钱的人做正确的事情。 你可以根据你的经验,你的内心或周围发生的事情(不可否认,如果每个人都使用React / Redux,那么有一个有效的论点,会更容易find一个资源继续你的工作)您可以尝试恢复驱动开发或炒作驱动开发方法。 他们可能更适合你。
总而言之,不可变态的东西就是让你与同龄人一起stream行起来,至less在下一次狂潮来临之前,你会很乐意继续前进。
我现在已经添加了这篇文章在我的博客=> 在JavaScript中的不可变性:逆向视图 。 如果你有强烈的感觉,你也可以自由地在那里回复;)。
为什么在JavaScript中不变性如此重要(或需要)?
不变性可以在不同的上下文中被跟踪,但最重要的是跟踪应用程序状态和应用程序UI。
我将把JavaScript Redux模式视为非常时髦和现代的方法,因为您提到了这一点。
对于UI,我们需要使其可预测 。 如果UI = f(application state)
,它将是可预测的。
应用程序(在JavaScript中)通过使用reducer函数实现的操作来改变状态。
reducer函数只是简单的采取行动和旧的状态,并返回新的状态,保持旧的状态不变。
new state = r(current state, action)
好处是:由于所有的状态对象都被保存了,所以你可以时间旅行状态,并且你可以在任何状态下渲染应用程序,因为UI = f(state)
所以你可以轻松地撤销/重做。
创build所有这些状态都可以提高内存的效率,与Git的类比非常好,我们在Linux操作系统中也有类似的符号链接(基于inode)。
Javascript中不可变性的另一个好处是它减less了时间耦合,这对于devise来说通常有很大的好处。 用两种方法考虑一个对象的接口:
class Foo { baz() { // .... } bar() { // .... } } const f = new Foo();
可能是这样的情况,需要调用baz()
来使对象处于有效状态,以便调用bar()
以正确工作。 但你怎么知道这个?
f.baz(); f.bar(); // this is ok f.bar(); f.baz(); // this blows up
为了解决这个问题,你需要仔细研究这个内部类,因为从公共接口的angular度来看并不是很明显。 这个问题可能在大量可变状态和类的代码库中爆炸。
如果Foo
是不可变的,那么这是一个非问题。
我认为亲不可变对象的主要原因是保持对象的状态有效。
假设我们有一个叫做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总是处于有效状态。