今天看到一篇文章,关于react re-render的原理,这里记录并学习一下
对于函数组件的re-render,大致分为一下三种情况:
- 组件本身使用useState 或者 useReducer 更新,引起的re-render;
- 父组件更新引起子组件的re-render;
- 组件本身使用了useContext,context更新引起的re-render;
1:组件本身使用useState 或者 useReducer 更新,引起的re-render
1.1 常规使用
以基数组件为例,每次点击add,都会打印"re-render",说明引起了re-render
import React, { useState } from 'react';
import './index.css';
export default function Counter() {
console.log('re-render');
const [count, addCount] = useState(0);
return (
<div className="counter">
<div className="counter-num">{count}</div>
<button
onClick={() => {
addCount(count + 1);
}}
>
add
</button>
</div>
);
}
1.2 immutation state
将上面的计数组件的state值改为引用类型,发现点击并不会引起re-render
import React, { useState } from 'react';
import './index.css';
export default function index() {
console.log('counter render');
const [count, addCount] = useState({ num: 0, time: Date.now() });
const clickHandler = () => {
count.num++;
count.time = Date.now();
addCount(count);
};
return (
<div className="counter">
<div className="counter-num">
{count.num}, {count.time}
</div>
<button onClick={clickHandler}>add</button>
</div>
);
}
这里能看到,只有在挂载阶段打印了re-render
再改一改: setState的时候改用参数形式,也会引发re-render
import React, { useState } from 'react';
import './index.css';
export default function index() {
console.log('counter render');
const [count, addCount] = useState({ num: 0, time: Date.now() });
const clickHandler = () => {
addCount((param) => ({ num: param.num + 1, time: Date.now() }));
};
return (
<div className="counter">
<div className="counter-num">
{count.num}, {count.time}
</div>
<button onClick={clickHandler}>add</button>
</div>
);
}
真实的原因在于,更新 state 的时候,会有一个新老 state 的比较,用的是 Object.is 进行比较,如果为 true 则直接返回不更新,源码如下(objectIs 会先判断 Object.is
是否支持,如果不支持则重新实现,eagerState 就是 oldState ):
if (objectIs(eagerState, currentState)) {
return;
}
所以更新 state 时候要注意,state 为不可变数据,每次需要一个新地址的值才会触发更新。
2:父组件更新引起子组件re-render
2.1、常规使用
现在稍微改造上面计数的组件,添加一个子组件 Hello
,如下点击会发现,每次都会输出 "hello render",也就是说,每次更新都引起了 Hello
的 re-render,但是其实 Hello
组件的属性根本就没有改变:
import React, { useState } from 'react';
import './index.css';
export default function index() {
const Hello = ({ name }) => {
console.log('hello render');
return <div style={{ color: 'green', fontSize: 24 }}>hello {name}</div>;
};
console.log('counter render');
const [count, addCount] = useState({ num: 0, time: Date.now() });
const clickHandler = () => {
addCount((param) => ({ num: param.num + 1, time: Date.now() }));
};
return (
<div className="counter">
<Hello name="react" />
<br />
<div className="counter-num">
{count.num}, {count.time}
</div>
<button onClick={clickHandler}>add</button>
</div>
);
}
对于这种不必要的re-reder,是有手段进行优化的
2.2、优化组件设计
2.2.1、将更新部分抽离成单独组件
import React, { useState } from 'react';
import './index.css';
export default function index() {
const Hello = ({ name }) => {
console.log('hello render');
return <div style={{ color: 'green', fontSize: 24 }}>hello {name}</div>;
};
return (
<div className="counter">
<Hello name="react" />
<Counter />
</div>
);
}
// 单独抽离成组件
const Counter = () => {
console.log('counter render');
const [count, addCount] = useState({ num: 0, time: Date.now() });
const clickHandler = () => {
addCount((param) => ({ num: param.num + 1, time: Date.now() }));
};
return (
<>
<div className="counter-num">
{count.num}, {count.time}
</div>
<button onClick={clickHandler}>add</button>
</>
);
};
2.3、React.memo
对于是否需要 re-render,类组件提供了两种方法:PureComponent
组件和 shouldComponentUpdate
生命周期方法。
对于函数组件来说,有一个 React.memo
方法,可以用来决定是否需要 re-render,如下我们将 Hello
组件 memo 化,这样点击更新数字的时候, Hello
组件是不会 re-render 的。除非 Hello
组件的 props 更新:
import React, { useState } from 'react';
import './index.css';
//React.memo
const Hello = React.memo(({ name }) => {
console.log('hello render');
return <div style={{ color: 'green', fontSize: 24 }}>hello {name}</div>;
});
export default function index() {
const [count, addCount] = useState(0);
console.log('counter render');
const clickHandler = () => {
addCount(count + 1);
};
return (
<div className="counter">
<Hello name="react" />
<div className="counter-num">{count}</div>
<button onClick={clickHandler}>add</button>
</div>
);
}
memo 方法的源码定义简略如下:
exportfunction memo<Props>(
type: React$ElementType, // react 自定义组件
compare?: (oldProps: Props, newProps: Props) => boolean, // 可选的比对函数,决定是否 re-render
) {
...
const elementType = {
$$typeof: REACT_MEMO_TYPE,
type,
compare: compare === undefined ? null : compare,
};
...
return elementType;
}
memo 的关键比对逻辑如下,如果有传入 compare 函数则使用 compare 函数决定是否需要 re-render,否则使用浅比较 shallowEqual
决定是否需要 re-render:
var compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
既然默认不传 compare 时,用的是浅对比,那么对于引用类的 props,就要注意了,尤其是事件处理的函数,如下,我们给 Hello
组件添加一个点击事件,这时我们发现每次点击计数,Hello
组件又开始 re-render 了:
import React, { useState } from 'react';
import './index.css';
//React.memo
const Hello = React.memo(({ name }) => {
console.log('hello render');
return <div style={{ color: 'green', fontSize: 24 }}>hello {name}</div>;
});
export default function index() {
const [count, addCount] = useState(0);
console.log('counter render');
const clickHandler = () => {
addCount(count + 1);
};
// Hello组件添加点击事件
const helloClick = () => {
console.log('Hello组件点击回调~~~');
};
return (
<div className="counter">
<Hello name="react" onClick={helloClick} />
<div className="counter-num">{count}</div>
<button onClick={clickHandler}>add</button>
</div>
);
}
这是因为每次点击计数,都会重新定义 clickHandler
处理函数,这样 shallowEqual
浅比较发现 onClick
属性值不同,于是将会进行 re-render。
2.3.1、useCallback
在上面的基础上,我们阔以用useCallback将定义的函数缓存起来,也不会触发re-render
import React, { useState } from 'react';
import './index.css';
//React.memo
const Hello = React.memo(({ name }) => {
console.log('hello render');
return <div style={{ color: 'green', fontSize: 24 }}>hello {name}</div>;
});
export default function index() {
const [count, addCount] = useState(0);
console.log('counter render');
const clickHandler = () => {
addCount(count + 1);
};
// 使用useCallback缓存
const helloClick = React.useCallback(() => {
console.log('Hello组件点击回调~~~');
}, []);
return (
<div className="counter">
<Hello name="react" onClick={helloClick} />
<div className="counter-num">{count}</div>
<button onClick={clickHandler}>add</button>
</div>
);
}
useCallback
的原理主要是在挂载的时候,将定义的 callback 函数及 deps 依赖挂载该 hook 的 memoizedState,当更新时,将依赖进行对比,如果依赖没变,则直接返回老的 callback 函数,否则则更新新的 callback 函数及依赖:
// 挂载时
function mountCallback(callback, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
// 更新时
function updateCallback(callback, deps) {
var hook = updateWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
var prevDeps = prevState[1];
// 如果依赖未变,则直接返回老的函数
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
// 否则更新新的 callback 函数
hook.memoizedState = [callback, nextDeps];
return callback;
}
看起来好像是没问题了,但是如果我们在刚才 callback 函数中使用了 count 这个 state 值呢?
// 使用useCallback缓存 并且使用count
const helloClick = React.useCallback(() => {
console.log('Hello组件点击回调~~~', 'count: ', count);
}, [count]);
当我们点击了几次计数,然后再点击 Hello
组件时,会发现我们打印的 count 还是挂载时候的值,而不是最新的 count 值。其实,这都是是闭包惹得祸(具体解释可参考:Be Aware of Stale Closures when Using React Hooks)。所以为了让 callback 函数中可以使用最新的 state,我们还要将该 state 放入 deps 依赖,但是这样依赖更新了,callback 函数也将会更新,于是 Hello
组件又将会 re-render,这又回到了从前。
这样我们得出了一个结论:
当 callback 函数需要使用 state 值时,如果是 state 值更新引起的更新,useCallback 其实是没有任何效果的。
其实react底层已经做了一些性能优化处理,就比如批量更新等等,但是我们在使用的时候也得注意下这些问题,
React和vue的另一个突出区别就是: vue底层已经最大限度的把性能优化给做了,不需要程序员去考虑性能问题,而React有一部分性能优化是留给了程序员,有没有这个感觉?