前言
刚刚使用 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 实际上是一个变量。
在实际项目中,通常会有层次很深且复杂的数据要进行处理,如果有一个很里层的数据要进行处理,这时就很头疼。我们常用有以下几种做法:
- 直接修改数据,上一个副本会被覆盖,无法确定哪些数据被更改。
myData.x.y.z = 7;
myData.a.b.push(9);
- 使用深拷贝,新建 myData 的副本,仅更改需要更改的部分。
const newData = deepCopy(myData);
newData.x.y.z = 7;
newData.a.b.push(9);
注意:深拷贝是很昂贵的,有的时候甚至是不可能的。
- 仅复制需要更改的对象和重新使用未更改的对象
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意为不变,不变性,永恒性。
这个轮子能做些什么?
在 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 。
- {$push: array}
向数组末尾添加一个或多个元素
const initialArray = [1, 2, 3]; // => [1, 2, 3]
const newArray = update(initialArray, {$push: [4]}); // => [1, 2, 3, 4]
- {$unshift: array}
在数组开头添加一或多个元素
const initialArray = [2, 3, 4]; // => [2, 3, 4]
const newArray = update(initialArray, {$unshift: [1]}); // => [1, 2, 3, 4]
- {$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]}]
- {$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']}
- {$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] }
- {$unset: array of strings}
从目标对象中删除数组中的键列表
const collection = [1, 2, 3, 4];
// => [1, 2, 3, 4]
const result = update(collection, {$unset: [1]});
// => [1, empty, 3, 4]
- {$merge: object}
合并对象
const obj = {a: 5, b: 3};
const newObj = update(obj, {$merge: {b: 6, c: 7}}); // => {a: 5, b: 6, c: 7}
- {$apply: function}
通过函数将一个值转为另外一个值
const obj = {a: 5, b: 3};
const newObj = update(obj, {b: {$apply: function(x) {return x * 2;}}});
// => {a: 5, b: 6}
- {$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"}
- {$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