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

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

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

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


当前回答

我认为支持不可变对象的主要原因是保持对象的状态有效。

假设我们有一个叫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始终处于有效状态。

其他回答

不变性的逆向观点

TL/DR:不可变性在JavaScript中更像是一种时尚趋势,而不是必须。如果你正在使用React,它确实为状态管理中一些令人困惑的设计选择提供了一个整洁的解决方案。然而,在大多数其他情况下,它带来的复杂性并不能增加足够的价值,更多的是用来填充简历,而不是满足客户的实际需求。

长话短说:阅读下文。

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

很高兴你问了这个问题!

前段时间,一个名叫Dan Abramov的天才写了一个javascript状态管理库Redux,它使用纯函数和不可变性。他还制作了一些非常酷的视频,让这个想法非常容易理解(和销售)。

时机很好。Angular的新鲜感正在消退,JavaScript世界已经准备好专注于最新的东西,它有合适的酷的程度,这个库不仅创新,而且与另一个硅谷巨头的React完美地结合在一起。

很遗憾,JavaScript的世界是由时尚主宰的。现在阿布拉莫夫被奉为半神,而我们所有的凡人都必须服从不朽之道……不管它是否有意义。

改变对象有什么错?

没有什么!

事实上,程序员一直在改变对象,呃…只要有对象就会变异。换句话说,50多年的应用程序开发。

为什么要把事情复杂化呢?当你有一个对象cat并且它死了,你真的需要第二个cat来跟踪变化吗?大多数人只会说猫。isDead =真的,结束吧。

(改变对象)难道不会让事情变得简单吗?

是的!. .当然了!

特别是在JavaScript中,它在实际应用中最有用的是呈现在其他地方维护的某些状态的视图(比如在数据库中)。

如果我有一个新的News对象需要更新怎么办?... 在这种情况下我该如何实现呢?删除商店并重新创建它?向数组中添加对象难道不是一种成本较低的操作吗?

好吧,您可以采用传统的方法并更新News对象,这样您在内存中对该对象的表示就会发生变化(以及显示给用户的视图,或者人们希望如此)……

或者……

您可以尝试迷人的FP/Immutability方法,并将对News对象的更改添加到跟踪每个历史更改的数组中,这样您就可以遍历数组并找出正确的状态表示形式(哎呀!)。

我想知道这里是什么。请启发我:)

时尚来来去去,伙计。剥猫皮的方法有很多。

很抱歉,您不得不忍受不断变化的编程范式所带来的困惑。但是,嘿,欢迎加入俱乐部!!

现在,关于不可变性,有几个重要的要点需要记住,你会以只有天真的人才能聚集起来的狂热强度向你抛出这些问题。

1)不可变性可以避免多线程环境中的竞争条件。

多线程环境(如c++, Java和c#)在多个线程想要更改对象时,会锁定对象。这对性能不利,但比数据损坏要好。然而,这并不像让一切都是不变的那样好(上帝赞美Haskell!)

但是唉!在JavaScript中,你总是在一个线程上操作。甚至是网络工作者(每个人都在独立的上下文中运行)。所以既然你不能在你的执行环境中有一个线程相关的竞态条件(所有那些可爱的全局变量和闭包),支持不可变性的主要观点就被排除在外了。

(话虽如此,在web worker中使用纯函数有一个优势,那就是你不会期望在主线程中摆弄对象。)

2)不可变性可以(以某种方式)避免应用状态中的竞态条件。

这里是问题的真正关键,大多数(React)开发者会告诉你不可变性和FP可以以某种方式创造这个魔法,让你的应用程序的状态变得可预测。

当然,这并不意味着你可以避免数据库中的竞态条件,要做到这一点,你必须协调所有浏览器中的所有用户,为此你需要一个后端推送技术,如WebSockets(下文将详细介绍),将更改广播给每个运行应用程序的人。

这也不意味着JavaScript中存在一些固有的问题,你的应用程序状态需要不可变才能变得可预测,任何在React之前编写前端应用程序的开发人员都会告诉你这一点。

