React/RN组件避免重复渲染的一些技巧

组件基础

回顾下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(
    () => () => { // 注意这里不同于 useCallback
      setCount((v) => v + 1);
      setSonCount((v) => v + 1);
    },
    []
  );

memo和useMemo的区别

对于一个函数组件,useMemo可以缓存它的执行结果,而不是函数组件本身。而memo 是高阶组件,它会比较当前组件的 propsstate 是否发生了变化,如果都没有变化,就不会重新渲染该组件,而是直接使用之前的结果。但是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.y
        if (scrollY > 0) {
            setShowTop(true)
        } else {
            setShowTop(false)
        }
    }, [])

  return (
    <SafeAreaView style={styles.container}>
      <FlatList
        ref={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

  • 22
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值