为什么react-redux reducers会死循环?

sideBar:{

open:false,
type:'temporary'

}
去掉这段代码就不会死循环了,这是为什么


import {
    SET_COLUMN,
    SET_BODY_WIDTH,
    TOGGLE_SIDEBAR,
    SET_SIDEBAR,
    SET_IS_MY_SITES
} from '../actions'

let layoutInfo={
    column:11,
    bodyWidth:0,
    isMySites:false,
    showSiteSidebar:false,
    sideBar:{
        open: true,
        type:'persistent'//temporary
    }
};

const layout = (state=layoutInfo,action)=>{
    switch (action.type){
        case SET_COLUMN:
            console.log('SET_COLUMN');
            return {
                ...state,
                column:action.column
            };
        case SET_BODY_WIDTH:
            console.log('SET_BODY_WIDTH');
            return{
                ...state,
                bodyWidth:action.width,
                showSiteSidebar:action.width>1024&&state.isMySites,
                sideBar:{
                    open:action.width>1024&&!state.isMySites,
                    type:action.width>1024?'persistent':'temporary'
                }
            };
        case TOGGLE_SIDEBAR:
            console.log('TOGGLE_SIDEBAR');
            return{
                ...state,
                column:state.sideBar.open?10:11,
                sideBar:{
                    ...state.sideBar,
                    open:!state.sideBar.open
                }
            };
        case SET_SIDEBAR:
            console.log('SET_SIDEBAR');
            return{
                ...state,
                sideBar:action.sideBar
            };
        case SET_IS_MY_SITES:
            console.log('SET_IS_MY_SITES');
            let isSite =state.bodyWidth>1024&&action.isMySites;
            return isSite?{
                ...state,
                showSiteSidebar:isSite,
                isMySites:action.isMySites,
                //start
                    sideBar:{
                        open:false,
                        type:'temporary'
                    }
                    //end:去掉这段代码就不会死循环了,这是为什么
            }:{
                    ...state,
                    showSiteSidebar:isSite,
                    isMySites:action.isMySites
                };
        default:
            return state;
    }
};

export default layout;
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from 'material-ui/styles';
import classNames from 'classnames';
import Topbar from '../components/layout/Topbar'
import Sidebar from '../components/layout/Sidebar'

import {Route,withRouter} from 'react-router-dom'

import {getUserInfo} from '../utils'
import VerifyEmailDlg from './Dialog/VerifyEmailDlg'
import SitesSidebar from '../components/layout/SitesSidebar'
import {connect} from 'react-redux';

import {setBodyWidth,setIsMySites,setSidebar} from '../actions'

const styles = theme => ({
    root: {
        width: '100%',
        height: '100%',
        zIndex: 1,
        overflow: 'hidden',
    },
    appFrame: {
        display: 'flex',
        width: '100%',
        height: '100%',
    },
    content: {
        position: 'absolute',
        top: '56px',
        right: 0,
        left: 0,
        bottom: 0,
        marginLeft:0,
        flexGrow: 1,
        boxSizing:'border-box',
        backgroundColor: '#fff',
        padding:0,
        transition: theme.transitions.create('margin', {
            easing: theme.transitions.easing.sharp,
            duration: theme.transitions.duration.leavingScreen,
        }),
        height: 'calc(100% - 56px)'
    },
    contentShift: {
        marginLeft: '240px',
        transition: theme.transitions.create('margin', {
            easing: theme.transitions.easing.easeOut,
            duration: theme.transitions.duration.enteringScreen,
        }),
    },
});
class Layout extends React.Component {
    constructor(props){
        super(props);
        this.state = {
            showVerifyEmailDig:false,
            user:null
        };
    }
    componentWillMount(){
        //this.showVerifyEmailDialog();
        window.addEventListener('resize', this.resizeListener);
        this.resizeListener();
    }
    componentWillUnmount(){
        window.removeEventListener('resize', this.resizeListener);
    }
    //监听路由变化
    componentWillReceiveProps(nextProps){
        if (nextProps.location.pathname.indexOf('mysites')!=-1){
            console.log('is ')
            this.props.dispatch(setIsMySites(true));
        }else{
            console.log('no is ')
            this.props.dispatch(setIsMySites(false));
        }
    };
    resizeListener=()=> {
        let bodyWidth = document.body.offsetWidth;
        this.props.dispatch(setBodyWidth(bodyWidth));
    };
    showVerifyEmailDialog=()=>{
        let that = this;
        getUserInfo(function(isLogin,user){
            if (isLogin&&user.is_active==0){
                that.setState({
                    showVerifyEmailDig:true,
                    user:user
                })
            }
        });
    };
    render() {
        const { classes,width,showSiteSidebar,sideBar} = this.props;
        console.log(`showSiteSidebar=${showSiteSidebar},sideBar.open=${sideBar.open}`)
        console.log('add')
        console.log(sideBar)
        return (
                <div className={classes.root}>
                    <VerifyEmailDlg
                        onClose={()=>{
                            this.setState({
                                showVerifyEmailDig:false
                            })
                        }}
                        showVerifyEmailDig={this.state.showVerifyEmailDig}
                        user={this.state.user}/>
                    <div className={classes.appFrame}>
                        <Topbar showSearchBar={width>1024}
                                handleDrawerOpen={this.handleDrawerOpen}/>
                        <Sidebar {...this.props.sideBar}/>
                        {showSiteSidebar ? <SitesSidebar/>:null}
                        <main className={classNames(classes.content,((showSiteSidebar||sideBar.open)&&width>1024) && classes.contentShift)}>
                            {this.props.children}
                        </main>
                    </div>
            </div>

        );
    }
}

