结合 Immutable.JS 使用 Redux

目录

为什么应该使用 Immutable.JS 等不可变的库?

Immutable.JS 不可变的库被设计旨在解决 JavaScript 中固有的不可变(Immutability)问题,为应用程序提供不可变带来的所有好处。

你是选择使用这样的库,还是坚持使用简单的 JavaScript,完全取决于你对向应用程序中添加另一个依赖是满意程度,或者取决于你是否确信使用它可以避免 JavaScript 处理不可变的方法中固有的缺陷。

无论你做何选择,请确保你熟悉不可变,副作用和突变的概念。尤其要确保你深入了解 JavaScript 在更新和复制值时所做的操作,以防止意外的突变(mutation)导致应用程序性能的降低,甚至完全破坏应用程序的性能。

更多信息

文档

文章

为什么应该选择 Immutable.JS 作为不可变的库?

Immutable.JS 旨在以一种高性能的方式提供不可变,以克服 JavaScript 不可变的局限性。其主要优点包括:

保证不可变

封装在 Immutable.JS 对象中的数据永远不会发生变换(mutate)。总是会返回一个新的拷贝对象。这与 JavaScript 相反,其中一些操作不会改变数据(例如,一些数组方法,包括 map,filter,concat,forEach 等),但有一些操作会改变数据(Array 的 pop,push,splice 等)。

拥有 API

Immutable.JS 提供了一组丰富的不可变对象来封装数据(例如,Maps,Lists,Sets,Records 等),以及一系列操作它们的方法,包括 sort,filter,数据分组,reverse,flatten 以及创建子集等方法。

性能

Immutable.JS 在实现过程中针对性能优化做了很多工作。这是非常关键的功能,因为使用不可变的数据结构可能需要进行大量昂贵的复制。尤其是对大型复杂数据集(如嵌套的 Redux state tree(状态树))进行不可变操作时,中间可能会产生很多拷贝对象,当浏览器的垃圾回收器清理对象时,这些拷贝对象会消耗内存并降低性能。

Immutable.JS 内部通过巧妙共享数据结构避免了这种情况,最大限度地减少了拷贝数据的情况。它还能执行复杂的操作链,而不会产生不必要的(且昂贵的)中间数据克隆,这些数据很快就会被丢弃。

你决不会看到这些,当然 - 你给 Immutable.JS 对象的数据永远不会发生变化。但是,它从 Immutable.JS 中生成的 intermediate 数据,可以通过链式调用序列中的数据进行自由的变换。因此,你可以拥有不可变数据结构的所有优势,并且不会产生任何潜在的(或很少)性能问题。

更多信息

文章

使用 Immutable.JS 有什么问题?

尽管功能强大,但 Immutable.JS 还是需要谨慎使用,因为它存在它自己的问题。注意,所有这些问题都可以通过谨慎编码轻松解决。

交互操作困难

JavaScript 没有提供不可变的数据结构。因此,要保证 Immutable.JS 其不可变,你的数据就必须封装在 Immutable.JS 对象(例如:MapList 等)中。一旦使用这种方式包裹数据,这些数据就很难与其他普通的 JavaScript 对象进行交互操作。

例如,你将不再能够通过标准 JavaScript 中的点语法或中括号引用对象的属性。相反,你必须通过 Immutable.JS 提供的 get()getIn() 方法来引用它们,这些方法使用了一种笨拙的语法,通过一个字符串字符串数组访问属性,每个字符串代表一个属性的 key。

例如,你将使用 myImmutableMap.getIn(['prop1', 'prop2', 'prop3']) 替代 myObj.prop1.prop2.prop3

这不仅使得与你自己的代码进行交互操作变得尴尬,而且还与其他库(如 lodash 或 ramda)的交互也会很尴尬,这些库都需要普通的 JavaScript 对象。

注意,Immutable.JS 对象确实包含 toJS() 方法,该方法会返回普通 JavaScript 数据结构形式的对象,但这种方法非常慢,广泛使用将会失去 Immutable.JS 提供的性能优势。

一旦使用,Immutable.JS 将遍布整个代码库

一旦使用 Immutable.JS 封装数据,你必须使用 Immutable.JS 的 get()getIn() 属性访问器来访问它。

这将会在整个代码库中传播 Immutable.JS,包括潜在组件,你可能不喜欢拥有这种外部依赖关系。你的整个代码库必须知道哪些应该是 Immutable.JS 对象,哪些不是。这也会使得当你想从应用程序中移除 Immutable.JS 变得非常困难。

如下面最佳实践部分所述,可以通过将应用程序逻辑与数据结构解耦来避免此问题。

没有解构或展开运算符(Spread Operators)

因为你必须通过 Immutable.JS 本身的 get()getIn() 方法来访问你的数据,所以你不能再使用 JavaScript 的解构运算符(或者提案中的 Object 扩展运算符),这使得你的代码更加冗余。

