组件基础
回顾下React中创建组件的几种方式:
- ES5的React.createClass方式
- ES6的React.Component方式
- 无状态的函数组件方式
- 带hooks的函数组件方式
这里我们只讨论Component和函数组件。我们知道Component是否发生重渲染是由shouldComponentUpdate决定的,默认情况下返true。自定义的Component会根据自身state和props是否发生变化,来决定自身是否需要重新渲染,这个过程往往需要进行深度比较的。而相对应的,PureComponent会通过shadowEqual对state和props进行浅比较,来决定是否需要重新渲染。一般而言,如果state和props没有发生变化,组件本身是不需要重新渲染的。
Component组件的渲染过程是组件调用自身的render函数来生成虚拟DOM,然后跟上次的进行比较,如果发生变化,就更新对应的平台DOM。React平台以及第三方库提供的组件一般都是基于Component的,因此只要确保props不变,就不会重新渲染。
而对于函数组件,只要这个函数被调用,虚拟DOM就会重新生成。从这个角度来看,函数是否执行,跟传给它什么参数是无关的。只是取决于它所在父Component组件是否调用了render函数,或者父函数组件是否被调用了。
useState这个hooks为函数组件提供了状态保持和触发渲染的功能。我们知道作为纯函数本身它内部是无法持久存储一个值的,每次函数执行完毕,里面创建的所有变量都会被清除。所以useState本质是把状态保存到了函数外部,这样每次执行函数,既能修改保存它的值,也能获取上次执行后的值。
一个React例子
接下去看一个例子,我们可以把代码拷贝到安装 – React 中文文档 的"尝试 React"中:
import {useState} from 'react'const Parent = () => {const [count, setCount] = useState(0);const [son1Count, setSon1Count] = useState(0);const [son2Count, setSon2Count] = useState(0);return (<div>{console.log("Parent render")}<button onClick={() => setCount(v => v + 1)}>Parent + 1</button><button onClick={() => setSon1Count(v => v + 1)}>Son1 + 1</button><button onClick={() => setSon2Count(v => v + 1)}>Son2 + 1</button><h3>Parent: {count}</h3><Son1 son1Count={son1Count} /><Son2 son2Count={son2Count} /></div>);
};
const Son1 = (props) => {return (<div>{console.log("Son1 render")}Son1: {props.son1Count}</div>);
};
const Son2 = (props) => {return (<div>{console.log("Son2 render")}Son2: {props.son2Count}</div>);
};export default function App() {return <Parent />
}
按照我们前面的描述,点击parent+1按钮会导致Parent组件重新执行,因此Son1和Son2尽管props没有发生变化,但由于它们是函数组件,它们依然会被执行。
使用memo
memo
允许你的组件在 props 没有改变的情况下跳过重新渲染。我们可以用它来改造上面的Son1和Son2:
const Son1 = memo((props) => {return (<div>{console.log("Son1 render")}Son1: {props.son1Count}</div>);
});
const Son2 = memo((props) => {return (<div>{console.log("Son2 render")}Son2: {props.son2Count}</div>);
现在我们再点击parent+1,就会发现Son1和Son2没有重新执行了。
props中包含函数
更新我们的例子,向memo后的Son组件props传递一个函数:
import {useState ,memo} from 'react'
const Parent = () => {const [count, setCount] = useState(0);const [sonCount, setSonCount] = useState(0);const allPlus = () => {setCount(v => v + 1);setSonCount(v => v + 1);};return (<div>{console.log("Parent render")}<button onClick={() => setCount(v => v + 1)}>Parent + 1</button><h3>Parent: {count}</h3><Son allPlus={allPlus} sonCount={sonCount} /></div>);
};
const Son = memo((props) => {return (<div>{console.log("Son render")}<p>Son: {props.sonCount}</p><button onClick={props.allPlus}>All + 1</button></div>);
});export default function App() {return <Parent />
}
尽管我们使用了memo,但当我们点击parent+1按钮时,Son也发生了渲染。问题的根源是Parent组件每次执行时,里面的allPlus函数会重新创建,导致Son的props发生了变化。默认情况下,React 将使用 Object.is 比较每个 prop。
memo的arePropsEqual
一个解决办法是我们实现memo的第二个参数arePropsEqual,以取代默认的实现:
const Son = memo((props) => {return (<div>{console.log("Son render")}<p>Son: {props.sonCount}</p><button onClick={props.allPlus}>All + 1</button></div>);
},
(prevProps, nextProps) => prevProps.sonCount === nextProps.sonCount);
再点击parent+1按钮,我们发现Son不再重新渲染了。
useCallback
另一个解决办法是保持函数的不变性,在React中,我们通常使用useCallback在多次渲染中缓存函数。它的作用看起来和useRef类似,但它还提供了第二个参数dependencies,用来在依赖发生改变时,更新以及返回这一次渲染传入的函数。将代码更新如下:
const allPlus = useCallback(() => {setCount(count + 1);setSonCount(sonCount + 1);
}, []);
这时候点击parent+1按钮也不会引起Son组件重新渲染。
useMemo
保持函数的不变性,我们也可以使用useMemo。useMemo缓存的是函数执行的结果。因为我们要缓存一个函数,因此我们要使用一个返回这个函数的函数作为useMemo的参数,修改代码如下:
const allPlus = useMemo(() => () => { // 注意这里不同于 useCallbacksetCount((v) => v + 1);setSonCount((v) => v + 1);},[]);
memo和useMemo的区别
对于一个函数组件,useMemo可以缓存它的执行结果,而不是函数组件本身。而memo
是高阶组件,它会比较当前组件的 props
和 state
是否发生了变化,如果都没有变化,就不会重新渲染该组件,而是直接使用之前的结果。但是memo组件本身每次渲染时都会重新创建
。因此,当我们需要保证组件不被重复渲染时,使用memo。当需要保证组件prop的不变性时,使用useMemo。
一个React Native例子
假设我们有一个FlatList,并且支持下拉刷新,因此给它设置了refreshControl。并且在滚动的时候控制一个”返回顶部“按钮的显示与隐藏。
const App = () => {const [refreshing, setRefreshing] = React.useState(false);const flatListRef = useRef<FlatList>(null)const [showTop, setShowTop] = useState(isShowTop)const onRefresh = React.useCallback(() => {setRefreshing(true);wait(2000).then(() => setRefreshing(false));}, []);const handleScrollToTop = () => {flatListRef.current?.scrollToOffset({ animated: true, offset: 0 })}const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {const scrollY = e.nativeEvent.contentOffset.yif (scrollY > 0) {setShowTop(true)} else {setShowTop(false)}}, [])return (<SafeAreaView style={styles.container}><FlatListref={flatListRef}data={DATA}onScroll={onScroll}renderItem={({item}) => <Item title={item.title} />}keyExtractor={item => item.id}refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}/>{showTop &&<Button onPress={handleScrollToTop} title="Top"></Button>}</SafeAreaView>);
};
当调用setShowTop时,APP组件会发生重新渲染。这里的onScroll、renderItem、refreshControl都会重新创建,导致FlatList重新渲染。使用前面我们提到的技巧(当然renderItem可以移动到App函数外面去),我们可以缓存这些prop来避免发生变化。对于refreshControl我们可以使用useMemo,例如:
const refreshControl = useMemo(() => <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />, [refreshing])
当然我们常见的代码,大多数使用我们一开始提供的版本,因为大多数情况下,多余的渲染并不会引起显著的性能问题。但是理解并发现多余的渲染,有助于我们在必要时进行相应的优化。
参考
https://www.cnblogs.com/hymenhan/p/16325708.html