不可变操作

不可变操作

# JavaScript Immutablitiy

React 开发中的一大痛点就是对于状态的处理与更新,譬如在我们需要创建编辑用户信息的表单时,往往我们会创建单一的响应处理函数来处理表单数据的变更,其形式可能如下:

updateState(event) {
 const {name, value} = event.target;
 let user = this.state.user;
 user[name] = value;
 return this.setState({user});
}

不过这种方式算是典型的错误,其并不能实际地触发页面重渲染;这是因为变量 user 只是对于状态的引用,React 默认的更新机制采用了浅比较,因此并不会判断对象的属性值是否发生了变化。React 官方文档中建议我们将 this.state 当做不可变数据对待,我们不应该直接修改 this.state 中的某个状态的属性值,而是应该像纯函数那样构造出新的对象;本部分即是讨论几种 React 开发中常见的不可变数据结构的操作方法。

原生不可变性实现

Object.assign 能够创建对象的拷贝,首个参数为需要拷贝的目标,我们往往会传入新创建的空对象;而后续的参数表示拷贝的其他来源。利用 Object.assign 复写的上述代码为:

updateState(event) {
 const {name, value} = event.target;
 let user = Object.assign({}, this.state.user);
 user[name] = value;
 return this.setState({user});
}

通过 Object.assign 我们创建了原本 user 对象的数据拷贝,这样我们直接操作新对象的属性然后调用 setState 函数就能够正确地触发界面重渲染了。不过我们需要注意的是Object.assign 只是一层浅复制,在某些情况下有可能会造成数据的异常改变;另外 IE 中并不支持 Object.assign,如果我们的部署环境包括 IE 那么需要引入 object-assign 这样的垫片。除了 Object.assign,我们还可以使用对象的扩展操作符来创建新的对象:

updateState(event) {
 const {name, value} = event.target;
 let user = {...this.state.user, [name]: value};
 this.setState({user});
}

扩展操作符能够自动将目标对象展开,将原属性复制到新对象中,其相较于 Object.assign 不需要额外的垫片(Babel 能够自动转化),并且代码也更为优雅。我们同样可以合并使用解构与扩展操作符:

updateState({target}) {
 this.setState({user: {...this.state.user, [target.name]: target.value}});
}

## immutability-helper

immutability-helper 是早期 React 内置的 react-addons-update 的替代,它能够帮助开发者修个某个对象深层嵌套的属性并且返回新的对象。其基本引入方式为:

// import update from 'react-addons-update';
import update from "immutability-helper";

const state1 = ["x"];
const state2 = update(state1, { $push: ["y"] }); // ['x', 'y']

在上文中我们强调过 Object.assign 与扩展操作符都是一层浅复制,如果我们的数据结构嵌套层次较深时,我们就比较麻烦地去修改某个内部属性的值,譬如:

myData.x.y.z = 7;
// or...
myData.a.b.push(9);

我们在上文中讨论过,直接修改某个对象的内部属性并不会影响到该对象的引用值;基本的做法应该是去创建 myData 的拷贝然后改变需要修改的部分:

const newData = deepCopy(myData);
newData.x.y.z = 7;
newData.a.b.push(9);

不过深层拷贝的性能耗费往往较大,并且在某些包含嵌套的环境下并不现实;我们往往会选择仅拷贝需要更改的部分然后重用为更改的部分,不过这种方式在原生的 JavaScript 中往往会较为复杂:

const newData = extend(myData, {
  x: extend(myData.x, {
    y: extend(myData.x.y, { z: 7 })
  }),
  a: extend(myData.a, { b: myData.a.b.concat(9) })
});

这种方式的性能损耗也较大,而 immutability-helper 正是提供了用于简化数据修改的语法糖,从而使得对象修改变得更为容易:

import update from "immutability-helper";

const newData = update(myData, {
  x: { y: { z: { $set: 7 } } },
  a: { b: { $push: [9] } }
});

该语法借鉴了 MongoDB 中查询语言的模式,以 $ 为前缀的键被称为命令,而待修改的对象称为目标,这里我们讨论几种常见的用法:

  • 简单的添加数据
const initialArray = [1, 2, 3];
const newArray = update(initialArray, {$push: [4]}); // => [1, 2, 3, 4]
  • 嵌套数组的切割
const collection = [1, 2, {a: [12, 17, 15]}];
const newCollection = update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}});
// => [1, 2, {a: [12, 13, 14, 15]}]
  • 根据现有值进行更新
const obj = { a: 5, b: 3 };
const newObj = update(obj, {
  b: {
    $apply: function(x) {
      return x * 2;
    }
  }
});
// => {a: 5, b: 6}
// This is equivalent, but gets verbose for deeply nested collections:
const newObj2 = update(obj, { b: { $set: obj.b * 2 } });
  • 对象合并
const obj = { a: 5, b: 3 };
const newObj = update(obj, { $merge: { b: 6, c: 7 } }); // => {a: 5, b: 6, c: 7}

## ImmutableJS

上一页