Stack Navigator中使用自定义的Render Callback

1,699 阅读3分钟

Stack Navigator使用component props传递组件

通常来说,Stack Navigator的默认用法,是这样的

<NavigationContainer>\
    <Stack.Navigator>\
        <Stack.Screen name="Home" component={HomeScreen} />\
    </Stack.Navigator>\
</NavigationContainer>

自定义的组件HomeScreen是作为component属性,传递给Stack.Screen的。这种默认的做法,会让Stack.Screen对Screen Component进行优化,避免了很多不必要的渲染。官方文档中,是这样描述的。

Note: By default, React Navigation applies optimizations to screen components to prevent unnecessary renders. Using a render callback removes those optimizations. So if you use a render callback, you'll need to ensure that you use React.memo or React.PureComponent for your screen components to avoid performance issues.

从这段话中,我们可以看出,当使用自定义的render callback时,避免组件重复渲染的工作,就移交给了使用者。render callback通常是为了传递extra props,但是优化方式和extra props是没什么关系的,以下的例子中,为了避免干扰,没有新引入extra props,只是用stack navigator传递给组件的默认属性来举例子。

为了更好的监控,HomeScreen是否被重复渲染,在代码中打印了一个随机数,便于观察日志输出。

无因素引起组件更新时,使用render callback的效果

下面这段代码,使用了render callback来渲染HomeScreen。

const homeInst = (props) => (<HomeScreen {...props} />)

运行起来的效果和不使用render callback的效果是一样的。在频繁的HomeScreen和DetailsScreen切换过程中,因为没有引起HomeScreen重绘的因素存在,所以HomeScreen并没有被重复渲染。

import React from 'react'
import { View, Text, Button } from 'react-native'
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'

function HomeScreen({ navigation }) {
  console.log(`home: ${Math.random(new Date().getTime())}`)
  const goToDetail = () => {
    navigation.navigate('Details')
  }
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button title='Go To Detail' onPress={goToDetail}></Button>
    </View>
  )
}

function DetailsScreen({ navigation }) {
  const goHome = () => {
    navigation.navigate('Home')
  }
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Details Screen</Text>
      <Button title='Go Home' onPress={goHome}></Button>
    </View>
  )
}

const Stack = createNativeStackNavigator()

function App() {
  const homeInst = (props) => (<HomeScreen {...props} />)
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName='Home'>
        <Stack.Screen name='Home'>
        {homeInst}
        </Stack.Screen>
        <Stack.Screen name='Details' component={DetailsScreen}/>
      </Stack.Navigator>
    </NavigationContainer>
  )
}

export default App

有因素引起组件更新时,使用component props的效果

为了引起HomeScreen组件的更新,以便验证Screen Navigator是否对HomeScreen做了避免重复渲染的优化,在代码中加入了一个新的状态age,当点击Button时,这个age不断的自增1,因为App里有state的更新,所以作为父组件的App会更新,而作为子组件的HomeScreen通常意义上(不通常的情况下,就是使用了React.memo等优化手段)说,也会重新渲染。因为这就是React的重绘机制:从父组件开始,一层一层向下重绘。

import React, {useState} from 'react'
import { View, Text, Button } from 'react-native'
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'

function HomeScreen({ navigation }) {
  console.log(`home: ${Math.random(new Date().getTime())}`)
  const goToDetail = () => {
    navigation.navigate('Details')
  }
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button title='Go To detail' onPress={goToDetail}></Button>
    </View>
  )
}

function DetailsScreen({ navigation }) {
  const goHome = () => {
    navigation.navigate('Home')
  }
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Details Screen</Text>
      <Button title='Go Home' onPress={goHome}></Button>
    </View>
  )
}

const Stack = createNativeStackNavigator()

function App() {
  const [age, setAge] = useState(20)
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName='Home'>
        <Stack.Screen name='Home' component={HomeScreen} />
        <Stack.Screen name='Details' component={DetailsScreen} />
      </Stack.Navigator>
      <View>
        <Text>{age}</Text>
        <Button title='Increase Age' onPress={() => (setAge(age + 1))}></Button>
      </View>
    </NavigationContainer>
  )
}

export default App

当我点击Button后,发现HomeScreen并没有重绘,所以当使用component props传递组件时,Stack Navigator确实是做了防止不必要重绘的优化。

具体效果可以参考下面的动画:

home_no_render.gif

有因素引起组件更新时,使用render callback的效果

那么在上面所说的场景下,用render callback会怎么样呢?答案显而易见,如果没有做任何优化处理,那么HomeScreen的不必要的重复渲染,是无法避免的了。

代码如下:

import React, { useState } from 'react'
import { View, Text, Button } from 'react-native'
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'

function HomeScreen({ navigation }) {
  console.log(`home: ${Math.random(new Date().getTime())}`)
  const goToDetail = () => {
    navigation.navigate('Details')
  }
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button title='Go To detail' onPress={goToDetail}></Button>
    </View>
  )
}

