为什么我们如此需要 Immutable Data

2,853 阅读3分钟

Immutable Data 的优点

Immutable 意为「不可变的」。在编程领域,Immutable Data 是指一种一旦创建就不能更改的数据结构。它的理念是:在赋值时,产生一个与原对象完全一样的新对象,指向不同的内存地址,互不影响。

避免副作用

当我们需要对一个对象进行修改时,直接在原对象上进行变更很方便,也很节省内存。但是在 JavaScript 中,对象都是引用类型,在按引用传递数据的场景中,会存在多个变量指向同一个内存地址的情况,这样会引发不可控的副作用:

let a = { x: 1 };
let b = a;
b.x = 6;

a.x // 6

在一个复杂应用中,如果有多个代码块同时更改这个引用,就会产生竞态。你需要关心这个对象会在哪个对方被修改,你对它的修改会不会影响其他代码的运行。使用 Immutable Data 就不会产生这个问题——因为每当状态更新时,都会产生一个新的对象:

let a = { x: 1, y: 2 }; // 初始状态
a = { ...a, x: 6 }; // 创建了一个新的对象,更新了 a 

状态可追溯

由于每次修改都会创建一个新对象,且对象不变,那么变更的记录就能够被保存下来,应用的状态变得可控、可追溯。Redux Dev Tool 和 Git 这两个能够实现「时间旅行」的工具就是秉承了 Immutable 的哲学。

git revert -- From Atlassian Git Tutorial

为什么 React.js 需要 Immutable Data

简短答案:为了让 React 精准地重渲染 UI。

自从 React.js 出现以来,社区中关于 Immutable 的讨论成倍增加,所以为什么 React.js 需要 Immutable?

我们知道,在 React 中,UI 是 state 的投影,state 的变更会引发 UI 的重新渲染。React 使用 Virtual DOM 来解决 UI 更新的问题——它会将新旧两棵 Virtual DOM 树进行比较,如果两者存在差异,则它会将这些差异来更新在真实的 DOM 上。

调用setState时,React 会以 shallowMerge 的方式将我们传入的对象与旧的 state 进行合并。shallowMerge 只会合并新旧 state 对象中第一层的内容,如果 state 中对象的引用未变,那么 React 认为这个对象前后没有发生变化。

所以如果我们以 mutable 的方式更改了 state 中的某个对象, React 会认为该对象并没有更新,那么相对应的 UI 就不会被重渲染。而以 Immutable 的方式更新 state 就不会出现这个问题。具体例子如下:

function App() {
  const [list, setList] = useState([1, 2, 3]);

  const handleFirstClick = () => {
    list.push("new Item");
    setList(list);
  };

  const handleSecondClick = () => {
    setList([...list, "new item"]);
  };

  return (
    <div className="App">
      <button onClick={handleFirstClick}>Add an item in mutable way</button>
      <button onClick={handleSecondClick}>Add an item in immutable way</button>
      <ItemList list={list} />
    </div>
  );
}

function ItemList({ list }) {
  return (
    <ul>
      {list.map(item => {
        return <li>{item}</li>;
      })}
    </ul>
  );
}

我们甚至可以看到,在多次点击第一个按钮后,点击第二个按钮时,列表会新增之前第一个按钮产生的列表项。

至于如何解决多次创建新对象以更新 state 带来的性能问题,以及如何优雅地以 Immutable 的形式更新一个复杂深层浅套的 state,这又是另外一个话题了,社区中的很多 Immutable 库(如 Immutable.js)提供了很好的解决方案。


参考资料