更新复杂对象的噩梦

在 React 类组件中,state是一个对象,当组件相对复杂时,state对象的结构可能也相当复杂。

比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class User extends Component {
constructor(props) {
super(props);
// 初始化state
this.state = {
name: "freewheelLee",
gender: "male",
phone: "12345678",
address: {
country: 'China',
city: {
name: 'Shanghai',
area: 'PuDong',
postcode: 200000, // 邮编号码
},
}
};
}

// 其他代码
}

假如用户在这个组件中可以更改个人资料 —— 如更改某个输入框内容就能更改所在城市的地区和邮编号码, 正规的传统的标准的写法如下

1
2
3
4
5
6
7
8
9
10
11
12
this.setState((prevState) => {
return {
address: {
...prevState.address,
city: {
...prevState.address.city,
area: 'JingAn',
postcode: prevState.address.city.postcode + 10,
}
}
}
});

为什么得这么写?

在上面的情景中,传统的写法已经十分繁琐了,而且有明显 代码噪音 —— 尽管代码表达的是更改 area 和 postcode 却不得不看到大量的扩展语法

更糟糕的是,容易出错 —— 每深入对象一层,扩展语法后的路径也需要再进一层(如 …prevState.address.city) ,在复制粘贴过程中弄错/弄丢路径是屡见不鲜的。

当 state对象结构的层级更深 的时候,改动最深层的state子节点写起来会更麻烦。

另一个类似的且常见的场景是 Redux reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const tagReducer = function(state, action){
switch(action.type){
case UPDATE_TAG: {
return {
...state,
products: {
...state.products,
tag: {
...state.products.tag,
info: {
...state.products.tag.info,
value: action.payload
}
}
}
};
}
// 其他代码
}
}

为什么不能直接更改 reducer 的参数 state ?

reducer 必须是纯函数,不能更改参数state,而要计算并返回下一个state,所以不得不这么写。

简单总结一下目前讨论的困境 ——

当我们需要更新某一个复杂的js对象的某个深层属性(子节点),且不允许直接更改这个js对象,单纯借助ES6展开语法导致代码非常繁琐且易出错。

解决方案

我们可以想出一个简单的解决方案 —— 先深拷贝出一个新的对象,然后直接更改新对象的属性

1
2
3
4
5
6
7
this.setState((prevState) => {
const newState = deepClone(prevState);
newState.address.city.area = 'JingAn';
newState.address.city.postcode = newState.address.city.postcode + 10;
return newState;
}
});

但是,这种方案有明显的性能问题 —— 不管打算更新对象的哪一个属性(子节点),每次都不得不深拷贝整个对象;当对象特别大的时候,深拷贝会导致性能问题。

终于到主角 immer 出场了!

1
2
3
4
5
6
7
8
9
10
import {produce} from 'immer';

// 其他代码

this.setState((prevState) => {
return produce(prevState, draftState =>{
draftState.address.city.area = 'JingAn';
draftState.address.city.postcode = draftState.address.city.postcode + 10;
});
});

不够香?那试试这个写法

1
2
3
4
this.setState(produce(draftState => {
draftState.address.city.area = "JingAn";
draftState.address.city.postcode = draftState.address.city.postcode + 10;
}));

更重要的是,在 immer 的背后做了性能优化,而不是简单的全部深度拷贝,所以不用担心性能问题。

immer 还默认地把返回的新对象设置成了不可变对象,从而避免人为意外地直接修改。当然,如果不想要这个行为,可以调用 immer 的setAutoFreeze(false) 取消掉。