PureComponent 中使用函数式 Props 导致的问题 随着使用 PureComponent 的增多,原本在 Component 中能避免重复渲染的使用方法变得不再适用,例如:
在 PureComponent 传递了函数式 Props 导致重复渲染问题 问题描述 采用 PureComponent 代替原先的 Component,可以避免根组件的重新渲染,以及带动所有子节点数的重新渲染。而对于 PureComponent 来说,机制是在原有的 Component 基础上,通过 shallowEqual 浅比较,也就是 shouldComponentUpdate 的判断,来避免在 props 和 state 不变的情况下,重复调用 render 函数而导致性能下降。 通常情况下使用 PureComponent 代替 Component 是没问题的,内部只需要将浅层的 props 传递即可。但如果遇到函数式的 prop 传递(如下方所示),则每次传递的引用都会改变,shallowEqual 在判断的时候,就会判定为 false,导致前功尽弃。
1 2 3 4 render () { return <ChildComponent onCallback ={() => { console.log('do sth.') }}> }
1 2 3 4 render () { return <ChildComponent onCallback ={() => { this.doSth.bind(this) }}> }
这两种情况可以通过在构造函数绑定 this,或者使用箭头函数提升作用域的方式解决:
1 2 3 4 5 6 7 onCallback = () => { } render () { return <ChildComponent onCallback ={ this.onCallback } /> }
或者
1 2 3 4 5 6 7 8 9 constructor () { this .onCallback = this .onCallback .bind (this ); } onCallback () { } render () { return <ChildComponent onCallback ={ this.onCallback } /> }
但是当出现如下这种情况:该 callback,需要在触发时候,依赖某些上层的数据来进行处理。如何不使用闭包函数进行处理?
1 2 3 4 5 6 7 8 doSomeThingWith (dependence) { } render () { const someAnycMethods = (dependence ) => { return <ChildComponent onCallback = {() => { this.doSthWith(dependence) }}/> } }
解决方案 通常情况下,我们可以自己维护 pureComponent,避免上面那两种情况,但是其他的同事可能未必知道你的规则。他们可能不小心传递了一个闭包箭头函数或者使用 bind,破坏了这个 pureComponent。考虑到项目中几乎没有传递函数不同会影响子组件内 dom 渲染的情况,比如 renderProps 这种设计模式就不适用。那如果我们假设传递回调函数的不同,对渲染的结果不造成影响,我们是否可以将函数类型的 props 忽略不计呢?
shallowEqual 源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 function is (x, y ) { if (x === y) { return x !== 0 || y !== 0 || 1 / x === 1 / y } else { return x !== x && y !== y } } const shallowEqual = (objA, objB ) => { if (is (objA, objB)) { return true } if ( typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null ) { return false } const keysA = Object .keys (objA) const keysB = Object .keys (objB) if (keysA.length !== keysB.length ) { return false } for (let i = 0 ; i < keysA.length ; i++) { if ( !hasOwnProperty.call (objB, keysA[i]) || !is (objA[keysA[i]], objB[keysA[i]]) ) { return false } } return true } export default shallowEqual
这里我的做法是,修改 shouldCompoentUpdate 中依赖的 shallowEqual 判断逻辑,添加函数式 props 例外:
1 2 3 4 5 6 7 8 9 function is (x, y ) { if (x === y) { return x !== 0 || y !== 0 || 1 / x === 1 / y } else { if (typeof x === "function" && typeof y === "function" ) return true return x !== x && y !== y } }
最后包装一个 UnsafePureComponent 如下
1 2 3 4 5 6 7 8 9 10 import { Component } from "react" import shallowEqual from "../util/shallow-equal-pached" export default class UnsafePureComponent extends Component { shouldComponentUpdate (nextProps, nextState ) { return ( !shallowEqual (this .props , nextProps) || !shallowEqual (this .state , nextState) ) } }
当使用 reactHooks 的时候 当我们使用 react Hooks 的时候,我们完全可以通过使用 useCallback 这一 hooks 来保持回调函数引用不变。这样我们可以在传递到子组件的时候 shouldComponentUpadate 在判断引用的时候就不变了。
1 2 3 4 5 6 const [count, setCount] = useState (1 )const [val, setVal] = useState (1 )const callBack = useCallback (() => { return count }, [count])
上面代码中我们 hook 了 2 个 state,并且 hook 了一个 useCallback,useCallback 的依赖项数值只有 count 而没有 val,那么只有当 count 变化后 callBack 就会是一个新的方法,否则和之前的引用一直。
当我们调用 setVal 后,假设之前的 callBack 为 c1,setVal 后的 callBack 为 c2,那么 c1 === c2,将返回 true,而如果我们调用了 setCount,那么 c1 === c2 将会返回 false,正如官网文档中所说:把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。