react-router原理之幕後history

Randal發表於2018-05-24

上一篇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)等都是不可呼叫執行的。

相關文章