不适用于经常改变的小数值

Immutable.JS 最适用于数据集合,越大越好。当你的数据包含大量小而简单的 JavaScript 对象时,速度会很慢,每个对象都包含几个基本数据类型的 key。

注意:无论如何,这都不适用于 Redux state tree,该树通常为大量数据的集合。

难以调试

Immutable.JS 对象,如 MapList 等可能很难调试,因为检查这样的对象会看到整个嵌套层级结构,这些层级是你不关心的 Immutable.JS 特定的属性,而且你真正关心的是实际数据被封装了几层。

要解决此问题,请使用浏览器扩展程序,如 Immutable.js 对象格式化扩展,它在 Chrome 开发工具中显示数据,并在检查数据时隐藏 Immutable.JS 的属性。

破坏对象引用,导致性能较差

不可变的一个主要优点是它可以浅层平等检查,大大提高了性能。

如果两个不同的变量引用同一个不可变对象,那么对这两个变量进行简单的相等检查就足以确定它们是否相等,并且它们所引用的对象是不可变的。等式检查从不必检查任何对象属性的值,因为它是不可变的。

然而,如果封装在 Immutable.JS 对象中的数据本身就是一个对象,渐层检查起不到任何作用。这是因为 Immutable.JS 的 toJS() 方法会将 Immutable.JS 对象中的数据作为 JavaScript 值并返回,每次调用它时都会创建一个新对象,并且使用封装数据来分解引用。

因此,如果调用 toJS() 两次,并将结果赋值给两个不同的变量将导致这两个变量的等式检查失败,即时对象值本身没有改变。

如果在包装组件的 mapStateToProps 函数中使用 toJS(),这就是一个特殊的问题了,因为 React-Redux 对返回的 props 对象中的每个值都进行了简单的比较。例如,下面代码中的 mapStateToProps 返回的 todos prop 所引用的值将始终是不同的对象,因此无法通过渐层等式检查。

// 避免在 mapStateToProps 中使用 .toJS()
function mapStateToProps(state) {
  return {
    todos: state.get('todos').toJS() // 总为新对象
  }
}

当浅层检查失败时,React-Redux 将导致组件重新渲染。因此,在 mapStateToProps 中使用 toJS() 的方式,将导致组件重新渲染,即时值未发生变化,也会严重影响性能。

该问题可以通过在高阶组件中使用 toJS() 来避免,如下面最佳实践部分所述。

更多信息

文章

Chrome 扩展程序

Immutable.JS 是否值得使用?

通常来说,是的。有各种各样的权衡和意见参考,但有很多很好的理由推荐使用。不要低估尝试追踪无意间突变的 state tree 中的属性的难度。

组件在不必要时重新渲染,在它必要时拒绝渲染,以及追踪致使出现渲染问题的错误都是非常困难的,因为渲染不正确的组件不一定是属性突变的组件。

这个问题主要是由 Redux 的 reducer 返回一个突变的 state 对象引起的。使用 Immutable.JS,此类问题根本不会出现,因此,你的应用程序中就排除了这类错误。

以上这些与它的性能以及丰富的数据操作 API 组合在一起,就是为什么值得使用 Immutable.JS 的原因了。

更多信息

文档

在 Redux 中使用 Immutable.JS 有哪些最佳实践?

Immutable.JS 可以为你的应用程序提供可靠性和显著的性能优化,但必须正确使用。如果你选择使用 Immutable.JS(记住,并不是必须使用它,还有其他不可变库可以使用),请遵循这些有见地的最佳实践,你将能充分利用它,从而不会被它可能导致的任何问题绊倒。

永远不要将普通的 JavaScript 对象与 Immutable.JS 混合使用

永远不要让一个普通的 JavaScript 对象包含 Immutable.JS 属性。同样,永远不要让 Immutable.JS 对象包含一个普通的 JavaScript 对象。

更多信息

文章

使整个 Redux state tree 成为 Immutable.JS 对象

对于使用 Redux 的应用程序来说,你的整个 state tree 应该是 Immutable.JS 对象,根本不需要使用普通的 JavaScript 对象。

  • 使用 Immutable.JS 的 fromJS() 函数创建树。

  • 使用 combineReducers 函数的 Immutable.JS 的感知版本,比如 redux-immutable 中的版本,因为 Redux 本身会将 state tree 变成一个普通的 JavaScript 对象。

  • 当使用 Immutable.JS 的 updatemergeset 方法将一个 JavaScript 对象添加到一个 Immutable.JS 的 Map 或者 List 中时,要确保被添加的对象事先使用了 fromJS() 转为一个 Immutable 的对象。

示例

// 避免
const newObj = { key: value }
const newState = state.setIn(['prop1'], newObj)
// newObj 作为普通的 JavaScript 对象,而不是 Immutable.JS 的 Map 类型。

