Immutable相关知识

1. JS 与 Immutable 的对应关系

  • JS.Array => Immutable.List

  • JS.Map => Immutable.Map

  • JS.Set => Immutable.Set

2. 比较 immutable 值

应该使用 is 或 equals 来比较 immutable 的值

import { is, Map } from 'immutable'

const a = Map({ a: 1, b: 2 })
const b = Map({ a: 1, b: 2 })
console.log(a.equals(b))     // true
console.log(is(a, b))        // true
console.log(a === b)         // false

如果一个操作,返回了一个没有变化的对象,那么可以使用 === 来进行比较:

import { Map } from 'immutable'

const a = Map({ a: 1, b: 2 })
const b = a.set('b', 2)         // 实际上并没有改变 a
console.log(a === b)            // true
console.log(a.equals(b))        // true
console.log(is(a, b))           // true

3. Immutable List 具有和 JS Array 一样的 API

import { is, List } from 'immutable'

const list1 = List([ 1, 2 ])         // List [1,2]
const list2 = list1.push(3, 4, 5)    // List [1,2,3,4,5]

console.log(list1 === list2)         // false,都是返回新的 List
console.log(list1.equals(list2))     // false
console.log(is(list1, list2))        // false

const list3 = list2.unshift(0)       // List [0,1,2,3,4,5]
list3.forEach(v => {
  console.log(v)                     // 0,1,2,3,4,5
})

4. 原生的 JS 对象或数组作为 Immutable 的函数参数

在任何能接受 Collection 的地方,都能传入原生的 JS 对象或数组作为参数

import { Map } from 'immutable'

const map1 = Map({ a: 1 })
const map2 = Map({ b: 2 })
const obj = { c: 2 }
console.log(map1.merge(map2, obj)) // Map {a:1,b:2,c:3}

const list1 = List([1])
const list2 = List([2])
const arr = [3]
console.log(list1.concat(list2, arr)) // List [1,2,3]

5. 原生 JS 对象与 Immutable 对象互相转换

fromJS: 将原生 JS 对象和数组转成 Immutable:

import { Map, List, fromJS } from 'immutable'

// 转换对象
const a = { 1: 'one' }
console.log(a['1'], a[1])    // 'one', 'one'
const map = fromJS(a)        // Map {'1' : 'one'}

// 当将原生 js 对象转为 immutable 时,所有的 key 都会转为字符串类型。
// 因此 map.get(1) 返回 undefined
console.log(map.get('1'), map.get(1)) // 'one', undefined


// 转换数组
const b = [1]
const list = fromJS(b)
console.log(list) // List [1]

将 immutable 转换为原生 JS 对象分为浅转换和深转换:

  • 浅转换: toArray(), toObject(), 即只转换一层

  • 深转换: toJS()

// 1. immutable 转对象
const map = Map({ a: 1, b: Map({ c: 2 }) })
console.log(map.toObject())   // { a: 1, b: Map {c: 2} }  其中 b 还是 Map 类型
console.log(map.toJS())       // { a: 1, b: { c: 2 } }  完全转化为 JS 对象类型

// 同时 immutable 对象都实现了 toJSON 方法,当调用 JSON.stringify 时会调用
console.log(JSON.stringify(map)) 


// 2. immutable 转数组
const arr = List([1, List([2])])
console.log(arr.toArray())   // [1, List[2]]  其中第二个元素还是 List 类型
console.log(arr.toJS())      // [1,[2]]  完全转化为 JS 数组类型

// 同时 immutable 对象都实现了 toJSON 方法,当调用 JSON.stringify 时会调用
console.log(JSON.stringify(arr))

6. 嵌套结构的读取和操作

import { fromJS } from 'immutable'

const map1 = fromJS({ a: { b: { c: [3, 4, 5] } } })


// 1. 合并
const map2 = map1.mergeDeep({ a: { b: { d: 6 } } })   
console.log(map2.toJS())                     // { a: { b: { c: [3, 4, 5] }, d: 6 } }

// 2. 根据路径读取
console.log(map2.getIn(['a', 'b', 'd']))     // 6

// 3. 更新属性值
const map3 = map2.updateIn([ 'a', 'b', 'd' ], value => value + 1)
console.log(map3.getIn(['a', 'b', 'd']))     // 7

