我目前正在研究React JS和React Native框架.当我读到Facebook的Flux和Redux实现时,在中途遇到Immutability或Immutable-JS库.
问题是,为什么不变性如此重要?变异对象有什么问题?它不简单吗?
举一个例子,让我们考虑一个简单的新闻阅读器应用程序,其中开始屏幕是新闻标题的列表视图.
如果我设置说最初具有值的对象数组,我无法操纵它.这就是不变性原则所说的,对吧?(如果我错了,请纠正我.)但是,如果我有一个必须更新的新的新闻对象怎么办?通常情况下,我可以将对象添加到数组中.在这种情况下我该如何实现?删除商店并重新创建它?是不是将一个对象添加到数组中的操作更便宜?
PS:如果这个例子不是解释不变性的正确方法,请让我知道什么是正确的实际例子.
我最近一直在研究同一个话题.我会尽力回答你的问题,并尝试分享我迄今所学到的知识.
问题是,为什么不变性如此重要?变异对象有什么问题?它不简单吗?
基本上,它归结为这样一个事实:不变性增加了可预测性,性能(间接)并允许突变跟踪.
可预测性
变异隐藏变化,产生(意外)副作用,这可能导致令人讨厌的错误.当您实施不变性时,您可以简化应用程序架构和心智模型,这样可以更轻松地推断您的应用程序.
性能
即使向不可变对象添加值意味着需要创建需要复制现有值的新实例,并且需要将新值添加到占用内存的新对象,但是不可变对象可以利用结构共享来减少内存高架.
所有更新都返回新值,但共享内部结构以大幅减少内存使用(以及GC抖动).这意味着如果附加到具有1000个元素的向量,则它实际上不会创建新的向量1001元素长.最有可能的是,内部只分配了几个小对象.
你可以在这里阅读更多相关信息.
突变追踪
除了减少内存使用外,不变性还允许您通过使用引用和值相等来优化应用程序.这使得很容易看出是否有任何变化.例如,反应组分的状态变化.您可以shouldComponentUpdate
通过比较状态对象来检查状态是否相同,并防止不必要的渲染.你可以在这里阅读更多相关信息.
其他资源:
不变的道
不可变数据结构和JavaScript
JavaScript中的不变性
如果我设置一个最初具有值的对象数组.我无法操纵它.这就是不变性原则所说的,对吧?(如果我错了,请纠正我).但是,如果我有一个必须更新的新的新闻对象怎么办?通常情况下,我可以将对象添加到数组中.在这种情况下我该如何实现?删除商店并重新创建它?是不是将一个对象添加到数组中的操作更便宜?
是的,这是正确的.如果你对如何在你的应用程序中实现它感到困惑,我建议你看看redux如何做到这一点以熟悉核心概念,它对我帮助很大.
我喜欢使用Redux作为例子,因为它包含不变性.它有一个单独的不可变状态树(称为store
),其中所有状态更改都是显式的,通过调度操作由reducer处理,该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 (); } }); // Handler that will execute when the store dispatches. const render = () => { ReactDOM.render({ news.map( ({ title }) =>
- { title }
) }, document.getElementById('news') ); }; // Entry point. store.subscribe(render); render();
此外,这些视频还详细说明了如何实现不变性:
数组
对象
简短回答:不可变性更像是时尚潮流,而不是JavaScript中的必需品.当它变得有用时(主要是React/Redux)有一些狭窄的情况,尽管通常是出于错误的原因.
答案很长:见下文.
为什么javascript中的不变性如此重要(或需要)?
好吧,我很高兴你问!
前段时间,一个名叫Dan Abramov的非常有才华的人写了一个名为Redux的javascript状态管理库,它使用纯函数和不变性.他还制作了一些非常酷的视频,让这个想法很容易理解(和销售).
时机非常完美.Angular的新颖性正在逐渐消失,JavaScript世界已经准备好了解具有适当程度酷感的最新产品,而且这个图书馆不仅具有创新性,而且与React完美契合,React正被另一个硅谷强者兜售.
可悲的是,时尚在JavaScript世界中占主导地位.现在阿布拉莫夫被称为半神半人,我们所有人都必须让自己接受不可变性之道 ......不管它是否有意义.
变异对象有什么问题?
没有!
事实上,只要有变异的对象,程序员就一直在变异对象.换句话说,50多年的应用程序开发.
为什么复杂的事情呢?当你有对象cat
而它死了,你真的需要一秒钟cat
跟踪变化吗?大多数人只会说cat.isDead = true
并完成它.
不(变异对象)使事情变得简单吗?
是!..当然有!
特别是在JavaScript中,实际上最有用的是渲染某些在其他地方维护的状态的视图(比如在数据库中).
如果我有一个必须更新的新新闻对象怎么办?......在这种情况下我该如何实现?删除商店并重新创建它?是不是将一个对象添加到数组中的操作更便宜?
好吧,你可以采用传统方法并更新News
对象,因此你对该对象的内存中表示会发生变化(并且视图会显示给用户,或者人们希望如此)......
或者......
您可以尝试性感的FP/Immutability方法,并将对News
对象的更改添加到跟踪每个历史更改的数组中,以便您可以遍历数组并找出正确的状态表示应该是什么(p!).
我想知道这里有什么.请赐教:)
时尚来来往往伙计.皮肤猫的方法有很多种.
对不起,你必须承受一组不断变化的编程范式的困惑.但是,嘿,欢迎来到俱乐部!
关于不变性,现在要记住几个重要的要点,你会发现这些只有天真才能鼓起的狂热强度.
1)不可变性对于避免多线程环境中的竞争条件非常有用.
多线程环境(如C++,Java和C#)在多个线程想要更改它们时锁定对象的做法是犯了错误.这对性能不利,但比数据损坏的替代方案更好.然而,并没有让一切变得一成不变(主赞美哈斯克尔!).
可惜!在JavaScript中,您始终在单个线程上运行.甚至是web worker(每个都在一个单独的上下文中运行).因此,在执行上下文中没有与线程相关的竞争条件(所有那些可爱的全局变量和闭包),支持Immutability的主要观点就会消失.
(话虽如此,在web worker中使用纯函数是有利的,这就是你不会想到在主线程上摆弄对象.)
2)不变性可以(某种程度上)避免你的应用程序状态中的竞争条件.
这是问题的真正关键,大多数(React)开发人员会告诉你,Immutability和FP可以以某种方式运行这种魔法,使你的应用程序的状态变得可预测.
当然,这并不意味着你可以避免在数据库中出现竞争条件,从而无需协调所有浏览器中的所有用户,为此你需要像WebSockets这样的后端推送技术 (以下更多内容)将向运行应用程序的每个人广播更改.
这种相当混乱的声明仅仅意味着分配给状态对象的最新值(由在其浏览器中运行的单个用户定义)变得可预测.这根本没有进展.因为您可以使用一个普通的变异变量来跟踪您的状态,并且您知道在访问它时您正在处理最新信息,这将适用于除React/Redux之外的任何内容.
为什么?因为React是特殊的..并且组件状态是通过一系列您无法控制的事件来管理的,并且依赖于您记住不要直接改变状态.从公关角度来看,这已经得到了令人惊讶的处理,因为React的炒作将缺点变成了性感时尚.除了时尚之外,我宁愿看到它的不变性,也就是当你选择的框架没有直观地处理状态时填补空白的工具.
3)种族条件绝对不好.
好吧,如果您使用React,它们可能就是这样.但如果您选择不同的框架,它们很少见.
此外,你通常有更大的问题要处理......依赖地狱等问题.就像一个臃肿的代码库.就像你的CSS没有加载.就像一个缓慢的构建过程或被困在一个单一的后端,使得迭代几乎不可能.就像没有经验的开发者一样,不了解最新情况并制造混乱的东西.
你懂.现实.但嘿,谁在乎呢?
4)不变性利用参考类型来减少跟踪每个状态变化对性能的影响.
因为严肃的说,如果你要在每次状态发生变化时复制内容,最好确保你很聪明.
5)不变性允许你UNDO的东西.
因为呃..这是你的项目经理要求的头号功能,对吧?
6)不可变状态与WebSockets结合具有很多很酷的潜力
最后但并非最不重要的是,状态增量的积累与WebSockets结合起来是一个非常引人注目的案例,它允许将状态简单地作为不可变事件的流程 ...
一旦一分钱落在这个概念上(状态是事件的流动 - 而不是代表最新观点的原始记录集),不可变的世界就变成了一个神奇的栖息地.一个充满事件的奇迹和超越时间的可能性的土地.并在完成后右这个绝对可以让实时应用程式EASI 呃来完成,你只播事件对大家感兴趣的流动,使他们能够建立自己现在的表现和自己的变化写回社区流动.
但是在某些时候你醒来并意识到所有奇迹和魔法都不是免费的.与你渴望的同事不同,你的利益相关者(是的,付钱给你的人)对时尚以及他们为建立他们可以销售的产品而支付的钱很少关心.最重要的是,它更难以编写不可变代码并且更容易破解它,如果你没有后端来支持它,那么拥有一个不可变的前端也没什么意义.当(以及如果!)你最终说服你的利益相关者你应该通过像WebSockets这样的推送技术发布和使用事件时,你会发现在生产中扩展是多么痛苦.
现在提出一些建议,你应该选择接受它吗?
使用FP/Immutability编写JavaScript的选择也是使您的应用程序代码库更大,更复杂和更难管理的选择.我强烈主张将这种方法限制在你的Redux减速器上......除非你知道你在做什么.换句话说:Just Keep It Simple™.在大多数情况下,你会变得更好.如果你不是,我会专注于通过你的(整个)应用程序获得不可变数据的好处,而不是构建一个纯粹的功能前端并调用完成的东西.
现在,如果你有幸能够在你的工作中做出选择,那么试着用你的智慧(或不用),并由付钱给你的人做正确的事.你可以根据你的经验,你的直觉,或者你周围的事情来确定这一点(诚然,如果每个人都使用React/Redux,那么就会有一个有效的论据,即找到一个资源来继续工作会更容易).或者,您可以尝试Resume Driven Development或Hype Driven Development方法.他们可能更像你的事.
总之,可以说对于不变性的事情是,它会令你的时尚与您同行,至少直到下一个热潮来临时,由此时你会很高兴地前进.
我现在已经在我的博客中添加了这篇文章=> JavaScript中的不变性:反向视图.如果您有强烈的感情,请随时回复;).
问题是,为什么不变性如此重要?变异对象有什么问题?它不简单吗?
实际上,情况恰恰相反:可变性使事情变得更加复杂,至少从长远来看是这样.是的,它使您的初始编码更容易,因为您可以随时随地更改内容,但是当您的程序变大时,它就成了一个问题 - 如果值发生了变化,是什么改变了它?
当你使一切变得一成不变时,就意味着数据不能再被惊讶地改变了.您肯定知道如果将值传递给函数,则无法在该函数中更改它.
简单地说:如果你使用不可变值,它可以很容易地推理你的代码:每个人都获得了一个独特的数据副本,所以它不能用它来破坏你的代码的其他部分.想象一下,这使得在多线程环境中工作变得容易多了!
注1:根据你正在做的事情,不可变性有潜在的性能成本,但像Immutable.js这样的东西尽可能优化.
注2:在不太常见的情况下你不确定,Immutable.js和ES6 const
意味着非常不同的东西.
通常情况下,我可以将对象添加到数组中.在这种情况下我该如何实现?删除商店并重新创建它?是不是将一个对象添加到数组中的操作更便宜?PS:如果这个例子不是解释不变性的正确方法,请让我知道什么是正确的实际例子.
是的,您的新闻示例非常好,您的推理完全正确:您不能只修改现有列表,因此您需要创建一个新列表:
var originalItems = Immutable.List.of(1, 2, 3); var newItems = originalItems.push(4, 5, 6);
虽然其他答案都很好,但是为了解决关于实际用例的问题(来自对其他答案的评论),让我们在你的运行代码之外走一会儿,然后看看你鼻子下无处不在的答案:git.如果每次推送提交都会覆盖存储库中的数据,会发生什么?
现在我们遇到了不可变集合面临的一个问题:内存臃肿.Git非常聪明,每次进行更改时都不会简单地创建新的文件副本,它只是跟踪差异.
虽然我对git的内部工作原理不太了解,但我只能假设它使用与您引用的库类似的策略:结构共享.在引擎盖下,库使用try或其他树只跟踪不同的节点.
该策略对于内存数据结构也是合理的性能,因为存在以对数时间操作的众所周知的树操作算法.
另一个用例:假设您想要在webapp上使用撤消按钮.使用不可变的数据表示,实现这些是相对微不足道的.但是如果你依赖于变异,那就意味着你必须担心缓存世界状态并进行原子更新.
简而言之,为运行时性能和学习曲线的不变性付出代价.但是,任何有经验的程序员都会告诉你,调试时间超过了代码编写时间一个数量级.而且,运行时性能的轻微打击可能会被用户不必忍受的与状态相关的错误所抵消.
问题是,为什么不变性如此重要?变异对象有什么问题?它不简单吗?
从技术角度来看,可变性没有任何问题.它很快,它重用了内存.开发人员从一开始就使用它(我记得它).使用这种使用可带来的可变性和麻烦存在问题.
如果对象没有与任何东西共享,例如存在于函数的范围内并且不暴露给外部,那么很难看到不可变性的好处.真的在这种情况下,不可变是没有意义的.当共享某些东西时,不变性感就开始了.
可变性头痛
可变共享结构很容易造成许多陷阱.访问引用的代码的任何部分的任何更改都会对具有此引用可见性的其他部分产生影响.这种影响将所有部分连接在一起,即使他们不应该知道不同的模块.一个函数中的变异可能会崩溃应用程序的完全不同部分.这样的事情是一个不好的副作用.
接下来经常出现的突变问题就是腐败状态.当突变程序在中间失败时会发生损坏状态,有些字段被修改而有些字段没有被修改.
更重要的是,突变很难跟踪变化.简单的参考检查不会显示差异,知道什么改变了一些深层检查需要完成.另外,为了监视变化,需要引入一些可观察的模式.
最后,突变是信任不足的原因.如果可以突变,你如何确定某些结构具有所需的价值.
const car = { brand: 'Ferrari' }; doSomething(car); console.log(car); // { brand: 'Fiat' }
如上例所示,通过可变结构总是可以通过具有不同的结构来完成.函数doSomething正在改变从外部给出的属性.不信任代码,你真的不知道你拥有什么以及你将拥有什么.所有这些问题都发生是因为:可变结构表示指向内存的指针.
不可变性意味着不对同一对象,结构进行更改,但更改以新的方式表示.这是因为引用不仅代表内存指针的值.每一次改变都会带来新的价值,而不会触及旧的价值.这些明确的规则回馈了信任和代码可预测性.函数是安全的,因为它们不是变异,而是处理具有自己值的自有版本.
使用值而不是内存容器可以确保每个对象代表特定的不可更改的值,并且使用它是安全的.
不可变结构代表价值.
我正在中篇文章中深入探讨这个主题 - https://medium.com/@macsikora/the-state-of-immutability-169d2cd11310
为什么JavaScript中的不变性如此重要(或需要)?
可以在不同的上下文中跟踪不变性,但最重要的是跟踪应用程序状态和应用程序UI.
我会将JavaScript Redux模式视为非常时髦和现代的方法,因为你提到了这一点.
对于UI,我们需要使其可预测.如果可以预测的话UI = f(application state)
.
应用程序(在JavaScript中)通过使用reducer函数实现的操作来改变状态.
reducer函数只需执行操作和旧状态并返回新状态,保持旧状态不变.
new state = r(current state, action)
好处是:由于保存了所有状态对象,因此您可以对状态进行时间旅行,并且可以在任何状态下呈现应用程序 UI = f(state)
发生所有这些状态仍然可以保持内存效率,与Git类比很好,我们在Linux OS中有类似的类比,带有符号链接(基于inode).
Java不可变性的另一个好处是它减少了时间耦合,这通常对设计具有实质性的好处。考虑对象的接口有两种方法:
class Foo { baz() { // .... } bar() { // .... } } const f = new Foo();
在某些情况下,可能需要进行调用baz()
才能使对象处于有效状态,才能使调用bar()
正常工作。但是你怎么知道呢?
f.baz(); f.bar(); // this is ok f.bar(); f.baz(); // this blows up
为了弄清楚这一点,您需要仔细检查类的内部,因为从检查公共接口并不能立即看出来。在具有大量可变状态和类的大型代码库中,此问题可能会爆炸。
如果Foo
是不变的,那么这不再是问题。可以安全地假定我们可以调用baz
或bar
以任何顺序调用,因为类的内部状态无法更改。