const App = () => {
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState('Tom');
React.useEffect(() => {
setInterval(() => {
setCount(s => s + 1);
}, 1000);
}, []);
return (
<h3>count: {count}</h3>
<Child name={name} />
function Child(props: { name: string }) {
console.log('child render', props.name);
return <div>name: {props.name}</div>;
React 对这种行为可以说是不负责任,不管你三七二十八,只要父组件渲染,所有它下面的子组件都会渲染,这种方式简单而粗暴。
既然如此,那么这块只能我们这些负责任的开发者去优化了。
浅比较优化
React 把优化问题抛给开发者,它给我们提供了三个用于性能优化的 API。
React.PurComponent
用于 class 写法的组件,它会对传入组件的 props 进行浅比较,如果比较的结果相同,则不会去渲染组件
React.memo
与 React.PurComponent 一样,用于函数组件
在 js 中,数据主要分两种:
基本类型(字符串、数字、undefined...),
引用类型,即对象(JSON、Array、Function、Regex...)
基本类型的数据属于原始值(primitive value),它直接存储在栈内存中,
引用类型的数据存在于堆内存中,通过存在栈内存中的指针来调用它
原始数据是 immutable 的,引用类型(object)一般是 mutable 的
原始数据比较直接通过值比较,而 object 则通过引用比较
var a = 1;
var b = 1;
a === b // true
var a = {};
var b = {};
a === b // false 比较的是引用 所以不相等
对于对象,不仅有引用比较,也有深比较和浅比较
const a = {num: 1};
const b = {num: 1};
a === b // false 引用不相等
a.num === b.num // true 对象的一级相等
const a = { p: {num: 1}, t: 2 }
const b = { p: {num: 1}, t: 2 }
a === b // false 引用不相等
shallowEqual(a, b) // false a.p === b.p 引用不等 浅比较为false
deepEqual(a, b) // true 深比较相等
如果对象的一级属性中存在引用比较,则不相等。
对象的深比较跳过了引用比较,仅仅是比较相同层级下的属性值。
由于对象存在于堆内存中 通过栈内存中的引用来调用,所以如果对象的值不变,对象的引用变了,就会导致 React 组件缓存失败,造成组件无必要的渲染,进而会造成性能问题
如果对象值变,引用不变,React 则不触发渲染,导致界面与数据不一致
React.memo 可以让 props 在变化时,该组件才会发生有意义的重新渲染
我们将子组件用 React.memo 包起来,只要 props 不变,在父组件更新时,子组件也不会重渲染
const Child = React.memo((props: { name: string }) => {
console.log('child render', props.name);
return <div>name: {props.name}</div>;
});
看起来问题解决了,React.memo 将前后的 props 进行浅比较,这基本能解决大多数问题。但如果 props 中含有对象数据,在浅比较时比较的是引用,这种方式就行不通了,好在 React.memo 提供了第二个参数,可以自定义比较前后的 props
浅比较行不通,那么深比较呢
const Child = React.memo((props) => {
return <div>{props.name}</div>;
}, (prev, next) => {
// 深比较
return deepEqual(prev,next)
});
这样的确可以达到效果,但如果是比较复杂的对象,就会存在较大的性能问题,甚至直接挂掉,因此不建议使用深比较去进行性能优化
还有一种方式:如果能保证对象的值相等,再保证对象的引用相等,就可以保证子组件在 props 没变的情况下不会渲染。
React.useRef 的返回值是固定的常量,我们可以使用它来做为对象的引用
const Child = React.memo(({ obj }) => {
console.log('child render', obj, obj.name);
return <div>name: {obj.name}</div>;
});
const App = () => {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setCount(s => s + 1);
}, 1000);
}, []);
const obj = React.useRef({
name: 'Jack'
});
return (
<h3>count: {count}</h3>
<Child obj={obj.current} />
这样还是存在一个严重的问题:如果 name 改变了,但 obj 没有变,导致子组件不会重新渲染,数据与UI界面不一致。 看来 useRef 只能用于常量
那我们只要保证 name 不变的时候 obj 和上次一样, name 才让子组件更新就可以了。没错,就是 useMemo。
useMemo 的特性就是保证依项不变时,对应的对象也不会变,只有依赖项变化时,对应的对象才会变
const App = () => {
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState('');
React.useEffect(() => {
setInterval(() => {
setCount(s => s + 1);
}, 1000);
}, []);
// 只有当 name 变化时,obj才会变化
const obj = React.useMemo(
() => ({
}),
[name]
return (
<h3>count: {count}</h3>
<input
type="text"
value={name}
onChange={e => {
setName(e.target.value);
<Child obj={obj} />
immutable
上面的方式算是一种解决方案,现在我们来看看其他的东西。
我们在 class 组件中更新状态,只需要一个 setState 就行,不管你传入什么 state,组件都会刷新
// class 的 setState 传什么都会更新组件
this.setState({
name: 'Jack'
但在 hooks 里, 如果前后两次的引用相等,就不会更新组件
const [state, setState] = useState({})
// 同一个引用,不会更新
setState(s => {
s.name = 'Tom'
return s
// 生成新应用,可以更新
setState(s => {
const newState = {
...s,
name: 'Tom'
return newState;
在 hooks 中,如果你想修改状态对象,必须保证前后修改的对象引用不等。这就要求我们不能直接更新老的 state,而是要保持老的 state 不变,去生成一个新的 state,也就是 immutable 方式。
老的 state 保持不变,也就意味着该 state 应该是 immutable obj
const state = [
name: 'Tom',
age: 20
name: 'Jack',
age: 30
state[0].name = 'Alen'
// hooks中需要如下写法
const newState = [
...state[0],
name: 'Alen'
...state
immutable 的写法过于繁琐,这不是我们想要的
其实,综上来说,我们的需求很简单:
需要改变状态
改变状态后和之前的状态引用不相等
第一个冲上脑门的答案就是:先深拷贝,然后做 mutable 修改就可以了
const state = [
name: 'Tom',
age: 20
name: 'Jack',
age: 30
const newState = deepClone(state);
newState[0].name = 'Alen'
深拷贝有两个缺点:
拷贝的性能问题
对于循环引用的处理
虽然市面上有些库支持高性能的深拷贝,但会对参考物对等(reference equality )造成了破坏
const state = [
name: 'Tom',
age: 20
name: 'Jack',
age: 30
const newState = lodash.cloneDeep(state)
state === newState // false
state[0] === newState[0] // false
可以发现,所有对象的结构都被破坏,在 React 中使用这种方式,即使对象属性没有任何变化,也会导致没有意义的重新渲染,仍会导致比较严重的性能问题
深拷贝是非常糟糕的
这么看来,更新状态还要其他的需求。
我们将 oldState 视为一个属性树,改变其中某节点时,能返回一个新的对象
当前节点及其组件节点的引用在新老 state 中不相等,这样能保证UI组件即时刷新
非当前节点及其祖先节点的引用在新老 state 中保持引用相等,这能保证状态不变时组件不重渲染
遗憾的是,JavaScript 并没有内置这种对 immutable 数据的支持,更不用说对 immutable 数据更新了,但可以使用一些三方库来解决这个问题,比如:immer 和 immutablejs
import Immutable from 'immutable'
var state = Immutable.Map({
a: 1,
b: 2
var newState = state.set('a', 3)
Immutable 数据使用结构共享的方式,只更新修改了子节点的引用,不会去修改未更改的子节点引用,达到我们想要的需求
默认情况下,React 组件更新会触发其下面所有的子组件递归渲染
通过 React.memo 的浅比较 props,来保证 props 不变的情况下,组件不会刷新
浅比较只对基本类型(primitive)生效,对于对象无效,即使对象中的值不变,也会引发重渲染
通过使用 useRef 和 useMemo 来缓存对象,在对象值没变时不用引发重渲染
不能通过深拷贝的方式修改 state,不仅导致性能问题,还会引起即使 state 内的值没有变化,但引用发生变化,从而造成无意义渲染,引发严重性能问题
这要求我们使用 immutable 的方式更新 state,来保证引用和缓存
可以通过第三方库:immer、immutablejs 来简化 immutable 的 state 更新写法