const mapStateToProps = state => {
    return {
        column:state.layout.column,
        width:state.layout.bodyWidth,
        sideBar:state.layout.sideBar,
        showSiteSidebar:state.layout.showSiteSidebar,
        isMySites:state.layout.isMySites
    }
};

export default connect(mapStateToProps)(withRouter(withStyles(styles)(Layout)));

clipboard.png


Sidebar.js

import React from 'react'
import PropTypes from 'prop-types';
import Drawer from 'material-ui/Drawer';
import classNames from 'classnames';
import { withStyles } from 'material-ui/styles';
import List, { ListItem, ListItemIcon, ListItemText } from 'material-ui/List';
import Divider from 'material-ui/Divider';
import IconButton from 'material-ui/IconButton';
import ChevronLeftIcon from 'material-ui-icons/ChevronLeft';
import PermIdentity from 'material-ui-icons/PermIdentity'
import Home from 'material-ui-icons/Home'
import Pets from 'material-ui-icons/Pets'
import Explore from 'material-ui-icons/Explore'
import Feedback from 'material-ui-icons/Feedback'
import Settings from 'material-ui-icons/Settings'
import Help from 'material-ui-icons/Help'
import {Link} from 'react-router-dom'
import {connect} from 'react-redux';

const drawerWidth = 240;
const styles = theme => ({
    sidebarButton:{
        marginLeft: 8,
        marginRight: 8,
    },
    drawerPaper: {
        position: 'fixed',
        bottom:'0',
        height: '100%',
        width: drawerWidth,
        backgroundColor:theme.palette.background.default,
        borderRight:'none !important'
    },
    drawerHeader: {
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'flex-end',
        padding: '0 8px',
        height: 56
    },
    listItem:{
        paddingTop:'8px',
        paddingBottom:'8px'
    },
    listItemTextRoot:{
        fontSize:'15px'
    }
});

