15 分钟学会 Immutable

更新日期: 2021-06-23阅读: 1.6k

1. 什么是 Immutable ?

Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。主要原理是采用了 Persistent Data Structure(持久化数据结构),就是当每次修改后我们都会得到一个新的版本,且旧版本可以完好保留,也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),就是对于本次操作没有修改的部分,我们可以直接把相应的旧的节点拷贝过去,这其实就是结构共享。


2. Immutable 有什么优点?

2.1 降低复杂度,避免副作用

在 Javascript 中,对象都是引用类型,在按引用传递数据的场景中,会存在多个变量指向同一个内存地址的情况,这样会引发不可控的副作用,如下代码所示:

let obj1 = { name: '张三' };
let obj2 = obj1;
obj2.name = '李四';
 
console.log(obj1.name); // 李四

使用 Immutable 后:

import { Map } from 'immutable';
let obj1 = Map({ name: '张三'});
let obj2 = obj1;
obj2.set({name:'李四'});
console.log(obj1.get('name'));  // 张三

当我们使用 Immutable 降低了 Javascript 对象 带来的复杂度的问题,使我们状态变成可预测的。

2.2 节省内存

Immutable 采用了结构共享机制,所以会尽量复用内存。

import { Map } from 'immutable';
let obj1 = Map({
  name: 'zcy',
  filter: Map({age:6})
});
let obj2 = obj1.set('name','zcygov');
console.log(obj1.get('filter') === obj2.get('filter')); // true
// 上面 obj1 和 obj2 共享了没有变化的 filter 属性

2.3 方便回溯

Immutable 每次修改都会创建一个新对象,且对象不变,那么变更的记录就能够被保存下来,应用的状态变得可控、可追溯,方便撤销和重做功能的实现,请看下面代码示例:

import { Map } from 'immutable';
let historyIndex = 0;
let history = [Map({ name: 'zcy' })];
function operation(fn) {
  history = history.slice(0, historyIndex + 1);
  let newVersion = fn(history[historyIndex]);
  // 将新版本追加到历史列表中
  history.push(newVersion);
  // 记录索引,historyIndex 决定我们是否有撤销和重做
  historyIndex++;
}
function changeHeight(height) {
  operation(function(data) {
    return data.set('height', height);
  });
}
// 判断是否有重做
let hasRedo = historyIndex !== history.length - 1;
// 判断是否有撤销
let hasUndo = historyIndex !== 0; 

2.4 函数式编程

Immutable 本身就是函数式编程中的概念,纯函数式编程比面向对象更适用于前端开发。因为只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。

2.5 丰富的 api

Immutable 实现了一套完整的 Persistent Data Structure,提供了很多易用的数据类型。像Collection、List、Map、Set、Record、Seq,以及一系列操作它们的方法,包括 sort,filter,数据分组,reverse,flatten 以及创建子集等方法,具体 API 请参考官方文档


3. 在 react 中如何使用 Immutable

我们都知道在 React 父组件更新会引起子组件重新 render,当我们传入组件的 props 和 state 只有一层时,我们可以直接使用 React.PureComponent,它会自动帮我们进行浅比较,从而控制 shouldComponentUpdate 的返回值。

但是,当传入 props 或 state 不止一层,或者传入的是 Array 和 Object 类型时,浅比较就失效了。当然我们也可以在 shouldComponentUpdate() 中使用使用 deepCopy 和 deepCompare 来避免不必要的 render() ,但 deepCopy 和 deepCompare 一般都是非常耗性能的。这个时候我们就需要 Immutable 。

以下示例通过浅比较的方式来优化:

import React, { Component } from 'react';
import Reactdom from 'react-dom';
class Counter extends Component {
      state = { counter: { number: 0 } }
      handleClick = () => {
          let amount = this.amount.value ? Number(this.amount.value) : 0;
          this.state.counter.number = this.state.counter.number + amount;
          this.setState(this.state);
      }
      // 通过浅比较判断是否需要刷新组件
      // 浅比较要求每次修改的时候都通过深度克隆每次都产生一个新对象
      shouldComponentUpdate(nextProps, nextState) {
          for (const key in nextState) {
              if (this.State[key] !== nextState[key]) {
                  return true;
              }
          }
          return false;
      }
  }
  render() {
      console.log('render');
      return (
          <div>
              <p>{this.state.number}</p>
              <input ref={input => this.amount = input} />
              <button onClick={this.handleClick}>+</button>
          </div>
      )
  }
}
ReactDOM.render(
    <Caculator />,
    document.getElementById('root')
)

