immutability-helper

前言

刚刚使用 React 的小伙伴可能会遇到,明明更新了 state ,为什么不渲染呢?

一时不清楚为什么,就把所有的可能性都调试了一遍,还是没有任何头绪,后来通过一位资深的前端伙伴的帮忙终于找到了原因。

原来 state 状态的修改执行的是浅比较,换句话说就是只关注key 对应的 value 有没有变化,如果有变化才会重新渲染,否则不会做任何改动。

那如何检测 value 有没有变化呢?其实就是根据变量的地址。

在 JavaScript 中,简单类型的数据被设计为不可变,但是复杂类型如数组、对象则是可变的。也就是说我们无法保证在字符串内存地址不变的情况下改变字符串,但却可以保证在数组内存地址不变的情况下增加或删除数组的某一个元素。所以这也是 state 没有重新渲染的原因。现在我们来看个例子:

假如现在有数组 hobbies ,将 hobbies 赋值给 hobbies2,再将 hobbies2 push 进一个新的数据,这时会发生什么呢?

const hobbies = ['qq', 'wx']; // undefined
const hobbies2 = hobbies; // undefined
hobbies2.push('dd'); // 3
hobbies === hobbies2; // true

可以看出,hobbies 和 hobbies2 拥有同一个内存地址,也就是说 hobbies 和 hobbies2 实际上是一个变量。

在实际项目中,通常会有层次很深且复杂的数据要进行处理,如果有一个很里层的数据要进行处理,这时就很头疼。我们常用有以下几种做法:

  1. 直接修改数据,上一个副本会被覆盖,无法确定哪些数据被更改。
myData.x.y.z = 7;
myData.a.b.push(9);
  1. 使用深拷贝,新建 myData 的副本,仅更改需要更改的部分。
const newData = deepCopy(myData);
newData.x.y.z = 7;
newData.a.b.push(9);

注意:深拷贝是很昂贵的,有的时候甚至是不可能的。

  1. 仅复制需要更改的对象和重新使用未更改的对象
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)})
});

在现在的 JavaScript 中,这种写法有些麻烦,甚至可能会产生 bug 。

那么我们还有没有其它更简洁、适用的方法呢?答案是有的。

让我们进入今天的主题 immutability-helper ,在 React 的官网上也能看到它。

immutability-helper

immutability-helper

immutability意为不变,不变性,永恒性。

这个轮子能做些什么?

在 immutability-helper 的介绍页面,作者对它做了标注,mutate a copy of data without changing the original source,意为:在不改变原始来源的情况下改变数据副本。

现在我们使用 immutability-helper 的 update 来实现一下上面的功能:

import update from 'immutability-helper';

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

update 围绕上面 3 的模式提供了简单的语法糖,使编写代码更加容易。

接下来,我们看一下可用的命令。

Commands

以 $ 开头的称作 commands 。

  1. {$push: array}

向数组末尾添加一个或多个元素

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

在数组开头添加一或多个元素

const initialArray = [2, 3, 4]; // => [2, 3, 4]
const newArray = update(initialArray, {$unshift: [1]}); // => [1, 2, 3, 4]
  1. {$splice: array of arrays}

从数组中添加/删除元素

const collection = [1, 2, 12, 17, 15];
// => [1, 2, 12, 17, 15]
const newCollection = update(collection, {$splice: [[1, 1, 13, 14]]});
// => [1, 13, 14, 12, 17, 15]

const collection1 = [1, 2, {a: [12, 17, 15]}];
// => [1, 2, {a: [12, 17, 15]}]
const newCollection1 = update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}});
// => [1, 2, {a: [12, 13, 14, 15]}]
  1. {$set: any}

给对象某个元素赋值

const obj = {a: 5, b: 3};
const newObj2 = update(obj, {b: {$set: obj.b * 2}});
// => {a: 5, b: 6}

// 计算属性名称用 [] 包裹
const collection = {children: ['zero', 'one', 'two']};
const index = 1;
const newCollection = update(collection, {children: {[index]: {$set: 1}}});
// => {children: ['zero', 1, 'two']}
  1. {$toggle: array of strings}

切换目标对象的布尔字段列表

const origin = { isCat: [true, false, false] };
// => { isCat: [true, false, false] }
const result = update(origin, {isCat: {$toggle: [1]}});
// => { isCat: [true, true, false] }
  1. {$unset: array of strings}

从目标对象中删除数组中的键列表

const collection = [1, 2, 3, 4];
// => [1, 2, 3, 4]
const result = update(collection, {$unset: [1]});
// => [1, empty, 3, 4]
  1. {$merge: object}

合并对象

const obj = {a: 5, b: 3};
const newObj = update(obj, {$merge: {b: 6, c: 7}}); // => {a: 5, b: 6, c: 7}
  1. {$apply: function}

通过函数将一个值转为另外一个值

const obj = {a: 5, b: 3};
const newObj = update(obj, {b: {$apply: function(x) {return x * 2;}}});
// => {a: 5, b: 6}
  1. {$add: array of objects}

为 Map 或者 Set 添加值

const map = new Map([[1, 2], [3, 4]]);
// => Map(2) {1 => 2, 3 => 4}
const result = update(map, {$add: [['foo', 'bar'], ['baz', 'boo']]});
// => Map(4) {1 => 2, 3 => 4, "foo" => "bar", "baz" => "boo"}
  1. {$remove: array of strings}

从 Map 或者 Set 移除值

const map = new Map([[1, 2], [3, 4]]);
// => Map(2) {1 => 2, 3 => 4}
const result = update(map, {$remove: [1]});
// => Map(1) {3 => 4}
如果需要设置深层嵌套的内容,可以参考如下写法:
const initial = {}
const content = {
  foo: [
    {
      bar: ['x', 'y', 'z']
    },
  ],
};

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

console.log(JSON.stringify(result) === JSON.stringify(content)) // true
你也可以使用 extend 功能添加你自己的命令
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 }));

最后

不难看出,用了 immutability-helper 以后少写了很多不必要的代码,并且在处理复杂对象的时候要比用原生 API 修改,或者深拷贝一个新的对象优雅很多。immutability-helper 实现的功能还不仅仅只是这些,有兴趣可以自行研究一下源码。它也是一个被antd推荐使用的轮子。

总而言之,十分推荐在 React 中使用 immutability-helper 来进行 state 的更新,兼具性能与优雅。

github 链接:https://github.com/kolodny/immutability-helper

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容