React 中的无限渲染

IT技术 javascript reactjs react-native
2021-04-22 06:43:04

我在弄清楚为什么我的应用程序正在执行无休止的渲染时遇到问题。

在里面,我的有状态组件,我在 componentDidMount 方法中调用了一个 redux 操作(调用 componentWillMount 也会做无休止的渲染)

class cryptoTicker extends PureComponent {
  componentDidMount() {
    this.props.fetchCoin()
    // This fetches some 1600 crypto coins data,Redux action link for the same in end
  }

  render() {
    return (
      <ScrollView>
        <Header />
        <View>
          <FlatList
            data={this.state.searchCoin ? this.displaySearchCrypto : this.props.cryptoLoaded}
            style={{ flex: 1 }}
            extraData={[this.displaySearchCrypto, this.props.cryptoLoaded]}
            keyExtractor={item => item.short}
            initialNumToRender={50}
            windowSize={21}
            removeClippedSubviews={true}
            renderItem={({ item, index }) => (
              <CoinCard
                key={item["short"]}
              />
            )} 
          />
        </View>
      </ScrollView>
    )
  }
}

在 CoinCard 中,除此之外我实际上什么都不做(注意扁平列表中的 CoinCard)

class CoinCard extends Component {
  render () { 
    console.log("Inside rende here")
    return (
        <View> <Text> Text </Text>  </View>
    )  
  }
}

现在,当我控制台登录我的硬币卡渲染时,我可以在这里看到Inside rende 的无限日志

[问题:]谁能帮我弄清楚为什么会发生这种情况?

你可以点击这里查看我的操作点击这里查看我的减速器

[更新:]如果您想克隆并自己查看,我的存储库就在这里

[更新:2]:我已经在 github 上推送了上面的共享代码,它仍然会记录无休止的 console.log 语句(如果你可以克隆、运行并返回到这个提交)。

