翻译|immutability-helper使用文档

3,186 阅读3分钟

本文基本等于直接翻译了immutability-helpergithub官方md,如果有兴趣也可直接点击链接跳转过去。

immutability-helper作为react-addons-update的一个替代,对于react中的状态使用可谓是帮助甚多,但是一直没有找到真正的官方文档以及中文文档,所以才出现了现在这篇文章。现阶段先翻译一个版本,后续会添加一些小的实例来充分介绍本模块的使用。

安装与使用:

npm install immutability-helper --save
// 或者...
yarn add immutability-helper

创建一个数据的复制且不会改变源数据

import update from 'immutability-helper';

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

简介:

React允许你使用任何你想要的数据管理样式,包括可变数据。但是,如果你可以在应用的性能关键部分使用不可变数据,那么很容易实现一个快速的shouldComponentUpdate()方法来显著提高应用程序的速度。

在JavaScript中处理不可变数据比使用Clojure这样的语言更困难。但是,我们提供了一个简单的不变性助手update(),它使处理这种类型的数据更加容易,而不会从根本上改变数据的表示方式。你也可以看看Facebook的Immutable.js以及React中使用不可变的数据结构(Using Immutable Data Structures)部分来获得关于Immutable.js的更多细节。

核心思想:

当你像这样使用可变数据:

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

当上一个副本被覆盖时,你将无法确定哪一个数据被更改。相反,你需要创建一个新的myData副本并且只修改需要被修改的部分。这时你可以在shouldComponentUpdate()中使用全等来对比新数据和myData的旧副本:

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

不幸的是,深拷贝不仅代价昂贵,有时甚至是不可能使用的。你可以通过只复制需要更改的对象和重用未更改的对象来缓解这种情况。不幸的是,在当下的JavaScript中,这可能很麻烦:

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

虽然这样性能相当之高(因为它只生成log n个对象的浅拷贝以及重用其余对象),但是编写它会带来很大的痛苦。看看所有的重复部分!这不仅让人厌恶,同时还为出现bug提供了大面积的代码。

update()

update()围绕此种模式提供了更简单的语法糖,使编写此种代码更加容易。代码变为:

import update from 'immutability-helper';

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

虽然需要一定的时间来适应语法(即使它是受MongoDB的查询语句启发),但它没有冗余代码,它是静态分析的并且对比可变属性版本并没有多太多类型。

带有$前缀的键被称为命令。所"变异"的数据结构被称为目标。

可用命令

  • {$push: array} 效果如同调用push()方法 将array中的所有项加入到目标后面。
  • {$unshift: array} 效果如同调用unshift()方法 将array中的所有项加入到目标前面。
  • {$splice: array of arrays} 循环arrays,使用它的每一项作为参数来调用splice()方法。注意:**arrays中的项是按照顺序依次调用的,因此他们的顺序很重要。在操作过程中目标的下标可能会发生变化。
  • {$set: any} 完全替代目标的值
  • {$toggle: array of strings} 依据数组中的每一项对对象中的布尔值进行取反。
  • {$unset: array of strings} 根据array中的键值移除目标对象中的属性。
  • {$merge: object}object的键值合并入目标中。
  • {$apply: function} 将当前值作为参数传递给函数,并将函数的返回值作为目标的新值
  • {$add: array of objects}MapSet添加值(不支持WeakMapWeakSet)。当向Set添加时你需要传入一个由任意值组成的数组来操作, 当向Map添加时,你需要传入一个结构为[key, value]格式的数组就像这样:update(myMap, {$add: [['foo', 'bar'], ['baz', 'boo']]})
  • {$remove: array of strings} 依据列表种的键名对MapSet中的值进行删除。
$apply的简写

此外,你可以传入一个函数而不是传入命令对象,他会被视为使用了$apply命令的命令对象:update({a: 1}, {a: function})。这个例子等同于update({a: 1}, {a: {$apply: function}})

局限性

⚠️ update 只适用于 data properties,而不适用于通过 Object.defineProperty定义的 accessor properties . 他可能会看不到后者,从而导致创建了隐藏数据属性,他可能会由于setter的副作用导致应用程序逻辑被破坏。因此, update只能应用于由 data properties作为子集的纯数据对象。

举个🌰

简易push
const initialArray = [1, 2, 3];
const newArray = update(initialArray, {$push: [4]}); // => [1, 2, 3, 4]