也可以通过深度比较的方式判断两个状态的值是否相等这样做的话性能非常低。

shouldComponentUpdate(nextProps, prevState) {
// 通过 lodash 中 isEqual 深度比较方法判断两个值是否相同
return !_.isEqual(prevState, this.state);
}

Immutable 则提供了简洁高效的判断数据是否变化的方法,只需 === 和 is 比较就能知道是否需要执行 render() ,而这个操作几乎 0 成本,所以可以极大提高性能。修改后的 shouldComponentUpdate 是这样的:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { is, Map } from 'immutable';
class Caculator extends Component {
    state = {
        counter: Map({ number: 0 })
    }
    handleClick = () => {
        let amount = this.amount.value ? Number(this.amount.value) : 0;
        let counter = this.state.counter.update('number', val => val + amount);
        this.setState({counter});
    }
    shouldComponentUpdate(nextProps = {}, nextState = {}) {
        if (Object.keys(this.state).length !== Object.keys(nextState).length) {
            return true;
        }
        // 使用 immutable.is 来进行两个对象的比较
        for (const key in nextState) {
            if (!is(this.state[key], nextState[key])) {
                return true;
            }
        }
        return false;
    }
    render() {
        return (
            <div>
                <p>{this.state.counter.get('number')}</p>
                <input ref={input => this.amount = input} />
                <button onClick={this.handleClick}>+</button>
            </div>
        )
    }
}
ReactDOM.render(
    <Caculator />,
    document.getElementById('root')
)

Immutable.is 比较的是两个对象的 hashCode 或 valueOf(对于 JavaScript 对象)。由于 immutable 内部使用了 Trie 数据结构来存储,只要两个对象的 hashCode 相等,值就是一样的。这样的算法避免了深度遍历比较,性能非常好。 使用 Immutable 后,如下图,当红色节点的 state 变化后,不会再渲染树中的所有节点,而是只渲染图中绿色的部分:


(图片引用自:Immutable 详解及 React 中实践)

所以使用 Immutable.is 可以减少 React 重复渲染,提高性能。


4. Immutable 结合 Redux 使用

下面是 Immutable 结合 Redux 使用的一个数值累加小示例:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'
import { createStore, applyMiddleware } from 'redux'
import { Provider, connect } from 'react-redux'
import immutable, { is, Map } from 'immutable';
import PureComponent from './PureComponent';
const ADD = 'ADD';
// 初始化数据时,使用 Map 保证数据不会被轻易修改
const initState = Map({ number: 0 });
function counter(state = initState, action) {
    switch (action.type) {
        case ADD:
            // 返回数据时采用 update 更新对象数据
            return state.update('number', (value) => value + action.payload);
        default:
            return state
    }
}
const store = createStore(counter);
class Caculator extends PureComponent {
    render() {
        return (
            <div>
                <p>{this.props.number}</p>
                <input ref={input => this.amount = input} />
                <button onClick={() => this.props.add(this.amount.value ? Number(this.amount.value) : 0)}>+</button>
            </div>
        )
    }
}
let actions = {
    add(payload) {
        return { type: ADD, payload }
    }
}
const ConnectedCaculator = connect(
    state => ({ number: state.get('number') }),
    actions
)(Caculator)
ReactDOM.render(
    <Provider store={store}><ConnectedCaculator /></Provider>,
    document.getElementById('root')
)

但由于 Redux 中内置的 combineReducers 和 reducer 中的 initialState 都为原生的 Object 对象,所以不能和 Immutable 原生搭配使用,当然我们可以通过重写 combineReducers 的方式达到兼容效果,如下代码所示:

// 重写 redux 中的 combineReducers
function combineReducers(reducers) {
// initialState 初始化为一个 Immutable Map对象
return function (state = Map(), action) {
       let newState = Map();
       for (let key in reducers) {
            newState = newState.set(key, reducers[key](state.get(key), action));
       }
       return newState;
   }
}
let reducers = combineReducers({
   counter
});
const ConnectedCaculator = connect(
   state => {
       return ({ number: state.getIn(['counter', 'number']) })
   },
   actions
)(Caculator)