// 4. 更新 List
const map4 = map2.updateIn([ 'a', 'b', 'c' ], list => list.push(6))
console.log(map4.getIn(['a', 'b', 'c']))     // List [3,4,5,6]

7. 自返回的优化和 withMutations 优化

自返回的优化:

const map1 = Map({ a: 1, b: 2 })
const map2 = map1.set('b', 2)
console.log(map1 === map2)    // true, 当生成的值不变时,immutable 会返回原来的对象


const map3 = map1.set('b', 3)
const map4 = map1.set('b', 3)
console.log(map3 === map4)    // false, 当生成的值变化时,immutable 每次都会返回新的对象,新对象之间是相互独立的
console.log(map3.equals(map4)) // true, 但是使用 is 或者 equals 检测是相等的

可以使用 withMutations 来合并一些变化, 因为【没有临时的中间对象生成】,因此能够优化性能

const list1 = List([ 1, 2, 3 ])
const list2 = list1.withMutations(function (list) {
  list.push(4).push(5).push(6)
})
console.log(list2)  // List [1, 2, 3, 4, 5, 6]

8. 性能优化举例

import { updateIn, Map } from 'immutable'

const Child = React.memo((props) => {
  console.log('render child')  // 每次点击都会打印
  return <div>{JSON.stringify(props)}</div>
})

export default class App extends Component {
  constructor (props) {
    super(props)
    this.state = {
      store: {
        a: {
          b: 1
        }
      }
    }
  }
  handleClick = () => {
    this.setState({ store: { a: { b: 1 } } })
  }
  render = () => {
    return (
      <div className='title' onClick={this.handleClick}>
        Hello, express-react-dev-template
        <Child store={this.state.store} />
      </div>
    )
  }
}

在平常写组件的时候,我们可能会有以上的代码,尽管实际上 store 的值没有发生变化,但是还是每次都是打印 render child

React.memo 没有生效,当然这种场景下我们可以通过自定义 React.memo 的比较函数,来保证这种情况下不渲染。

但是当 props 数据结构比较复杂时,自定义函数也会很麻烦

而使用 immutable 就没有这个问题:

const Child = React.memo((props) => {
  console.log('render h1')    // 只会打印一次
  return <div>{JSON.stringify(props)}</div>
})

export default class App extends Component {
  constructor (props) {
    super(props)
    this.state = {
      store: Map({ a: { b: 1 } })
    }
  }
  handleClick = () => {
    this.setState({ store: updateIn(this.state.store, ['a', 'b'], value => value) })
  }
  render = () => {
    return (
      <div className='title' onClick={this.handleClick}>
            Hello, express-react-dev-template
        <Child store={this.state.store} />
      </div>
    )
  }
}

通过定义 immutable 类型的数据作为 state 的值,React.memo 就会认为两次的 props 相等,从而避免无谓的渲染,

9. List

9.1 构造函数

List 是函数而不是类,因此不需要使用 new 关键字

List 接受实现了 Iterable 接口的值(如数组,Set 等)作为参数,返回 List 结构。

const a = List()            // 空数组
console.log(a)

const b = List([1, 2, 3])   // 接受数组作为参数
console.log(b)

const c = List(new Set([1, 2, 3]))   // 接受 set 作为参数

console.log(c.equals(b))     // true,equals 会进行值的比较,而不会严格要求引用相等

9.2 静态方法

// List.isList: 判断是否是 List
List.isList([])              // false
List.isList(List([]))        // true

// List.of: 根据参数,生成 List 结构
List.isList(List.of(1, 2)) // true

9.3 size

// 返回元素数量
List.of(1, 2).size        // 2

9.4 改变数据的方法

// set(index, value),  给 index 位置设置 value 值
const a = List([1, 2])
console.log(a.set(2, 3))              // [1, 2, 3]

// delete(index)  删除 index 所在的元素
// 别名: alias
const a = List([1, 2, 3])
console.log(a.delete(2))              // [1, 2]

// insert(index, value) 在 index 插入值 value
const a = List([1, 2])
console.log(a.insert(2, 3))           // [1,2,3]

// clear()     清空 list
const a = List([1, 2])
console.log(a.clear())           // []

// push(value)   添加元素
const a = List([1, 2])
console.log(a.push(3))           // [1,2,3]