class Sidebar extends React.Component{
    constructor(props){
        super(props);
    }
    render(){
        const { classes,sideBar } = this.props;
        return(
            <Drawer
                open={sideBar.open}
                type={sideBar.type}
                classes={{
                    paper: classes.drawerPaper,
                }}>
                <div>
                    <div className={classes.drawerHeader}>
                        <IconButton>
                            <ChevronLeftIcon />
                        </IconButton>
                    </div>
                    <Divider />
                    <List>
                        <ListItem
                            component={Link}
                            to="/"
                            button
                            classes={{
                            root:classes.listItem
                            }}>
                            <ListItemIcon>
                                <Home className={classNames(classes.sidebarButton)} />
                            </ListItemIcon>
                            <ListItemText classes={{
                                text:classes.listItemTextRoot
                            }} primary="首页">

                            </ListItemText>
                        </ListItem>
                        <ListItem
                            component={Link}
                            to="/explore"
                            button
                            classes={{
                            root:classes.listItem}}
                        >
                            <ListItemIcon>
                                <Explore className={classNames(classes.sidebarButton)} />
                            </ListItemIcon>
                            <ListItemText classes={{
                                text:classes.listItemTextRoot
                            }}
                                          primary="发现">
                            </ListItemText>
                        </ListItem>
                        <ListItem
                            component={Link}
                            to="/user"
                            button
                            classes={{
                            root:classes.listItem
                        }}>
                            <ListItemIcon>
                                <PermIdentity className={classNames(classes.sidebarButton)} />
                            </ListItemIcon>
                            <ListItemText classes={{
                                    text:classes.listItemTextRoot
                                }}
                                primary="个人主页" />
                        </ListItem>
                        <ListItem
                            component={Link}
                            to="/mysites"
                            button
                            classes={{
                            root:classes.listItem
                        }}>
                            <ListItemIcon>
                                <Pets className={classNames(classes.sidebarButton)}/>
                            </ListItemIcon>
                            <ListItemText classes={{
                                text:classes.listItemTextRoot
                            }} primary="我的网址" />
                        </ListItem>
                        <Divider />
                        <ListItem
                            component={Link}
                            to="/"
                            button
                            classes={{
                            root:classes.listItem
                        }}>
                            <ListItemIcon>
                                <Settings className={classNames(classes.sidebarButton)}/>
                            </ListItemIcon>
                            <ListItemText classes={{
                                text:classes.listItemTextRoot
                            }} primary="设置" />
                        </ListItem>
                        <ListItem
                            component={Link}
                            to="/"
                            button
                            classes={{
                            root:classes.listItem
                        }}>
                            <ListItemIcon>
                                <Help className={classNames(classes.sidebarButton)}/>
                            </ListItemIcon>
                            <ListItemText classes={{
                                text:classes.listItemTextRoot
                            }} primary="帮助" />
                        </ListItem>
                        <ListItem
                            component={Link}
                            to="/"
                            button
                            classes={{
                            root:classes.listItem
                        }}>
                            <ListItemIcon>
                                <Feedback className={classNames(classes.sidebarButton)}/>
                            </ListItemIcon>
                            <ListItemText classes={{
                                text:classes.listItemTextRoot
                            }} primary="反馈" />
                        </ListItem>
                    </List>
                </div>
            </Drawer>
        )
    }
}

const mapStateToProps = state => {
    return {
        sideBar:state.layout.sideBar
    }
};

export default connect(mapStateToProps)(withStyles(styles)(Sidebar));

Tobbar.js

import React from 'react'
import {withStyles} from 'material-ui/styles';
import AppBar from 'material-ui/AppBar';
import classNames from 'classnames';
import Toolbar from 'material-ui/Toolbar';
import IconButton from 'material-ui/IconButton';
import MenuIcon from 'material-ui-icons/Menu';
import Typography from 'material-ui/Typography';
import SearchIcon from 'material-ui-icons/Search'
import Avatar from 'material-ui/Avatar';
import Button from 'material-ui/Button';
import Notification from 'material-ui-icons/Notifications'
import Apps from 'material-ui-icons/Apps'
import SearchBar from 'material-ui-search-bar'

import {withRouter} from 'react-router-dom'

import {connect} from 'react-redux';
import {getLoginAction, logout,setColumn,toggleSideBar} from '../../actions'


