我目前正在开发React JS和React Native框架。在半路上,当我读到Facebook的Flux和Redux实现时,我遇到了Immutability或Immutable-JS库。

问题是,为什么不变性如此重要?改变对象有什么错?这不是让事情变得简单了吗?

举个例子,让我们考虑一个简单的新闻阅读器应用程序,它的打开屏幕是一个新闻标题的列表视图。

如果我设置一个带有值的对象数组,一开始我不能操作它。这就是不可变原理,对吧?(如果我说错了请指正。) 但是,如果我有一个新的News对象需要更新怎么办?通常情况下,我可以将对象添加到数组中。 在这种情况下我该如何实现呢?删除存储并重新创建它? 向数组中添加对象难道不是一种成本较低的操作吗?


当前回答

虽然其他答案都很好,但为了解决您关于实际用例的问题(来自其他答案的评论),让我们暂时离开您的运行代码,看看您眼前无处不在的答案:git。如果每次提交都会覆盖存储库中的数据,会发生什么情况?

现在我们遇到了不可变集合面临的一个问题:内存膨胀。Git非常聪明,它不会在每次更改时简单地复制文件,而是简单地跟踪差异。

虽然我不太了解git的内部工作原理,但我只能假设它使用了与您所参考的库类似的策略:结构化共享。在底层,库使用try或其他树只跟踪不同的节点。

对于内存中的数据结构,这种策略的性能也相当好,因为有一些知名的树操作算法在对数时间内运行。

另一个用例:假设你想在你的webapp上有一个撤销按钮。对于数据的不可变表示,实现这一点相对简单。但是如果您依赖于突变,这意味着您必须担心缓存世界的状态并进行原子更新。

简而言之,运行时性能和学习曲线的不可变性是要付出代价的。但是任何有经验的程序员都会告诉您,调试时间比代码编写时间要长一个数量级。运行时性能受到的轻微影响可能被用户不必忍受的与状态相关的错误所抵消。

其他回答

从前,线程之间的数据同步出现了一个问题。这个问题非常麻烦,有10多个解决方案。有些人试图从根本上解决这个问题。它是函数式编程诞生的地方。这就像马克思主义。我不明白Dan Abramov是怎么把这个想法卖给JS的,因为它是单线程的。他是个天才。

我可以举个小例子。在gcc中有一个属性__attribute__((pure))。编译器试图解决你的函数是否是纯的,如果你不特别声明它。你的函数可以是纯的,即使你的状态是可变的。不可变性只是保证你的函数是纯粹的100多种方法中的一种。实际上95%的函数都是纯函数。

如果你真的没有明确的理由,你就不应该使用任何限制(比如不可变性)。如果你想“撤销”某些状态,你可以创建事务。如果你想简化通信,你可以发送带有不可变数据的事件。这取决于你。

我写这条信息来自后马克思主义共和国。我相信任何思想的激进化都是错误的。

问题是,为什么不变性如此重要?改变对象有什么错?这不是让事情变得简单了吗?

关于可变性

从技术角度来看,可变性并没有错。它是快速的,它是重复使用内存。开发人员从一开始就习惯了它(我记得)。可变性的使用存在问题,也会带来一些麻烦。

如果object不与任何东西共享,例如存在于函数的作用域中并且不对外公开,那么就很难看到不变性的好处。在这种情况下,不可变是没有意义的。一成不变的感觉始于某些东西被分享。

可变性头痛

可变的共享结构很容易产生许多陷阱。对访问引用的代码的任何部分的任何更改都会影响到具有此引用可见性的其他部分。这种影响将所有部分连接在一起,即使它们不应该意识到不同的模块。一个函数的突变可能会导致应用程序的完全不同部分崩溃。这样的事情是一个糟糕的副作用。

其次经常与突变的问题是损坏的状态。当突变过程中途失败,一些字段被修改,一些字段没有被修改时,就会发生损坏状态。

更重要的是,通过突变很难追踪变化。简单的参考检查不会显示出差异,要知道发生了什么变化需要做一些深入的检查。此外,为了监测变化,还需要引入一些可观察的模式。

最后,突变是信任缺失的原因。如果某个结构可以变异,你怎么能确定它有你想要的价值。

const car = { brand: 'Ferrari' };
doSomething(car);
console.log(car); // { brand: 'Fiat' }