// pop(), shift(), unshift(value)  略

// update(index, (value) => value)  更新该值
const a = List([1, 2])
console.log(a.update(0, value => value + 1)) // [2,2]

9.5 深度改变数据的方法

// setIn(keyPath: [], value ) 根据路径设置值
const a = List([1, 2, List([3, 4])])
// 相当于 a[2][0] = 4
console.log(a.setIn([2, 0], 4)) // [1,2, List[4,4]]

// deleteIn(keyPath: [] )  删除指定 keypath 的值
const a = List([1, 2, List([3, 4])])
console.log(a.deleteIn([2, 0])) // [1,2, List[ undefined, 4]]

// updateIn(), mergeIn(). mergeDeepIn() 略,查看 Map

10. Map

和 JS 中的 Map 类似,可以把 对象 作为 key。

10.1 构造函数

const a = Map()  // 空 Map

const a = Map({ key: 'value' })  // Map {key: 'value' }
// 或者
const a = Map([ ['key', 'value'] ])  // Map { 'key': 'value' }

10.2 静态方法

// Map.isMap(value)  // 判断 value 是否为 Map 类型
const a = Map([ ['key', 'value'] ])
console.log(Map.isMap(a))          // true
console.log(Map.isMap('1'))        // false

10.3 size

const a = Map([ ['key', 'value'] ])
console.log(a.size)          // 1

10.4 改变数据的方法

// set(key, value)  设置属性和值
const a = Map()
a.set('key', 'value')    // Map { key : value }

// delete(key)  删除某个属性
const a = Map({'key': 'value'})
a.delete('key')         // Map {}

// deleteAll(keys: string[])  删除一批属性
const a = Map({'key': 'value', 'foo': 'bar'})
a.deleteAll(['key', 'foo'])         // Map {}

// clear()  清空 Map
const a = Map({'key': 'value', 'foo': 'bar'})
a.clear()         // Map {}

// update(key, (value) => value)
const a = Map({'key': 'value'})
a.update('key', value => value + value)   // Map {'key': 'valuevalue'}
// 设置数组
const a = Map({'key': List([])})
a.update('key', list => list.push(1))   // Map {'key': [1]}

// merge, 合并对象
const map1 = Map({ a: 1, b: 2 })
const map2 = Map({ a: 2, c: 3 })
map1.merge(map2)         // {a:2,b:2,c3}
map2.merge(map1)         // {a:1,b:2,c3}

// mergeWith(), 和 merge 类似,可以提供一个函数,用于处理当map1 和 map2 相同 key 的情况
// mergeDeep(), mergeDeepWith()  略

10.5 深度改变数据的方法

// setIn(keyPath, value) 根据 path 设置值
const map = Map({ a: { b: { c: { d: 1 } } } })
console.log(map.setIn(['a', 'b'], 1))  // Map {a: {b: 1}}

// deleteIn(keyPath)  根据 path 删除值
const map = Map({ a: { b: { c: { d: 1 } } } })
console.log(map.deleteIn(['a', 'b'], 1))  // Map {a:{}}

// updateIn(keyPath,(value) => value)  根据 keyPath 更新值
const map = Map({ a: { b: { c: { d: 1 } } } })
console.log(map.updateIn(['a', 'b'], value => 2))  // Map {a: {b: 2}}

11. 别的类库

在日常开发中,我们还可以使用诸如 immutability-helper 这样的简单一些的库。

比如刚刚那个性能优化的例子,如果使用 immutability-helper 那就是这样:

import update from 'immutability-helper'

const Child = React.memo((props) => {
  console.log('render h1')   // 同样只会触发一次
  return <div>{JSON.stringify(props)}</div>
})

export default class App extends Component {
  constructor (props) {
    super(props)
    this.state = {
      store: {
        a: {
          b: 1
        }
      }
    }
  }
  handleClick = () => {
    console.log('click')
    const newStore = update(this.state.store, { a: { b: { $set: 1 } } }) 

    console.log(newStore === this.state.store) // true,即 update 的返回值是 JS 对象, 而不是 immutable 对象
    this.setState({ store: newStore })
  }
  render = () => {
    return (
      <div className='title' onClick={this.handleClick}>
        Hello, express-react-dev-template
        <Child store={this.state.store} />
      </div>
    )
  }
}

参考资料

Last updated