React Router 4 把Route
看成普通的React组件,能够在任意组件内使用Route
,而再也不像以前的版本那样,必须在一个地方集中定义全部的Route
。所以,使用React Router 4 的项目中,常常会有Route
和其余组件出如今同一个组件内的状况。例以下面这段代码:前端
class App extends Component {
render() {
const { isRequesting } = this.props;
return (
<div>
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/login" component={Login} />
<Route path="/home" component={Home} />
</Switch>
</Router>
{isRequesting && <Loading />}
</div>
);
}
}
复制代码
页面加载效果组件Loading
和Route
处于同一层级,这样,Home
、Login
等页面组件都共用外层的Loading组件。当和Redux一块儿使用时,isRequesting会存储到Redux的store中,App
会做为Redux中的容器组件(container components),从store中获取isRequesting。Home
、Login
等页面根组件通常也会做为容器组件,从store中获取所需的state,进行组件的渲染。代码演化成这样:react
class App extends Component {
render() {
const { isRequesting } = this.props;
return (
<div>
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/login" component={Login} />
<Route path="/home" component={Home} />
</Switch>
</Router>
{isRequesting && <Loading />}
</div>
);
}
}
const mapStateToProps = (state, props) => {
return {
isRequesting: getRequestingState(state)
};
};
export default connect(mapStateToProps)(App);
复制代码
class Home extends Component {
componentDidMount() {
this.props.fetchHomeDataFromServer();
}
render() {
return (
<div>
{homeData}
</div>
);
}
}
const mapStateToProps = (state, props) => {
return {
homeData: getHomeData(state)
};
};
const mapDispatchToProps = dispatch => {
return {
...bindActionCreators(homeActions, dispatch)
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Home);
复制代码
Home
组件挂载后,调用this.props.fetchHomeDataFromServer()
这个异步action从服务器中获取页面所需数据。fetchHomeDataFromServer
通常的结构会是这样:git
const fetchHomeDataFromServer = () => {
return (dispatch, getState) => {
dispatch(REQUEST_BEGIN);
return fetchHomeData().then(data => {
dispatch(REQUEST_END);
dispatch(setHomeData(data));
});
}
复制代码
这样,在dispatch
setHomeData(data)
前,会dispatch
另外两个action改变isRequesting,进而控制App
中Loading
的显示和隐藏。正常来讲,isRequesting的改变应该只会致使App
组件从新render,而不会影响Home
组件。由于通过Redux connect后的Home
组件,在更新阶段,会使用浅比较(shallow comparison)判断接收到的props是否发生改变,若是没有改变,组件是不会从新render的。Home
组件并不依赖isRequesting,render方法理应不被触发。github
但实际的结果是,每一次App
的从新render,都伴随着Home
的从新render。Redux浅比较作的优化都被浪费掉了!bash
到底是什么缘由致使的呢?最后,我在React Router Route
的源码中找到了罪魁祸首:服务器
componentWillReceiveProps(nextProps, nextContext) {
warning(
!(nextProps.location && !this.props.location),
'<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
)
warning(
!(!nextProps.location && this.props.location),
'<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
)
// 注意这里,computeMatch每次返回的都是一个新对象,如此一来,每次Route更新,setState都会从新设置一个新的match对象
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
})
}
render() {
const { match } = this.state
const { children, component, render } = this.props
const { history, route, staticContext } = this.context.router
const location = this.props.location || route.location
// 注意这里,这是传递给Route中的组件的属性
const props = { match, location, history, staticContext }
if (component)
return match ? React.createElement(component, props) : null
if (render)
return match ? render(props) : null
if (typeof children === 'function')
return children(props)
if (children && !isEmptyChildren(children))
return React.Children.only(children)
return null
}
复制代码
Route
的componentWillReceiveProps
中,会调用setState
设置match,match由computeMatch
计算而来,computeMatch
每次都会返回一个新的对象。这样,每次Route
更新(componentWillReceiveProps被调用),都将建立一个新的match,而这个match由会做为props传递给Route
中定义的组件(这个例子中,也就是Home
)。因而,Home
组件在更新阶段,总会收到一个新的match
属性,致使Redux的浅比较失败,进而触发组件的从新渲染。事实上,上面的状况中,Route
传递给Home
的其余属性location、history、staticContext都没有改变,match虽然是一个新对象,但对象的内容并无改变(一直处在同一页面,URL并无发生变化,match的计算结果天然也没有变)。react-router
若是你认为这个问题只是和Redux一块儿使用时才会遇到,那就大错特错了。再举两个不使用Redux的场景:app
App
结构基本不变,只是再也不经过Redux获取isRequesting,而是做为组件自身的state维护。Home
继承自React.PureComponent
,Home
经过App
传递的回调函数,改变isRequesting,App
从新render,因为一样的缘由,Home
也会从新render。React.PureComponent
的功效也浪费了。App
和Home
组件经过@observer
修饰,App
监听到isRequesting改变从新render,因为一样的缘由,Home
组件也会从新render。一个Route
的问题,居然致使全部的状态管理库的优化工做都大打折扣!痛心!异步
我已经在github上向React Router官方提了这个issue,但愿能在componentWillReceiveProps
中先作一些简单的判断,再决定是否要从新setState
。但使人失望的是,这个issue很快就被一个Collaborator给close掉了。分布式
好吧,求人不如求己,本身找解决方案。
几个思路:
既然Loading
放在和Route
同一层级的组件中会有这个问题,那么就把Loading
放到更低层级的组件内,Home
、Login
中,大不了多引几回Loading
组件。但这个方法治标不治本,Home
组件内依然可能会定义其余Route
,Home
依赖状态的更新,一样又会致使这些Route
内组件的从新渲染。也就是说,只要在container components中使用了Route
,这个问题就绕不开。但在React Router 4 Route
的分布式使用方式下,container components中是不可能彻底避免使用Route
的。
重写container components的shouldComponentUpdate
方法,方法可行,但每一个组件重写一遍,心累。
接着2的思路,经过建立一个高阶组件,在高阶组件内重写shouldComponentUpdate
,若是Route
传递的location属性没有发生变化(表示处于同一页面),那么就返回false。而后使用这个高阶组件包裹每个要在Route
中使用的组件。
新建一个高阶组件connectRoute
:
import React from "react";
export default function connectRoute(WrappedComponent) {
return class extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.location !== this.props.location;
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
复制代码
用connectRoute
包裹Home
、Login
:
const HomeWrapper = connectRoute(Home);
const LoginWrapper = connectRoute(Login);
class App extends Component {
render() {
const { isRequesting } = this.props;
return (
<div>
<Router>
<Switch>
<Route exact path="/" component={HomeWrapper} />
<Route path="/login" component={LoginWrapper} />
<Route path="/home" component={HomeWrapper} />
</Switch>
</Router>
{isRequesting && <Loading />}
</div>
);
}
}
复制代码
这样就一劳永逸的解决问题了。
咱们再来思考一种场景,若是App
使用的状态一样会影响到Route
的属性,好比isRequesting
为true时,第三个Route
的path也会改变,假设变成<Route path="/home/fetching" component={HomeWrapper} />
,而Home
内部会用到Route
传递的path(其实是经过match.path
获取), 这时候就须要Home
组件从新render。 但由于高阶组件的shouldComponentUpdate
中咱们只是根据location作判断,此时的location依然没有发生变化,致使Home
并不会从新渲染。这是一种很特殊的场景,可是想经过这种场景告诉你们,高阶组件shouldComponentUpdate
的判断条件须要根据实际业务场景作决策。绝大部分场景下,上面的高阶组件是足够使用。
Route
的使用姿式并不简单,且行且珍惜吧!
欢迎关注个人公众号:老干部的大前端,领取21本大前端精选书籍!