// 推荐
const newObj = { key: value }
const newState = state.setIn(['prop1'], fromJS(newObj))
// newObj 现在是 Immutable.JS 的 Map 类型。

更多信息

文章

在除了 Dumb 组件外的组件使用 Immutable.JS

在任何地方使用 Immutable.JS 都可以保证代码的高性能。在你的 smart 组件中,选择器中,saga 或 thunk 中,action 创建函数 中,特别是你的 reducer 中都可以使用它。

但是,请不要在你的 Dumb 组件中使用 Immutable.JS。

更多信息

文章

限制对 toJS() 的使用

toJS() 是一个昂贵(性能)的函数,并且与使用 Immutable.JS 的目的相违背。避免使用它。

更多信息

议题

你的选择器应该返回 Immutable.JS 对象

总是返回 Immutable.JS 对象。

在 Smart 组件中使用 Immutable.JS 对象

通过 React Redux 的 connect 函数访问 store 的 Smart 组件必须使用 Immutable.JS 作为选择器的返回值。以确保你避免了由于不必要的组件重新渲染而导致的潜在问题。必要时使用库来记忆选择器(例如:reselect)。

更多信息

文档

文章

绝对不要在 mapStateToProps 中使用 toJS()

使用 toJS() 将 Immutable.JS 对象转换为 JavaScript 对象时,每次都会返回一个新的对象。如果在 mapStateToProps 中执行此操作,则会导致组件在每次 state tree 更改时都认为该对象已更改,因此会触发不必要的重新渲染。

更多信息

文档

永远不要在你的 Dumb 组件中使用 Immutable.JS

你的 dumb 组件应该是纯粹的;也就是说,它们应该在给定相同的输入的情况下产生相同的输出,并不具有外部依赖性。如果你将这一一个组件作为 props 传递给一个 Immutable.JS 对象,那么你需要依赖 Immutable.JS 来提取 props 的值,并以其他的方式操纵它。

这种依赖性会导致组件不纯,使组件测试更加困难,并且使组件复用和重构变得非常困难。

更多信息

文章

使用高阶组件来转换从 Smart 组件的 Immutable.JS props 到 Dumb 组件的 JavaScript props

有些东西需要将 Smart 组件中的 Immutable.JS props 映射到 Dumb 组件中的纯 JavaScript props。这里的有些东西是指高阶组件(HOC),它只需从 Smart 组件中获取 Immutable.JS props,然后使用 toJS() 将它们转换为普通 JavaScript props,然后传递给你的 Dumb 组件。

这是一个关于 HOC 的例子:

import React from 'react'
import { Iterable } from 'immutable'

export const toJS = WrappedComponent => wrappedComponentProps => {
  const KEY = 0
  const VALUE = 1

  const propsJS = Object.entries(wrappedComponentProps).reduce(
    (newProps, wrappedComponentProp) => {
      newProps[wrappedComponentProp[KEY]] = Iterable.isIterable(
        wrappedComponentProp[VALUE]
      )
        ? wrappedComponentProp[VALUE].toJS()
        : wrappedComponentProp[VALUE]
      return newProps
    },
    {}
  )

  return <WrappedComponent {...propsJS} />
}

以下为如何在 Smart 组件中使用它:

import { connect } from 'react-redux'

import { toJS } from './to-js'
import DumbComponent from './dumb.component'

const mapStateToProps = state => {
  return {
    // obj 是一个 Smart 组件中的不可变对象,
    // 但它通过 toJS 被转换为普通 JavaScript 对象,并以纯 JavaScript 的形式传递给 Dumb 组件对象。
    // 因为它在 mapStateToProps 中仍然是 Immutable.JS 对象,
    // 虽然,这是无疑是错误重新渲染。
    obj: getImmutableObjectFromStateTree(state)
  }
}
export default connect(mapStateToProps)(toJS(DumbComponent))

通过在 HOC 中将 Immutable.JS 对象转换为纯 JavaScript 值,我们实现了 Dumb 的可移植性,也没在 Smart 组件中使用 toJS() 影响性能。

注意: 如果你的应用程序需要高性能,你可能需要完全避免使用 toJS(),所以必须在你的 Dumb 组件中使用 Immutable.JS。但是,对于大多数应用程序来说并非如此,将 Immutable 保留在 Dumb 组件(可维护性,可移植性和更简单的测试)等方面的好处远远超过了保持它任何方面性能优化。

另外,在高阶组件中使用 toJS 应该不会引起任何性能的下降,因为只有在 connect 组件的 props 改变时才会调用组件。与任何性能问题一样,在决定优化什么之前先进行性能检测。

更多信息

文档

文章

议题

Gists

使用不可变对象格式化 Chrome 扩展来辅助调试

安装 Immutable 对象格式化扩展,并检查你的 Immutable.JS 数据,而不会看到 Immutable.JS 本身的对象属性混淆视听。

更多信息

Chrome 扩展