[更新:3]:当我的意思是无限渲染时,我也不再使用<ScrollView />in <FlatList />,我的意思是它是无限的(& 不必要地)将相同的props传递给子组件(<Coincard />),如果我使用 PureComponent,它不会无休止地记录在render () {但在componentWillRecieveProps,如果我这样做console.log(nextProps),我可以看到一遍又一遍地传递相同的日志

3个回答

在您的代码中有一些需要注意的地方。

  • CoinCard组件必须是PureComponent,这将不会重新渲染,如果props是浅相等。
  • 您不应该FlatlistScrollView组件内部渲染您组件,这会使组件一次渲染其中的所有组件,这可能会导致Flatlist之间出现更多循环ScrollView
  • 您还height可以对渲染的组件进行定义以减少为其他props渲染组件的次数。
  • 另一件需要注意的事情是,基于下面提到的日志语句只有组件中的 props 会在滚动底部呈现

    import {Dimensions} from 'react-native'
    
    const {width, height} = Dimensions.get('window)
    
    class CoinCard extends React.PureComponent {
    render () { 
      console.log(this.props.item.long)  //... Check the prop changes here, pass the item prop in parent Flatlist. This logs component prop changes which will show that same items are not being re-rendered but new items are being called.
      return (
        <View style={{height / 10, width}}> //... Render 10 items on the screen
          <Text>
            Text
          </Text>
        </View>
      )  
     }
    }
    

更新

这个额外的日志记录是由于 props 来自Flatlist你的组件而没有PureComponent 浅比较

请注意,componentWillReceiveProps()已弃用,您应该在代码中避免使用它们。 React.PureComponent在幕后工作,用于shouldComponentUpdatecurrentupdatedprops之间使用浅层比较因此,登录console.log(this.props.item.long)您的PureComponent' render将记录可以检查的唯一列表。

就像 izb 提到的,pb 的根本原因是在纯组件上完成的业务调用,而它刚刚加载。这是因为您的组件做出了业务决策(<=>“我决定何时必须在我自己身上展示某些东西”)。在 React 中这不是一个好习惯,在使用 redux 时更是如此。组件必须尽可能愚蠢,甚至不能决定做什么和什么时候做。

正如我在您的项目中看到的,您没有正确处理组件和容器概念。你的容器中不应该有任何逻辑,因为它应该只是一个愚蠢的纯组件的包装器。像这样:

import { connect, Dispatch } from "react-redux";
import { push, RouterAction, RouterState } from "react-router-redux";
import ApplicationBarComponent from "../components/ApplicationBar";

export function mapStateToProps({ routing }: { routing: RouterState }) {
    return routing;
}

export function mapDispatchToProps(dispatch: Dispatch<RouterAction>) {
    return {
        navigate: (payload: string) => dispatch(push(payload)),
    };
}
const tmp = connect(mapStateToProps, mapDispatchToProps);
export default tmp(ApplicationBarComponent);

和匹配的组件:

import AppBar from '@material-ui/core/AppBar';
import IconButton from '@material-ui/core/IconButton';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import { StyleRules, Theme, withStyles, WithStyles } from '@material-ui/core/styles';
import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import AccountCircle from '@material-ui/icons/AccountCircle';
import MenuIcon from '@material-ui/icons/Menu';
import autobind from "autobind-decorator";
import * as React from "react";
import { push, RouterState } from "react-router-redux";

const styles = (theme: Theme): StyleRules => ({
  flex: {
    flex: 1
  },
  menuButton: {
    marginLeft: -12,
    marginRight: 20,
  },
  root: {
    backgroundColor: theme.palette.background.paper,
    flexGrow: 1
  },
});
export interface IProps extends RouterState, WithStyles {
  navigate: typeof push;
}

@autobind
class ApplicationBar extends React.PureComponent<IProps, { anchorEl: HTMLInputElement | undefined }> {
  constructor(props: any) {
    super(props);
    this.state = { anchorEl: undefined };
  }
  public render() {

    const auth = true;
    const { classes } = this.props;
    const menuOpened = !!this.state.anchorEl;
    return (
      <div className={classes.root}>
        <AppBar position="fixed" color="primary">
          <Toolbar>
            <IconButton className={classes.menuButton} color="inherit" aria-label="Menu">
              <MenuIcon />
            </IconButton>
            <Typography variant="title" color="inherit" className={classes.flex}>
              Title
            </Typography>

            <Tabs value={this.getPathName()} onChange={this.handleNavigate} >
              {/* <Tabs value="/"> */}
              <Tab label="Counter 1" value="/counter1" />
              <Tab label="Counter 2" value="/counter2" />
              <Tab label="Register" value="/register" />
              <Tab label="Forecast" value="/forecast" />
            </Tabs>

            {auth && (
              <div>
                <IconButton
                  aria-owns={menuOpened ? 'menu-appbar' : undefined}
                  aria-haspopup="true"
                  onClick={this.handleMenu}
                  color="inherit"
                >
                  <AccountCircle />
                </IconButton>
                <Menu
                  id="menu-appbar"
                  anchorEl={this.state.anchorEl}
                  anchorOrigin={{
                    horizontal: 'right',
                    vertical: 'top',
                  }}
                  transformOrigin={{
                    horizontal: 'right',
                    vertical: 'top',
                  }}
                  open={menuOpened}
                  onClose={this.handleClose}
                >
                  <MenuItem onClick={this.handleClose}>Profile</MenuItem>
                  <MenuItem onClick={this.handleClose}>My account</MenuItem>
                </Menu>
              </div>
            )}
          </Toolbar>
        </AppBar>
      </div >
    );
  }
  private getPathName(): string {
    if (!this.props.location) {
      return "/counter1";
    }
    return (this.props.location as { pathname: string }).pathname;
  }
  private handleNavigate(event: React.ChangeEvent<{}>, value: any) {
    this.props.navigate(value as string);
  }

  private handleMenu(event: React.MouseEvent<HTMLInputElement>) {
    this.setState({ anchorEl: event.currentTarget });
  }

  private handleClose() {
    this.setState({ anchorEl: undefined });
  }
}
export default withStyles(styles)(ApplicationBar);

然后你会告诉我:“但是我在哪里发起将填写我的名单的电话?” 好吧,我在这里看到您使用了 redux-thunk(我更喜欢 redux observable……学习起来更复杂,但 waaaaaaaaaaaaaaaaay 更强大),那么这应该是启动调度的 thunk!

总结一下:

  • 组件:最愚蠢的元素,通常应该只有渲染方法,以及一些其他的方法处理程序来冒泡用户事件。此方法只负责向用户显示其属性。除非您拥有仅属于该组件的视觉信息(例如弹出窗口的可见性),否则不要使用状态。任何显示或更新的内容都来自上面:更高级别的组件或容器。它不会决定更新自己的值。充其量,它处理一个子组件上的用户事件,然后在上面冒泡出另一个事件,并且......也许在某个时候,它的容器会返回一些新的属性!
  • 容器:非常愚蠢的逻辑,包括将顶级组件包装到 redux 中,以便将事件插入到操作中,并将存储的某些部分插入到属性中
  • Redux thunk(或 redux observable):它处理整个用户应用程序逻辑。这个人是唯一知道什么时候触发什么的人。如果前端的一部分必须包含复杂性,那就是这个!
  • Reducers:定义如何组织存储中的数据以使其尽可能易于使用。
  • 存储:理想情况下,每个顶级容器一个,唯一一个包含必须向用户显示的数据。其他人不应该。

如果您遵循这些原则,您将永远不会遇到诸如“为什么这被称为两次?以及……是谁制造的?为什么在此时此刻?”之类的问题。

别的东西:如果你使用 redux,使用不变性框架。否则你可能会遇到问题,因为 reducer 必须是纯函数。为此,您可以使用流行的 immutable.js 但一点也不方便。而后来的 ousider 实际上是一个杀手:immer(由作者或 mobx 制作)。

似乎上面评论中的 Jacob 设法使组件仅呈现两次。

这肯定会导致双重初始渲染(如果它不是 PureComponent,则会导致无限渲染):

componentDidUpdate() {
    var updateCoinData;

    if (!updateCoinData) { // <- this is always true
        updateCoinData = [...this.props.cryptoLoaded];
        this.setState({updateCoinData: true}); // <- this will trigger a re render since `this.state.updateCoinData` is not initially true
    }
    ...
}

链接到您的存储库中的问题

在我最近的提交中,if (this.state.updateCoinData || this.updateCoinData.length < 1 ) { this.updateCoinData = [...this.props.cryptoLoaded]; this.setState({updateCoinData: true}) }即使没有它,我也将其更改为 ,它仍然导致无限渲染
2021-05-27 06:43:04