class Topbar extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            value: ''
        }
    }

    componentWillMount() {
        this.props.getLoginInfo();
    }

    login = () => {
        this.props.history.push('/account/login')
    };
    quit = () => {
        this.props.logout();
    };
    handleDrawerOpen=()=>{
        this.props.setSideBar();
    };
    render() {
        const {classes} = this.props;
        return (
            <AppBar className={classNames(classes.appBar)} classes={{
                root: classes.appBarRoot
            }}>
                <Toolbar disableGutters classes={{
                    root: classes.toolbarRoot
                }}>
                    <div className={classes.topBarLeft}>
                        <IconButton
                            color="contrast"
                            aria-label="open drawer"
                            onClick={this.handleDrawerOpen}
                            className={classNames(classes.menuButton)}
                        >
                            <MenuIcon />
                        </IconButton>
                        <img className={classes.logoImg} src={require('../../../images/awbeci-logo.png')} width={25}
                             height={25}/>
                        <Typography type="title" color="inherit" noWrap>
                            Awbeci
                        </Typography>
                    </div>
                    <div className={classes.flexCenter}>
                        { this.props.showSearchBar &&
                        <SearchBar
                            hintText="搜索"
                            onChange={() => console.log('onChange')}
                            onRequestSearch={() => console.log('onRequestSearch')}
                            className={classes.searchBar}
                        />
                        }
                    </div>
                    <div className={classes.topBarRight}>
                        { !this.props.showSearchBar && <IconButton
                            color="contrast"
                            classes={{
                                root: classes.iconBtn
                            }}
                            aria-label="open drawer"
                            onClick={this.props.handleDrawerOpen}
                        >
                            <SearchIcon/>
                        </IconButton>}

                        {
                            this.props.isLogin ? (
                                    <div>
                                        <IconButton
                                            color="contrast"
                                            classes={{
                                                root: classes.iconBtn
                                            }}
                                            aria-label="应用"
                                            onClick={this.props.handleDrawerOpen}>
                                            <Apps />
                                        </IconButton>
                                        <IconButton
                                            color="contrast"
                                            classes={{
                                                root: classes.iconBtn
                                            }}
                                            aria-label="通知"
                                            onClick={this.props.handleDrawerOpen}>
                                            <Notification />
                                        </IconButton>
                                        <IconButton
                                            color="contrast"
                                            classes={{
                                                root: classes.iconBtnAvatar
                                            }}
                                            aria-label="头像"
                                            onClick={this.quit}>
                                            <Avatar alt="Remy Sharp"
                                                    src={require('../../../images/zhangwei.png')}
                                                    className={classes.avatar}/>
                                        </IconButton>
                                    </div>)
                                :
                                (<Button color="contrast"
                                         onClick={this.login}>登录</Button>)
                        }

                    </div>
                </Toolbar>
            </AppBar>
        )
    }
}
const mapStateToProps = state => {
    return {
        isLogin: state.user.isLogin,
        sideBar:state.layout.sideBar
    }
};

const mapDispatchToProps = (dispatch,ownProps) => {
    return {
        getLoginInfo: () => {
            dispatch(getLoginAction())
        },
        logout: () => {
            dispatch(logout())
        },
        setSideBar:()=>{
            dispatch(toggleSideBar())
        }
    }
};

const styles = theme => ({
    appBar: {
        zIndex: '1499',
        boxShadow: 'none !important'
    },
    appBarRoot: {},
    toolbarRoot: {
        [theme.breakpoints.up('sm')]: {
            minHeight: '56px'
        },
    },
    menuButton: {
        marginLeft: 12,
        marginRight: 12,
    },
    iconBtn: {
        width: 32
    },
    iconBtnAvatar: {},
    logoImg: {
        marginRight: 8
    },
    avatar: {
        width: 30,
        height: 30
    },
    flex: {
        flex: 1,
    },
    flexCenter: {
        display: 'flex',
        alignItems: 'center',
        flex: 1
    },
    input: {
        color: '#fff',
        margin: theme.spacing.unit,
    },
    topBarLeft: {
        width: 240,
        display: 'flex',
        position: 'relative',
        alignItems: 'center'
    },
    searchBar: {
        margin: '0 auto',
        width: '500px !important',
        maxWidth: 500
    },
    topBarRight: {
        minWidth: 120,
        width: 160,
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'flex-end',
        boxSizing: 'border-box',
        padding: '0 12px'
    },
    flexRight: {
        display: 'flex',
        alignItems: 'center'
    }
});

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(Topbar)));
阅读 5.5k
3 个回答

说明下原因:componentWillReceiveProps本就是当props发生改变时触发的事件,所以当在componentWillReceiveProps里面改变props值本身就有问题。心寒啊,这么久才找到问题的根本!!!

componentWillReceiveProps(nextProps) {
        const navigated = nextProps.location !== this.props.previousLocation;
        const { location } = this.props;

        if (navigated) {
            this.props.dispatch(setPreviousLocation(location));
            this.routerChanged(location.pathname);
        }
    }

就可以了
参考:react-router-config

两种可能,我先说复杂的那种,具体是哪种要看你代码:(实际上是侧栏的展开/折叠会不会触发resize函数)