function DetailsScreen({ navigation }) {
  const goHome = () => {
    navigation.navigate('Home')
  }
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Details Screen</Text>
      <Button title='Go Home' onPress={goHome}></Button>
    </View>
  )
}

const Stack = createNativeStackNavigator()

function App() {
  const [age, setAge] = useState(20)
  const homeInst = (props) => (<HomeScreen {...props} />)
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName='Home'>
        <Stack.Screen name='Home'>
          {homeInst}
        </Stack.Screen>
        <Stack.Screen name='Details' component={DetailsScreen} />
      </Stack.Navigator>
      <View>
        <Text>{age}</Text>
        <Button title='Increase Age' onPress={() => (setAge(age + 1))}></Button>
      </View>
    </NavigationContainer>
  )
}

export default App

动画效果如下:

home_render.gif

可以看到,当我点击Button改变App的状态时,本来没有必要变化的HomeScreen,就疯狂的重绘了起来,当然每次重绘的结果,都和之前一样,这就是无效的重绘,我们应该避免。

有因素引起组件更新时,在render callback中使用React.memo

根据上面官网文档给出的提示,如果想避免重绘,应该用React.memo (因为感觉FB已经全面拥抱Hook了,所以这里也不考虑PureComponent了)来包装你的组件。

const MemoHomeScreen = React.memo(HomeScreen)

说一百句,也顶不上一句代码,具体代码如下(都是可以copy到你的环境中直接运行的):

import React, {useState} from 'react';
import { View, Text, Button } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

function HomeScreen({ navigation }) {
  console.log(`home: ${Math.random(new Date().getTime())}`)
  const goToDetail = () => {
    navigation.navigate('Details')
  }
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button title='Go To detail' onPress={goToDetail}></Button>
    </View>
  )
}

function DetailsScreen({ navigation }) {
  const goHome = () => {
    navigation.navigate('Home')
  }
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Details Screen</Text>
      <Button title='Go Home' onPress={goHome}></Button>
    </View>
  )
}

const Stack = createNativeStackNavigator()

const MemoHomeScreen = React.memo(HomeScreen)

function App() {
  const [age, setAge] = useState(20)
  const homeInst = (props) => (<MemoHomeScreen {...props} />)
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName='Home'>
        <Stack.Screen name='Home'>
          {homeInst}
        </Stack.Screen>
        <Stack.Screen name='Details' component={DetailsScreen} />
      </Stack.Navigator>
      <View>
        <Text>{age}</Text>
        <Button title='Increase Age' onPress={() => (setAge(age + 1))}></Button>
      </View>
    </NavigationContainer>
  )
}

export default App;

上面这段代码的运行效果,和使用component props传递HomeScreen的运行效果一样。只不过前者是使用者自己优化了重绘,后者是Stack Navigator替你优化了。

有因素引起组件更新时,在render callback中使用useCallback

如果我们再稍微多想一下,hostInst本质上是一个function,而说道function的避免重复计算的手段,自然想到了useCallback。我用useCallback来包装一下,看看是否能达到一样的效果:

const homeInst = useCallback((props) => (<HomeScreen {...props} />), [])

完整代码如下:

// In App.js in a new project

import React, {useState, useCallback} from 'react'
import { View, Text, Button } from 'react-native'
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'

function HomeScreen({ navigation }) {
  console.log(`home: ${Math.random(new Date().getTime())}`)
  const goToDetail = () => {
    navigation.navigate('Details')
  }
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button title='Go To detail' onPress={goToDetail}></Button>
    </View>
  )
}

function DetailsScreen({ navigation }) {
  const goHome = () => {
    navigation.navigate('Home')
  }
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Details Screen</Text>
      <Button title='Go Home' onPress={goHome}></Button>
    </View>
  )
}

const Stack = createNativeStackNavigator()

function App() {
  const [age, setAge] = useState(20)
  const homeInst = useCallback((props) => (<HomeScreen {...props} />), [])
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName='Home'>
        <Stack.Screen name='Home'>
          {homeInst}
        </Stack.Screen>
        <Stack.Screen name='Details' component={DetailsScreen} />
      </Stack.Navigator>
      <View>
        <Text>{age}</Text>
        <Button title='Increase Age' onPress={() => (setAge(age + 1))}></Button>
      </View>
    </NavigationContainer>
  )
}

export default App

我试了一下,效果和使用React.memo是一样的,都可以达到避免无效重复绘制HomeScreen的目的。

总结

Stack Navigator的使用,除非特殊情况,非得加extraData,否则强烈推荐用props的方式传递组件,减少思维负担。如果要使用render callback,那么我是推荐使用useCallback代替React.memo的,因为配合useCallback的第二个参数,控制起来更加有针对性。