上一篇react-router原理之Link跳轉中提到了Link在onClick的處理函式中會呼叫history的push(或replace)方法。接下來我們就以push方法為例來看一下history具體都做了些什麼。Link中的history是通過context傳入進來的,需要向外層進行查詢,繼續以官網為例,最外層是BrowserRouter。
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
const BasicExample = () => (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
...
</ul>
<Route exact path="/" component={Home} />
...
</div>
</Router>
);
複製程式碼
開啟BrowserRouter檔案,可以看到宣告瞭例項屬性history物件,history物件的建立來自history包的createBrowserHistory方法。
import { createBrowserHistory as createHistory } from "history";
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
複製程式碼
createBrowserHistory(儲存在modules/createBrowserHistory.js)最後返回一個history物件,history物件上擁有許多的屬性和方法,其中就有push、replace、listen等。
關於push方法核心程式碼就兩行
globalHistory.pushState({ key, state }, null, href);
setState({ action, location });
複製程式碼
globalHistory對應的瀏覽器環境中的window.history物件,雖然window可以監聽popstate事件,但是執行pushState或者replaceState是不會觸發該事件的,只有點選瀏覽器的前進後退按鈕時才會觸發,因此呼叫pushState方法只是改變了位址列的url,其他的沒有任何變化。
為了達到url變化即重新渲染頁面的目的,就需要用到setState方法了(這裡的setState方法只是一個普通的函式)
setState方法中最關鍵的就是下面這一行程式碼,執行notifyListeners方法遍歷listeners陣列中的每個listener並呼叫執行。
transitionManager.notifyListeners(history.location, history.action);
// notifyListeners方法定義
let listeners = [];
const notifyListeners = (...args) => {
listeners.forEach(listener => listener(...args));
};
複製程式碼
如果把重新渲染頁面的邏輯加入到listeners陣列中,那麼當點選Link的時候就可以實現頁面更新的目的了。接下來就需要回到history生成的地方也就是BrowserHistory去找一找新增listener的邏輯,BrowserRouter在建立好history物件之後,通過props的形式把history傳遞給了Router。
Router針對history做了兩件事
- 新增到context上,使得Link通過context即可獲得history物件
- 在componentWillMount中呼叫history.listen方法增加對url變更的監聽,當url變化的時候呼叫setState觸發Router的重新渲染
componentWillMount() {
const { children, history } = this.props;
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
});
});
}
複製程式碼
Router元件是Route的父元件,所以當Router重新render的時候,那麼Route自然也可以觸發render,這樣就可以響應最新的url狀態了。
history包與html5 history的關係
html5也提供了history方法,為什麼react-router要用history包呢?
雖然history包的createBrowserHistory其實底層依賴的就是html5的history,不過history除了支援createBrowserHistory之外,還提供createHashHistory和createMemoryHistory,這三種方式底層依賴的基礎技術各不相同,但是對外暴露的介面都是一致的。這其實就是history包的意義所在
history包對環境的差異進行抽象,提供統一的一致性介面,輕鬆實現了會話的管理
StaticRouter與BrowserRouter的區別
react-router是支援伺服器端渲染的,由於在伺服器環境中不存在html5的history物件,因此無法使用history包,所以也不能使用BrowserRouter。
針對伺服器端環境,react-router提供了StaticRouter,StaticRouter與BrowserRouter最大的區別就體現在建立的history物件上面,兩者的history物件擁有幾乎完全一致的屬性方法。由於伺服器環境沒有history,因此也不會有history的改變,因此StaticRouter的history的方法(push、replace、go)等都是不可呼叫執行的。