PureComponent缺点(其一)

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 = () =>{
// do sth.
}
render () {
return <ChildComponent onCallback={ this.onCallback } />
}

或者

1
2
3
4
5
6
7
8
9
constructor () {
this.onCallback = this.onCallback.bind(this);
}
onCallback () {
// do sth.
}
render () {
return <ChildComponent onCallback={ this.onCallback } />
}

但是当出现如下这种情况:该 callback,需要在触发时候,依赖某些上层的数据来进行处理。如何不使用闭包函数进行处理?

1
2
3
4
5
6
7
8
doSomeThingWith (dependence) {
// use dependence to do sth.
}
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 {
//--添加函数式props例外
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)的子组件时,它将非常有用。