如上例所示,传递可变结构总是可以通过具有不同的结构来完成。函数doSomething正在改变外部给定的属性。没有对代码的信任,你真的不知道你拥有什么,你将拥有什么。所有这些问题的发生是因为:可变结构表示指向内存的指针。

不可变性与价值有关

不可变性意味着改变不是在相同的对象、结构上完成的,而是在新的对象、结构中表示的。这是因为引用不仅表示内存指针的值。每一次改变都会创造新的价值,而不会改变旧的价值。这样清晰的规则给予了信任和代码的可预测性。函数使用起来是安全的,因为它们处理的不是突变,而是具有自己值的自己的版本。

使用值而不是内存容器可以确定每个对象都表示特定的不可更改的值,并且使用它是安全的。

不可变结构表示值。

我将在一篇中型文章(https://medium.com/@macsikora/the-state-of- immutabil-169d2cd11310)中更深入地探讨这个主题

我最近也在研究同样的话题。我会尽我最大的努力回答你的问题,并尝试分享我到目前为止学到的东西。

问题是,为什么不变性如此重要?有什么问题 变异的对象?这不是让事情变得简单了吗?

基本上可以归结为这样一个事实:不可变性增加了可预测性、性能(间接地),并允许突变跟踪。

可预测性

突变隐藏了更改,而更改会产生(意想不到的)副作用,从而导致严重的bug。当您强制执行不可变性时,您可以保持应用程序架构和心理模型的简单性,这使您更容易对应用程序进行推理。

性能

尽管向不可变对象添加值意味着需要创建一个新实例,其中需要复制现有值,并且需要向新对象添加新值,这会消耗内存,但不可变对象可以利用结构共享来减少内存开销。

所有更新都返回新值,但内部结构共享给 大幅度减少内存使用(和GC抖动)。这意味着如果 你附加到一个有1000个元素的向量,它实际上并不创建 一个新的向量,长度为1001个元素。最有可能的是,内部只有几个 分配小对象。

你可以在这里阅读更多相关内容。

突变跟踪

除了减少内存使用外,不可变性还允许您通过使用引用和值相等来优化应用程序。这使得查看是否有任何更改非常容易。例如,react组件的状态变化。您可以使用shouldComponentUpdate通过比较状态对象来检查状态是否相同,并防止不必要的呈现。 你可以在这里阅读更多相关内容。

额外的资源:

不变之道 不可变数据结构和JavaScript JavaScript中的不可变性

如果我在初始值中设置一个对象数组。我不能 对它进行操作。这就是不可变原理,对吧?(正确的 如果我错了,请告诉我)。但是,如果我有一个新的News对象 被更新吗?通常情况下,我可以将对象添加到 数组中。在这种情况下我该如何实现呢?删除商店并重新创建它? 向数组中添加对象难道不是一种成本较低的操作吗?

是的,这是正确的。如果你对如何在你的应用程序中实现这一点感到困惑,我建议你看看redux是如何做到这一点的,以熟悉核心概念,它对我有很大帮助。

我喜欢使用Redux作为例子,因为它包含了不变性。它有一个单一的不可变的状态树(称为存储),其中所有的状态变化都是通过分派操作来显式的,这些操作由reducer处理,reducer接受前一个状态和所述操作(一次一个)并返回应用程序的下一个状态。你可以在这里阅读更多关于它的核心原则。

在egghead上有一门很好的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();

此外,这些视频进一步详细演示了如何实现不变性:

数组 对象

为什么不可变性在JavaScript中如此重要(或需要)?

不可变性可以在不同的上下文中跟踪,但最重要的是根据应用程序状态和应用程序UI跟踪它。

我认为JavaScript Redux模式是非常流行和现代的方法,因为你提到了。

对于UI,我们需要让它具有可预测性。 如果UI = f(应用程序状态),这将是可预测的。

应用程序(在JavaScript中)通过使用reducer函数实现的操作来改变状态。

reducer函数只是接受动作和旧状态,并返回新状态,保持旧状态不变。

new state  = r(current state, action)

好处是:你可以穿越状态,因为所有的状态对象都被保存了,你可以在任何状态下呈现应用程序,因为UI = f(state)

所以你可以很容易地撤销/重做。


碰巧创建所有这些状态仍然可以是内存高效的,与Git类似是很好的,我们在Linux操作系统中有类似的符号链接(基于索引节点)。

Javascript中不可变的另一个好处是减少了时间耦合,这对设计有很大的好处。考虑一个具有两个方法的对象的接口:

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是安全的,因为类的内部状态不能改变。