用它的原因是为了解决问题:解决项目当中状态大规模管理的深拷贝的问题(防止对象/数组因为引用传递而在使用过程中出现的各种问题)。
JS中数据修改问题
- 我们先来看一段熟悉的代码:
import React, { Component } from "react";
class App extends Component {
state = {
str: "CSDN",
obj: {
y: 1,
},
arr: [1, 2, 3],
};
componentDidMount() {
const newState = this.state;
console.log(newState === this.state);
}
render() {
return <div></div>;
}
}
export default App;
- 由于js的对象和数组都是引用类型。所以newState的state实际上是指向于同一块内存地址的, 所以结果是newState和state是相等的。
- 此时,我们尝试修改一下数据:
componentDidMount() {
const newState = this.state;
newState.str = "CSDNnb";
console.log(newState,this.state);
}
- 可以看到,newState的修改也会引起state的修改(对象是引用传递)。要解决这个问题,js中提供了另一种修改数据的方式,要修改一个数据之前先制作一份数据的拷贝,像这样:
componentDidMount() {
const newState = Object.assign({}, this.state);
newState.str = "CSDNnb";
console.log(newState,this.state);
}
- 我们可以使用很多方式在js中复制数据,比如:
Object.assign
,Object.freeze
,slice
,concat
,map
,filter
,reduce
等方式进行复制- 但这些都是浅拷贝,就是只拷贝第一层数据,更深层的数据还是同一个引用,比如:
componentDidMount() {
const newState = Object.assign({}, this.state);
newState.str = "CSDNnb";
newState.obj.y = 2;
newState.arr.push(4);
console.log(newState,this.state);
}
- 可以看到,当在更改
newState
更深层次的数据的时候,还是会影响到state
的值。如果要深层复制,就得一层一层的做递归拷贝,这是一个复杂的问题。虽然有些第三方的库已经帮我们做好了,但是深层复制是非常消耗性能的。那么这个问题如何解决呢?这就需要用到immutable.js
了。
介绍
- 项目地址:https://immutable-js.github.io/immutable-js/
- immutable.js出自Facebook,是最流行的不可变数据结构的实现之一。
不可变数据 (Immutable Data)就是一旦创建,就不能再被更改的数据。对Immutable对象的任何修改或添加删除操作都会返回一个新的Immutable对象。Immutable实现的原理是持久化数据结构(Persistent Data Structure),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免深层拷贝把所有节点都复制一遍带来的s性能损耗,Immutable使用了 结构共享(Structural Sharing),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享(不变)。
-
如果上面这张图不能直观的表现出变化,我们可以看下面这张图:
-
使用immutable.js的优缺点:
1. 降低mutable带来的复杂度
2. 节省内存
3. 历史追溯性(版本控制的感觉)。每时每刻的值都被保留了,想回退到哪一步只要简单的将数据取出就行。
4. 拥抱函数式编程。immutable本来就是函数式编程的概念,纯函数式编程的特点就是,只要输入一致,输出必然一致,相比于面向对象,这样开发组件和调试更方便。
- 缺点
1. 需要重新学习api(我们需要去查阅文档,增加了学习成本)
2. 资源包大小增加(源码大约5000行左右)
3. 容易与原生对象混淆:由于api与原生不同,混用的话容易出错
- 与其他包一样,
immutable.js
包默认没有被安装到react中,因此学习使用前需要先进行安装到项目:
npm i -S immutable
常用API
immutable.js提供了许多永久不可变数据结构,包括:
List
,Stack
,Map
,OrderedMap
,Set
,OrderedSet
和Record
。为了满足工作开发需求,至少需要掌握以下API(API信息全部来自官网,除了以下体现到的API外,更多API请访问官网):在使用的时候我们只是将其格式在两种之间来回转化,转化之后其就可以帮助我们解决深层拷贝的问题。(序列化与反序列化的感觉)
注意:当前还没有将immutable整合到框架,因此以下api测试的代码不要写在组件中。此处可以另起js文件,让这个js文件被入口文件包含即可。
object转Map对象
import { Map, is } from "immutable";
const state = {
id: 1,
name: "张三",
age: 22,
mobile: { public: "1300000000", private: "13333333333" },
};
const map1 = Map(state);
const map2 = Map(state);
// 比较上的差异
console.log(map1 === map2);
console.log(map1.equals(map2));
console.log(is(map1, map2));
// 获取上的差异
console.log(state.id);
console.log(state.mobile.private);
console.log(map1.get("id"));
console.log(map1.getIn(["mobile","private"]));
// 修改上的差异
state.name = "王二麻";
console.log(state);
const newMap1 = map1.set("name", "王二麻");
console.log(newMap1.get("name"));
console.log(newMap1.setIn(["mobile", "private"], "13800138000").getIn(["mobile", "private"]));
console.log(newMap1.update("age", (val) => val + 1).get("age"));
console.log(newMap1.updateIn(["mobile", "private"], () => "13888888888").getIn(["mobile", "private"]));
array转List对象
import { List } from "immutable";
const state = ["灞波儿奔", "奔波儿灞"];
let list = List(state);
// 获取
console.log(list.get(0));
// 合并
list = list.concat(["黑鱼精", "鲇鱼怪"]);
console.log(list.get(3));
// 追加
list = list.push("乱石山碧波潭");
console.log(list.get(4));
// 把List对象转成js数组
console.log(list.toArray());
JS转immutable
import { Map, is, fromJS } from "immutable";
const state = {
id: 1,
name: "张三",
age: 22,
mobile: { public: "1300000000", private: "13333333333" },
};
// 转immutable对象
const immutable = fromJS(state);
console.log(immutable);
// 获取
console.log(immutable.get("name"));
console.log(immutable.getIn(["mobile","private"]));
immutable转JS
import { Map, is, fromJS } from "immutable";
const state = {
id: 1,
name: "张三",
age: 22,
mobiles: { public: "1300000000", private: "13333333333" },
};
// 转immutable对象
const immutable = fromJS(state);
console.log(immutable.toJS());
- 注意:toJS方法不需要导入(本身就在map对象的原型上),导入了实际也不会被使用。
Redux中集成
- redux官网推荐使用redux-immutable进行redux和immutable的集成。
redux-immutable
包的安装命令如下:
npm i -S redux-immutable
- 以登录案例为例,步骤如下:
- (固定)将redux仓库创建入口文件中合并reducer的方法改成redux-immutable提供的合并方法(新方法与之前的是同名的combineReducers)
// 模块化操作
// combineReducers方法是redux中内置的,作用是合并多个reducer和数据源,参数是一个对象,对象中属性值是单个reducer,而其属性名有点类似于vuex模块化时的命名空间的概念
import { createStore, applyMiddleware, compose } from "redux";
// 导入redux-thunk
import thunk from "redux-thunk";
import counter from "./Reducers/counter";
import global from "./Reducers/global";
// 导入redux-immutable提供的合并reducer的方法
import { combineReducers } from 'redux-immutable'
// 解决插件报错的操作
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
// 合并多个reducer(整合数据源),不合并会报错
combineReducers({ counter, global }),
// 应用中间件
composeEnhancers(applyMiddleware(thunk))
// 必须要加上一段插件的配置工具,才能在浏览器中使用redux扩展
// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
- 将数据源设置成immutable对象
// 功能模块公用的数据源
// 注意,api都是immutable中提供的,不要导入错了包
import { fromJS } from "immutable";
const state = fromJS({
_token: "",
});
export default state;
- 在组件中使用的时候将其转成js对象
// 将数据源的数据映射成当前组件自身的props属性
function mapStateToProps(state) {
// console.log(state);
// 返回props对象
// 这里可以获取整个仓库的数据:state
// 也可以只要特定模块的数据,例如:state.global
return state.toJS().global;
}
如果说这里没有使用react-redux,
而是使用的之前的订阅的方式,
则需要在订阅那个位置做数据格式的转化。
- 在修改的时候使用immutable的api实现修改
// jwt的reducer
import defaultState from "../States/global";
function reducer(state = defaultState, actions) {
// 判断是否是设置token的操作
if (actions.type === "set_token") {
// return { ...state, _token: actions.payload };
console.log(actions);
// 修改之后返回新值,可以直接ruturn或者用变量接收再返回
return state.update("_token", () => actions.payload);
}
// 在返回之前写修改数据源的操作
return state;
}
export default reducer;