This rather confusing claim simply means that if you use React your application is prone to race conditions, but that immutability allows you to take that pain away. Why? Because React is special.. its been designed first and foremost as a highly optimised rendering library with state management subverted to that aim, and thus component state is managed via an asynchronous chain of events (aka "one-way data binding") that optimize rendering but you have no control over and rely on you remembering not to mutate state directly...

在这种情况下,很容易看出对不可变性的需求与JavaScript没有多大关系,而与React有很大关系:如果在你的新应用程序中有一堆相互依赖的更改,并且没有简单的方法来确定当前的状态,你会感到困惑,因此使用不可变性来跟踪每一个历史更改是非常有意义的。

3)比赛条件非常糟糕。

如果你在用React的话,它们可能是。但如果你选择不同的框架,它们就很少出现。

除此之外,你通常还有更大的问题要处理……像依赖性这样的问题。比如一个臃肿的代码库。比如你的CSS没有被加载。比如缓慢的构建过程,或者被固定在一个单一的后端,使得迭代几乎不可能。就像没有经验的开发者不明白发生了什么,把事情搞得一团糟。

你知道的。现实。但是,谁会在乎这些呢?

4)不可变性利用引用类型来减少跟踪每个状态变化对性能的影响。

因为说真的,如果你打算在每次状态改变时复制东西,你最好确保你在这方面是聪明的。

5)不可变性允许你撤销某些内容。

因为er . .这是项目经理要求的第一个功能,对吧?

6)不可变状态与WebSockets结合有很多很酷的潜力

最后但并非最不重要的是,状态增量的积累与WebSockets结合使用是一个非常引人注目的例子,它允许将状态轻松地作为不可变事件流使用……

Once the penny drops on this concept (state being a flow of events -- rather than a crude set of records representing the latest view), the immutable world becomes a magical place to inhabit. A land of event-sourced wonder and possibility that transcends time itself. And when done right this can definitely make real-time apps easier to accomplish, you just broadcast the flow of events to everyone interested so they can build their own representation of the present and write back their own changes into the communal flow.

But at some point you wake up and realise that all that wonder and magic do not come for free. Unlike your eager colleagues, your stakeholders (yea, the people who pay you) care little about philosophy or fashion and a lot about the money they pay to build a product they can sell. And the bottom line is that its harder to code for immutability and easier to break it, plus there is little point having an immutable front-end if you don't have a back-end to support it. When (and if!) you finally convince your stakeholders that you should publish and consume events via a push techology like WebSockets, you find out what a pain it is to scale in production.


现在给你一些建议,如果你选择接受的话。

A choice to write JavaScript using FP/Immutability is also a choice to make your application code-base larger, more complex and harder to manage. I would strongly argue for limiting this approach to your Redux reducers, unless you know what you are doing... And IF you are going to go ahead and use immutability regardless, then apply immutable state to your whole application stack, and not just the client-side. After all, there is little point having an immutable front-end, and then connect it to a database where all records have a single mutable version... you just go back to the same problems you were trying to get away from!

Now, if you are fortunate enough to be able to make choices in your work, then try and use your wisdom (or not) and do what's right by the person who is paying you. You can base this on your experience, on your gut, or whats going on around you (admittedly if everyone is using React/Redux then there a valid argument that it will be easier to find a resource to continue your work).. Alternatively, you can try either Resume Driven Development or Hype Driven Development approaches. They might be more your sort of thing.

简而言之,关于不可变性,我们要说的是,它会让你在同龄人中变得时髦,至少在下一次热潮到来之前,到那时你会很高兴地离开。


现在,在这段自我治疗之后,我想指出的是,我已经在我的博客中添加了一篇文章=> JavaScript的不变性:逆向观点。如果你有强烈的感觉,也想倾诉,请在那里回复;)

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

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

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

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

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

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

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

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

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

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

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

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

new state  = r(current state, action)

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

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


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

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

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

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

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

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

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

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

可预测性

突变隐藏了更改,而更改会产生(意想不到的)副作用,从而导致严重的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();

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

数组 对象