1. 侧栏状态原因

    case SET_IS_MY_SITES:
        console.log('SET_IS_MY_SITES');
        let isSite =state.bodyWidth>1024&&action.isMySites;
        return isSite?{
            ...state,
            showSiteSidebar:isSite,
            isMySites:action.isMySites,
            //start
                sideBar:{
                    open:false,
                    type:'temporary'
                }
                //end:去掉这段代码就不会死循环了,这是为什么
        }:{
                ...state,
                showSiteSidebar:isSite,
                isMySites:action.isMySites
            };
                

openfalse使得侧栏无法展开,所以下面的resize函数无法调用

resizeListener=()=> {
    let bodyWidth = document.body.offsetWidth;
    this.props.dispatch(setBodyWidth(bodyWidth));
};

因此state.bodyWidth>1024的条件一直为true,因此isSite一直为true,这样就会循环。

如果去掉open,那么侧栏展开,resize函数调用后改变bodyWidth,使得isSitefalse,则会返回下面的state

            {
                ...state,
                showSiteSidebar:isSite,
                isMySites:action.isMySites
            }
            

在这个state中,只改变了两个值,showSiteSidebar因为侧栏已展开,所以一直为false,而isMysites恒为true,所以这两个值是不变的,所以connect内部的shouldComponentUpdate算法返回的结果是false,即不更新,则死循环到此结束。

2.引用地址原因

这种情况原因一目了然。

在死循环中,isSitetrue,一直返回下面的state

            {
                ...state,
                showSiteSidebar:isSite,
                isMySites:action.isMySites,
                sideBar:{
                    open:false,
                    type:'temporary'
                }
                    
            }

在这里sideBar是引用类型,每次都给赋给他一个新对象,所以他的引用地址一直在变。这样对于connectshouldComponenetUpdate而言,每次的返回值都是true,即每次都更新,进入死循环。

而为什么去掉就不会死循环?如果上面你认真看了应该能猜出来了,因为改变的两个值跟上一次的值是一样的,shouldComponentUpdate会返回false

那么如何判断是哪种原因导致的呢?需要两个改动辅助判断:
1.把isSitetrue时的state改成如下

            {
                ...state,
                showSiteSidebar:isSite,
                isMySites:action.isMySites,
                sideBar:{
                    open:true,
                    type:'temporary'
                }
                    
            }

同时打印isSite的值,即reducer处理函数变成如下:

        case SET_IS_MY_SITES:
            console.log('SET_IS_MY_SITES');
            let isSite =state.bodyWidth>1024&&action.isMySites;
            
            // 新加
            console.log('isSite: ', isSite);
            
            return isSite?{
                ...state,
                showSiteSidebar:isSite,
                isMySites:action.isMySites,
                sideBar:{
                    open:open,  //新改动
                    type:'temporary'
                }
                   
            }:{
                    ...state,
                    showSiteSidebar:isSite,
                    isMySites:action.isMySites
                };

然后观察打印出来的isSite,若isSitefalse,或者没有死循环了,即证明是第一种原因。若仍有死循环,则为第二种原因,解决方法如下:

        case SET_IS_MY_SITES:
            console.log('SET_IS_MY_SITES');
            let isSite =state.bodyWidth>1024&&action.isMySites;
            
            if(isSite){
                state.sideBar.open = true;
                state.sideBar.type = 'temporary'
            }
            
            return isSite?{
                ...state,
                showSiteSidebar:isSite,
                isMySites:action.isMySites,               
            }:{
                    ...state,
                    showSiteSidebar:isSite,
                    isMySites:action.isMySites
                };

直接修改state其实是不对的,这是对于当前状况的一种处理方案。更好的方法是从逻辑上避免死循环

            {
                ...state,
                showSiteSidebar:isSite,
                isMySites:action.isMySites,
                //start
                    sideBar:{
                        open:false,
                        type:'temporary'
                    }
               //end:去掉这段代码就不会死循环了,这是为什么
            }

首先,你这段代码,sideBar.open永远等于false。Layout组件中把sideBar传给了组件Sidebar了。这里我看不到组件Sidebar的代码,但是我猜Sidebar肯定因为sideBar.open等于false,触发了reducer中的TOGGLE_SIDEBAR,然而他把sideBar.open改为true。由于Layout组件又是把sideBar放到props中去的,sideBar发生改变之后,便触发了componentWillReceiveProps了,然后reducer中的SET_IS_MY_SITES又把sideBar.open置为false。如此往复便陷入死循环里面。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题