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淺比較作的優化都被浪費掉了!服務器
到底是什麼緣由致使的呢?最後,我在React Router Route
的源碼中找到了罪魁禍首:react-router
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的計算結果天然也沒有變)。app
若是你認爲這個問題只是和Redux一塊兒使用時纔會遇到,那就大錯特錯了。再舉兩個不使用Redux的場景:異步
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掉了。ide
好吧,求人不如求己,本身找解決方案。
幾個思路:
Loading
放在和Route
同一層級的組件中會有這個問題,那麼就把Loading
放到更低層級的組件內,Home
、Login
中,大不了多引幾回Loading
組件。但這個方法治標不治本,Home
組件內依然可能會定義其餘Route
,Home
依賴狀態的更新,一樣又會致使這些Route
內組件的從新渲染。也就是說,只要在container components中使用了Route
,這個問題就繞不開。但在React Router 4 Route
的分佈式使用方式下,container components中是不可能徹底避免使用Route
的。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本大前端精選書籍!