也可以通过引入 redux-immutable 中间件的方式实现 redux 与 Immutable 的搭配使用,对于使用 Redux 的应用程序来说,你的整个 state tree 应该是 Immutable.JS 对象,根本不需要使用普通的 JavaScript 对象。


5.使用 Immutable 需要注意的点

  • 不要混合普通的 JS 对象和 Immutable 对象 (不要把 Imuutable 对象作为Js对象的属性,或者反过来)。
  • 把整颗 Reudx 的 state 树作为 Immutable 对象。
  • 除了展示组件以外,其他地方都应该使用 Immutable 对象(提高效率,而展示组件是纯组件,不应该使用) 。
  • 少用 toJS 方法(这个方法操作非常耗性能,它会深度遍历数据转换成JS对象)。
  • 你的 Selector 应该永远返回 Immutable 对象 (即 mapStateToProps,因为 react-redux 中是通过浅比较来决定是否 re-redering,而使用 toJs 的话,每次都会返回一个新对象,即引用不同)。

6.总结

实际情况中有很多方法可以优化我们的 React 应用,例如延迟加载组件,使用 serviceWorks 缓存应用状态,使用 SSR 等,但在考虑优化之前,最好先理解 React 组件的工作原理,了解 Diff 算法,明白这些概念之后才能更好的针对性的去优化我们的应用。


作者:政采云前端团队
原文: 15 分钟学会 Immutable


链接: https://www.fly63.com/article/detial/10462

挑战常规--不要使用||赋予默认值

js是一种弱类型的编程语言,代表着传入的变量并不清楚作为何种类型使用。对js来说传入的任意参数都应该考虑不同类型的结果,而不是单单考虑一种情况。若传入0、false等,||所要实现默认值的功能完全错误的

YodaOS 中是如何生成 API 的

本文简单介绍了 YodaOS 在 API 设计过程中,如何利用 DSL,解决 YodaOS API 在多种应用形态保持一致性。以此,我们希望抛砖引玉:帮助读者更好地了解 YodaOS API 的生成过程,帮助读者了解到 DSL,也能将这种思路应用在自己的项目中

JavaScript 私有成员

JavaScript 一直没有私有成员并不是没有原因,所以这一提议给 JavaScript 带来了新的挑战。但同时,JavaScript 在 ES2015 发布的时候已经在考虑私有化的问题了,所以要实现私有成员也并非毫无基础。

web前端学习之路

对于程序员来说,如果哪一天开始他停止了学习,那么他的职业生涯便开始宣告消亡。这不是什么危言耸听的怪语,而是一位大牛几年前告诉我的,他的信条。

web前端开发好学吗?

随着互联网+时代的到来,移动互联网行业的发展也是突飞猛进。无论你是否承认,这个时代已经被网页所包围了,这所有一切,都是前端工程师的杰作。今天给大家聊的就是\"前端真的好学吗?\"

Web前端小白入门

Web前端开发怎么入门,主要都有哪些要素组成?Web前端开发是由网页制作演变而来的,主要由HTML、CSS、JavaScript三大要素组成。专业的Web前端开发入门知识也一定会包含这些内容,下面就给大家简单介绍一下。

highcharts 时间少8小时问题

Highcharts 中默认开启了UTC(世界标准时间),由于中国所在时区为+8,所以经过 Highcharts 的处理后会减去8个小时。如果不想使用 UTC,有2种方法可供使用:

巧妙利用引用,将数组转换成树形数组

笔者所做的一个项目需要做一个前端的树形菜单,后端返回的数据是一个平行的list,list中的每个元素都是一个对象,例如list[0]的值为{id: 1, fid: 0, name: 一级菜单},每个元素都指定了父元素,生成的菜单可以无限级嵌套

NodeJS/JWT/Vue 实现基于角色的授权

在本教程中,我们将完成一个关于如何在 Node.js 中 使用 JavaScript ,并结合 JWT 认证,实现基于角色(role based)授权/访问的简单例子。作为例子的 API 只有三个路由,以演示认证和基于角色的授权

forward和redirect的区别?http状态码301,302分别代表什么?

从地址栏显示来说:forward是服务器内部重定向,客户端浏览器的网址不会发生变化;redirect发生一个状态码,告诉服务器去重新请求那个网址,显示的的新的网址数据共享:forward使用的是同一个request

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!