返回介绍

结合 Immutable.JS 使用 Redux

发布于 2019-08-14 字数15349 浏览 772 评论 0

目录

  • 为什么应该使用 Immutable.JS 等不可变的库?
  • 为什么应该选择 Immutable.JS 作为不可变的库?
  • 使用 Immutable.JS 有什么问题?
  • Immutable.JS 是否值得使用?
  • 在 Redux 中使用 Immutable.JS 有哪些最佳实践?

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

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

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

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

更多信息

文档

  • 技巧:关于不可变(immutability)和突变(mutation)以及副作用

文章

  • Immutable.js 和函数式编程概念介绍
  • React.js 使用不可变的优点和缺点

为什么应该选择 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,持续化数据结构与结构共享
  • PDF: JavaScript Immutability – 不要更改

  • Immutable.js

使用 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() 来避免,如下面最佳实践部分所述。

更多信息

文章

  • Immutable.js,持续化数据结构与结构共享
  • 不可变的数据结构与 JavaScript
  • React.js 纯粹渲染性能反面模式(anti-pattern)
  • 使用 React 和 Redux 构建高效的用户界面

Chrome 扩展程序

  • Immutable 对象格式化扩展

Immutable.JS 是否值得使用?

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

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

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

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

更多信息

文档

  • 排错:dispatch action 后什么也没有发生

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

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

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

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

更多信息

文章

  • 不可变的数据结构与 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 类型。

更多信息

文章

  • 不可变的数据结构与 JavaScript

  • redux-immutable

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

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

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

更多信息

文章

  • 不可变的数据结构与 JavaScript
  • React 中的 Smart 和 Dumb 组件

限制对 toJS() 的使用

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

更多信息

议题

  • Lee Byron 的 Twitter: “Perf tip for #immutablejs…”

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

这种做法可以带来以下几个好处:

  • 它避免了在选择器中调用 .toJS() 导致的不必要的重新渲染(因为 .toJS() 将始终返回一个新对象)。
    • 虽然可以在调用 .toJS() 的地方对选择器进行缓存,但是如果返回的是 immutable.js 对象,就不需要多余的缓存。
  • 为选择器建立一致的接口; 您将不必考虑返回的是 Immutable.js 对象还是纯 JavaScript 对象。

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

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

更多信息

文档

  • 技巧:计算衍生数据
  • FAQ:Immutable 数据
  • Reselect 文档:如何使用 Reselect 结合 Immutable.js?

文章

  • Redux 模式和反面模式

  • Reselect: Redux 的选择器库

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

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

更多信息

文档

  • FAQ: Immutable 数据

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

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

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

更多信息

文章

  • 不可变的数据结构与 JavaScript
  • React 中的 Smart 和 Dumb 组件
  • 更好的 Redux 体系结构的小贴士:企业规模的经验教训

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

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

下面是一个关于 HOC 的例子。为了方便您,还以 NPM 包的形式提供了类似的 HOC:with-immutable-props-to-js。

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 改变时才会调用组件。与任何性能问题一样,在决定优化什么之前先进行性能检测。

更多信息

文档

  • React:高阶组件

文章

  • 深入了解 React 的高阶组件

议题

  • Reddit: acemarke 和 cpsubrian 对 Dan Abramov 的评论:Redux 不是一种架构或设计模式,它只是一个库。

Gists

  • cpsubrian: React decorators for redux/react-router/immutable ‘smart’ components

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

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

更多信息

Chrome 扩展

  • Immutable 对象格式化扩展

您暂时不能评论!

管理员开启了需要登录才能够评论,你可以免费注册一个本站的账号。

还没有评论!

目前还没有任何评论,快来抢沙发吧!