为什么 React 需要 Immutable Data
简单来说为了让 React 精准地重渲染 UI。我们知道,在 React 中,UI 是 state 的投影,state 的变更会引发 UI 的重新渲染。React 使用 Virtual DOM 来解决 UI 更新的问题——它会将新旧两棵 Virtual DOM 树进行比较,如果两者存在差异,则它会将这些差异来更新在真实的 DOM 上。
调用setState时,React 会以 shallowMerge(浅层合并) 的方式将我们传入的对象与旧的 state 进行合并。shallowMerge 只会合并新旧 state 对象中第一层的内容,如果 state 中对象的引用未变,那么 React 认为这个对象前后没有发生变化。所以如果我们以 mutable 的方式更改了 state 中的某个对象, React 会认为该对象并没有更新,那么相对应的 UI 就不会被重渲染。而以 Immutable 的方式更新 state 就不会出现这个问题。
import React, { useState } from "react";
export default function App() {
const [list, setList] = useState([1, 2, 3]);
const addMutable = () => {
list.push("新数据");
setList(list);
};
const addImmutable = () => {
setList([...list, "新数据"]);
};
return (
<div className="App">
<button onClick={addMutable}>已可变的方式添加</button>
<button onClick={addImmutable}>已不可变的方式添加</button>
{list.map((item, index) => (<li key={index}>{item}</li>))}
</div>
);
}
redux / flux 要求采用返回新对象的形式,来触发数据更新、re-render,一般推荐的做法就是采用对象解构的方式。如果 state 对象巨大(注意:对象巨大),在结构、拷贝 state 的过程中,耗时会较长。
return {
...state,
settings: {
...state.settings,
profile:{
...state.settings.profile,
darkmode: true,
}
}
}
Immer:
**开源库实现思路:**原始对象先做了一层 Proxy 代理,得到 draftState 传递给 function。function(带副作用) 直接更改 draftState,最后 produce 返回新的对象
npm install immer
import React, { useState } from "react";
import produce from "immer";
export default function App() {
const [list, setList] = useState([1, 2, 3]);
const addMutable = () => {
list.push("新数据");
setList(list);
};
const addImmutable = () => {
/**
* 第一个参数是要代理的数据
* 第二个参数是一个函数
*/
const newVal = produce(list, draft => {
/**
* draft 相当于 list
* 在这个方法里面,可以直接修改draft,注意draft也只能在这个方法里面修改
* 不需要返回值,immer内部已经帮我处理好了
*/
draft.push('新数据')
})
console.log(newVal)
setList(newVal);
};
return (
<div className="App">
<button onClick={addMutable}>已可变的方式添加</button>
<button onClick={addImmutable}>已不可变的方式添加</button>
{list.map((item, index) => (<li key={index}>{item}</li>))}
</div>
);
}
让我们看一个示例,将 Immer 与 React 结合使用。
import produce from "immer";
this.state={
id: 14,
email: "stewie@familyguy.com",
profile: {
name: "Stewie Griffin",
bio: "You know, the... the novel you've been working on",
age:1
}
}
changeBioAge = () => {
this.setState(prevState => ({
profile: {
...prevState.profile,
age: prevState.profile.age + 1
}
}))
}
可以通过更改如下所示的状态来重构这段代码。
changeBioAge = () => {
this.setState(
produce(draft => {
draft.profile.age += 1
})
)
}
结合 Hooks 使用
Immer 的另一个重要特性是它支持 React Hooks。Immer 使用一个名为 use-immer 的附加库来实现此功能。让我们来看一个示例,以便更好地理解。
const [state, setState] = useState({
id: 14,
email: "stewie@familyguy.com",
profile: {
name: "Stewie Griffin",
bio: "You know, the... the novel you've been working on",
age:1
}
});
function changeBio(newBio) {
setState(current => ({
...current,
profile: {
...current.profile,
bio: newBio
}
}));
}
通过将 useState 替换为 useImmer Hook,我们可以进一步简化 Hooks 示例,还可以通过更改组件状态来更新 React 组件。
import { useImmer } from 'use-immer';
const [state, setState] = useImmer({
id: 14,
email: "stewie@familyguy.com",
profile: {
name: "Stewie Griffin",
bio: "You know, the... the novel you've been working on",
age:1
}
});
function changeBio(newBio) {
setState(draft => {
draft.profile.bio = newBio;
});
}
注:这里还有一点,useState是全覆盖,setState是合并