initialArray 的值仍然是 [1, 2, 3]

嵌套集合
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]}]

这里的入口为collection索引值为2,键名为a的项,起始下标为1(移除值为17的项)并插入1314两个值。

基于当前值进行更新
const obj = {a: 5, b: 3};
const newObj = update(obj, {b: {$apply: function(x) {return x * 2;}}});
// => {a: 5, b: 6}
// 这里效果等同,但是对于深层嵌套会显得非常冗余:
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}
计算属性名称

可以通过ES2015的 计算属性名称(Computed Property Names)功能通过运行时变量来使用索引值。对象属性名表达式可以通过包裹一个方括号[]来,它会在运行时获取到值并作为最终的属性名。

const collection = {children: ['zero', 'one', 'two']};
const index = 1;
const newCollection = update(collection, {children: {[index]: {$set: 1}}});
// => {children: ['zero', 1, 'two']}
从数组中移除元素
// 删除一个特定索引值的项,无论他是什么值
update(state, { items: { $splice: [[index, 1]] } });

Autovivification

Autovivification是一种在需要时自动创建新的数组与对象的创造器。对于JavaScript上下文中,这意味着类似于这样:

const state = {}
state.a.b.c = 1; // state应该等同于 { a: { b: { c: 1 } } }

因为JavaScript没有这种“特性”,所以在immutability-helper也是如此。在JavaScript及immutability-helper扩展当中几乎不可能实现的原因如下:

var state = {}
state.thing[0] = 'foo' // state.thing应该是什么类型? 他应该是数组还是对象?
state.thing2[1] = 'foo2' // 那么thing2呢? 他肯定应该是一个对象!
state.thing3 = ['thing3'] // 这就是普通的js语法, 他并没有基于autovivification运行
state.thing3[1] = 'foo3' // emmmmm, 还记得state.thing2是一个对象, 但这里是一个数组
state.thing2.slice // 应该是undefined
state.thing2.slice // 应该是function

如果你需要对一个深层嵌套的内容进行赋值,并且不想一层层赋值下来,请考虑这个技术,这里有一个小案例:

var state = {}
var desiredState = {
  foo: [
    {
      bar: ['x', 'y', 'z']
    },
  ],
};

const state2 = update(state, {
  foo: foo =>
    update(foo || [], {
      0: fooZero =>
        update(fooZero || {}, {
          bar: bar => update(bar || [], { $push: ["x", "y", "z"] })
        })
    })
});

console.log(JSON.stringify(state2) === JSON.stringify(desiredState)) // true
// 注意state可以被声明为以下任意一种,并且仍然会输出true:
// var state = { foo: [] }
// var state = { foo: [ {} ] }
// var state = { foo: [ {bar: []} ] }

你也可以选择使用扩展功能来添加$auto$autoArray命令:

import update, { extend } from 'immutability-helper';

extend('$auto', function(value, object) {
  return object ?
    update(object, value):
    update({}, value);
});
extend('$autoArray', function(value, object) {
  return object ?
    update(object, value):
    update([], value);
});

var state = {}
var desiredState = {
  foo: [
    {
      bar: ['x', 'y', 'z']
    },
  ],
};
var state2 = update(state, {
  foo: {$autoArray: {
    0: {$auto: {
      bar: {$autoArray: {$push: ['x', 'y', 'z']}}
    }}
  }}
});
console.log(JSON.stringify(state2) === JSON.stringify(desiredState)) // true

添加自定义命令

此模块与react-addons-update最主要的区别在于本模块支持你扩展更多的命令:

import update, { extend } from 'immutability-helper';

extend('$addtax', function(tax, original) {
  return original + (tax * original);
});
const state = { price: 123 };
const withTax = update(state, {
  price: {$addtax: 0.8},
});
assert(JSON.stringify(withTax) === JSON.stringify({ price: 221.4 }));

请注意,上面函数中的original是原始对象,因此如果你希望他作为可变数据使用,你必须先对该对象进行浅拷贝。另一个方案是通过update来让返回变为update(original, {foo:{$set:{bar}}})

如果你不想和全局导出的update函数搞混,你可以自己创建一个副本并使用它进行工作:

import { Context } from 'immutability-helper';

const myContext = new Context();

myContext.extend('$foo', function(value, original) {
  return 'foo!';
});

myContext.update(/* args */);