一 React 几种控制 render 方法
说到对render 的控制,究其本质,主要有以下两种方式:
- 第一种就是从父组件直接隔断子组件的渲染,经典的就是 memo,缓存 element 对象。
- 第二种就是组件从自身来控制是否 render ,比如:PureComponent ,shouldComponentUpdate
1 缓存React.element对象
useMemo 用法:
const cacheSomething = useMemo(create,deps)
create:第一个参数为一个函数,函数的返回值作为缓存值,如上 demo 中把 Children 对应的 element 对象,缓存起来。deps: 第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。cacheSomething:返回值,执行 create 的返回值。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。
useMemo原理: useMemo 会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在,但是 deps 中如果有一项改变,就会重新执行 create ,返回值作为新的值记录到 fiber 对象上。
useMemo应用场景:
- 可以缓存 element 对象,从而达到按条件渲染组件,优化性能的作用。
- 如果组件中不期望每次 render 都重新计算一些值,可以利用 useMemo 把它缓存起来。
- 可以把函数和属性缓存起来,作为 PureComponent 的绑定方法,或者配合其他Hooks一起使用。
原理揭秘
如上讲了利用 element 的缓存,实现了控制子组件不必要的渲染,究其原理是什么呢?
原理其实很简单,上述每次执行 render 本质上 createElement 会产生一个新的 props,这个 props 将作为对应 fiber 的 pendingProps ,在此 fiber 更新调和阶段,React 会对比 fiber 上老 oldProps 和新的 newProp ( pendingProps )是否相等,如果相等函数组件就会放弃子组件的调和更新,从而子组件不会重新渲染;如果上述把 element 对象缓存起来,上面 props 也就和 fiber 上 oldProps 指向相同的内存空间,也就是相等,从而跳过了本次更新。
2 PureComponent
/* 纯组件本身 */
class Children extends React.PureComponent{
state={
name:'alien',
age:18,
obj:{
number:1,
}
}
changeObjNumber=()=>{
const { obj } = this.state
obj.number++
this.setState({ obj })
}
render(){
console.log('组件渲染')
return <div >
<div> 组件本身改变state </div>
<button onClick={() => this.setState({ name:'alien' }) } >state相同情况</button>
<button onClick={() => this.setState({ age:this.state.age + 1 }) }>state不同情况</button>
<button onClick={ this.changeObjNumber } >state为引用数据类型时候</button>
<div>hello,my name is alien,let us learn React!</div>
</div>
}
}
/* 父组件 */
export default function Home (){
const [ numberA , setNumberA ] = React.useState(0)
const [ numberB , setNumberB ] = React.useState(0)
return <div>
<div> 父组件改变props </div>
<button onClick={ ()=> setNumberA(numberA + 1) } >改变numberA</button>
<button onClick={ ()=> setNumberB(numberB + 1) } >改变numberB</button>
<Children number={numberA} />
</div>
}
- 对于 props ,PureComponent 会浅比较 props 是否发生改变,再决定是否渲染组件,所以只有点击 numberA 才会促使组件重新渲染。
- 对于 state ,如上也会浅比较处理,当上述触发 ‘ state 相同情况’ 按钮时,组件没有渲染。
- 浅比较只会比较基础数据类型,对于引用类型,比如 demo 中 state 的 obj ,单纯的改变 obj 下属性是不会促使组件更新的,因为浅比较两次 obj 还是指向同一个内存空间,想要解决这个问题也容易,浅拷贝就可以解决,将如上 changeObjNumber 这么修改。这样就是重新创建了一个 obj ,所以浅比较会不相等,组件就会更新了。
PureComponent 原理及其浅比较原则
PureComponent 内部是如何工作的呢,首先当选择基于 PureComponent 继承的组件。原型链上会有 isPureReactComponent 属性。一起看一下创建 PureComponent 时候:
/* pureComponentPrototype 纯组件构造函数的 prototype 对象,绑定isPureReactComponent 属性。 */ pureComponentPrototype.isPureReactComponent = true;
isPureReactComponent 这个属性在更新组件 updateClassInstance 方法中使用的,
在这个函数内部,有一个专门负责检查是否更新的函数 checkShouldComponentUpdate 。
function checkShouldComponentUpdate(){
if (typeof instance.shouldComponentUpdate === 'function') {
return instance.shouldComponentUpdate(newProps,newState,nextContext) /* shouldComponentUpdate 逻辑 */
}
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
}
}
- isPureReactComponent 就是判断当前组件是不是纯组件的,如果是 PureComponent 会浅比较 props 和 state 是否相等。
- 还有一点值得注意的就是 shouldComponentUpdate 的权重,会大于 PureComponent。
- shallowEqual 是如何浅比较的呢,由于我不想在章节中写过多的源码,我在这里就直接描述过程了。
shallowEqual 浅比较流程:
- 第一步,首先会直接比较新老 props 或者新老 state 是否相等。如果相等那么不更新组件。
- 第二步,判断新老 state 或者 props ,有不是对象或者为 null 的,那么直接返回 false ,更新组件。
- 第三步,通过 Object.keys 将新老 props 或者新老 state 的属性名 key 变成数组,判断数组的长度是否相等,如果不相等,证明有属性增加或者减少,那么更新组件。
- 第四步,遍历老 props 或者老 state ,判断对应的新 props 或新 state ,有没有与之对应并且相等的(这个相等是浅比较),如果有一个不对应或者不相等,那么直接返回 false ,更新组件。
到此为止,浅比较流程结束, PureComponent 就是这么做渲染节流优化的。
PureComponent注意事项
PureComponent 可以让组件自发的做一层性能上的调优,但是,父组件给是 PureComponent 的子组件绑定事件要格外小心,避免两种情况发生:
1 避免使用箭头函数。不要给是 PureComponent 子组件绑定箭头函数,因为父组件每一次 render ,如果是箭头函数绑定的话,都会重新生成一个新的箭头函数, PureComponent 对比新老 props 时候,因为是新的函数,所以会判断不想等,而让组件直接渲染,PureComponent 作用终会失效。
2 PureComponent 的父组件是函数组件的情况,绑定函数要用 useCallback 或者 useMemo 处理。这种情况还是很容易发生的,就是在用 class + function 组件开发项目的时候,如果父组件是函数,子组件是 PureComponent ,那么绑定函数要小心,因为函数组件每一次执行,如果不处理,还会声明一个新的函数,所以 PureComponent 对比同样会失效,如下情况:
class Index extends React.PureComponent{}
export default function (){
const callback = function handerCallback(){} /* 每一次函数组件执行重新声明一个新的callback,PureComponent浅比较会认为不想等,促使组件更新 */
return <Index callback={callback} />
}
综上可以用 useCallback 或者 useMemo 解决这个问题,useCallback 首选,这个 hooks 初衷就是为了解决这种情况的。
export default function (){
const callback = React.useCallback(function handerCallback(){},[])
return <Index callback={callback} />
}
useCallback 接受二个参数,第一个参数就是需要缓存的函数,第二个参数为deps, deps 中依赖项改变返回新的函数。如上处理之后,就能从根本上解决 PureComponent 失效问题。
useCallback 和 useMemo 有什么区别?
答:useCallback 第一个参数就是缓存的内容,useMemo 需要执行第一个函数,返回值为缓存的内容,比起 useCallback , useMemo 更像是缓存了一段逻辑,或者说执行这段逻辑获取的结果。那么对于缓存 element 用 useCallback 可以吗,答案是当然可以了。
3 shouldComponentUpdate
有的时候,把控制渲染,性能调优交给 React 组件本身处理显然是靠不住的,React 需要提供给使用者一种更灵活配置的自定义渲染方案,使用者可以自己决定是否更新当前组件,shouldComponentUpdate 就能达到这种效果。在生命周期章节介绍了 shouldComponentUpdate 的用法,接下来试一下 shouldComponentUpdate 如何使用。
class Index extends React.Component{ //子组件
state={
stateNumA:0,
stateNumB:0
}
shouldComponentUpdate(newProp,newState,newContext){
if(newProp.propsNumA !== this.props.propsNumA || newState.stateNumA !== this.state.stateNumA ){
return true /* 只有当 props 中 propsNumA 和 state 中 stateNumA 变化时,更新组件 */
}
return false
}
render(){
console.log('组件渲染')
const { stateNumA ,stateNumB } = this.state
return <div>
<button onClick={ ()=> this.setState({ stateNumA: stateNumA + 1 }) } >改变state中numA</button>
<button onClick={ ()=> this.setState({ stateNumB: stateNumB + 1 }) } >改变stata中numB</button>
<div>hello,let us learn React!</div>
</div>
}
}
export default function Home(){ // 父组件
const [ numberA , setNumberA ] = React.useState(0)
const [ numberB , setNumberB ] = React.useState(0)
return <div>
<button onClick={ ()=> setNumberA(numberA + 1) } >改变props中numA</button>
<button onClick={ ()=> setNumberB(numberB + 1) } >改变props中numB</button>
<Index propsNumA={numberA} propsNumB={numberB} />
</div>
}
shouldComponentUpdate 可以根据传入的新的 props 和 state ,或者 newContext 来确定是否更新组件,如上面例子🌰,只有当 props 中 propsNumA 属性和 state 中 stateNumA 改变的时候,组件才渲染。但是有一种情况就是如果子组件的 props 是引用数据类型,比如 object ,还是不能直观比较是否相等。那么如果想有对比新老属性相等,怎么对比呢,而且很多情况下,组件中数据可能来源于服务端交互,对于属性结构是未知的。
immutable.js 可以解决此问题,immutable.js 不可变的状态,对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。鉴于这个功能,所以可以把需要对比的 props 或者 state 数据变成 Immutable 对象,通过对比 Immutable 是否相等,来证明状态是否改变,从而确定是否更新组件。
4 React.memo
React.memo(Component,compare)
React.memo 可作为一种容器化的控制渲染方案,可以对比 props 变化,来决定是否渲染组件,首先先来看一下 memo 的基本用法。React.memo 接受两个参数,第一个参数 Component 原始组件本身,第二个参数 compare 是一个函数,可以根据一次更新中 props 是否相同决定原始组件是否重新渲染。
memo的几个特点是:
- React.memo: 第二个参数 返回 true 组件不渲染 , 返回 false 组件重新渲染。和 shouldComponentUpdate 相反,shouldComponentUpdate : 返回 true 组件渲染 , 返回 false 组件不渲染。
- memo 当二个参数 compare 不存在时,会用浅比较原则处理 props ,相当于仅比较 props 版本的 pureComponent 。
- memo 同样适合类组件和函数组件。
被 memo 包裹的组件,element 会被打成 REACT_MEMO_TYPE 类型的 element 标签,在 element 变成 fiber 的时候, fiber 会被标记成 MemoComponent 的类型。
react/src/ReactMemo.js
function memo(type,compare){
const elementType = {
$$typeof: REACT_MEMO_TYPE,
type, // 我们的组件
compare: compare === undefined ? null : compare, //第二个参数,一个函数用于判断prop,控制更新方向。
};
return elementType
}
react-reconciler/src/ReactFiber.js
case REACT_MEMO_TYPE:
fiberTag = MemoComponent;
那么对于 MemoComponent React 内部又是如何处理的呢?首先 React 对 MemoComponent 类型的 fiber 有单独的更新处理逻辑 updateMemoComponent 。首先一起看一下主要逻辑:
react-reconciler/src/ReactFiberBeginWork.js
function updateMemoComponent(){
if (updateExpirationTime < renderExpirationTime) {
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual //如果 memo 有第二个参数,则用二个参数判定,没有则浅比较props是否相等。
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current,workInProgress,renderExpirationTime); //已经完成工作停止向下调和节点。
}
}
// 返回将要更新组件,memo包装的组件对应的fiber,继续向下调和更新。
}
memo 主要逻辑是
- 通过 memo 第二个参数,判断是否执行更新,如果没有那么第二个参数,那么以浅比较 props 为 diff 规则。如果相等,当前 fiber 完成工作,停止向下调和节点,所以被包裹的组件即将不更新。
- memo 可以理解为包了一层的高阶组件,它的阻断更新机制,是通过控制下一级 children ,也就是 memo 包装的组件,是否继续调和渲染,来达到目的的。