理解 react-router 中的 history

react-router 的分析,目前準備主要集中在三點:javascript

a. `history` 的分析。
b. `history` 與 `react-router` 的聯繫。
c. `react-router` 內部匹配及顯示原理。
複製代碼

這篇文章準備着重理解 history.html

推薦:★★★☆java

索引

引子

  • 一段顯而易見出如今各大 react v16+ 項目中的代碼是這樣的:node

    import React, {Component} from 'react'
    import { render } from 'react-dom'
    import { Router, Route } from 'react-router'
    import { createBrowserHistory } from 'history'
    const history = createBrowserHistory()
    
    const App = () => (
      <Router history={history} /> <div id="app"> {/* something */} </div> </Router>
    
    )
    
    render(<App/>, document.body.querySelector('#app'))
    複製代碼
  • react v16+ 版本里,一般 react-router 也升級到了 4 以上。react

  • react-router v4+ 一般是配合 history v4.6+ 使用的。git

  • 下面就先從 history 開始,讓咱們一步一步走近 react-router 的神祕世界。github

history核心

  • history源碼
  • history 在內部主要導出了三個方法:
    • createBrowserHistory, createHashHistory, createMemoryHistory.設計模式

    • 它們分別有着本身的做用:api

      • createBrowserHistory 是爲現代主流且支持 HTML5 history 瀏覽器提供的 API.
      • createHashHistory 是爲不支持 history 功能的瀏覽器提供的 API.
      • createMemoryHistory 則是爲沒有 DOM 環境例如 nodeReact-Native 或測試提供的 API.
    • 咱們就先從最接地氣的 createBrowserHistory 也就是咱們上文中使用的方法開始看起。瀏覽器

走進createBrowserHistory

  • 話很少說,直接走進 createBrowserHistory源碼

    /**
     * Creates a history object that uses the HTML5 history API including
     * pushState, replaceState, and the popstate event.
     */
    複製代碼
  • 在該方法的註釋裏,它說明了是它基於 H5 的 history 建立的對象,對象內包括了一些經常使用的方法譬如

    • pushState,replaceState,popstate 等等。

history 對象

  • 那麼它具體返回了什麼內容呢,下面就是它目前全部的方法和屬性:

    const globalHistory = window.history;
      const history = {
        length: globalHistory.length, // (number) The number of entries in the history stack
        action: "POP", // (string) The current action (`PUSH`, `REPLACE`, or `POP`)
        location: initialLocation, // (object) The current location. May have the following properties.
        createHref,
        push, // (function) Pushes a new entry onto the history stack
        replace, // (function) Replaces the current entry on the history stack
        go, // (function) Moves the pointer in the history stack by `n` entries
        goBack, // (function) Equivalent to `go(-1)`
        goForward, // (function) Equivalent to `go(1)`
        block, // (function) Prevents navigation
        listen
      }
    複製代碼
  • globalHistory.length 顯而易見是當前存的歷史棧的數量。

  • createHref 根據根路徑建立新路徑,在根路徑上添加原地址所帶的 search, pathname, path 參數, 推測做用是將路徑簡化。

  • location 當前的 location, 可能含有如下幾個屬性。

    • path - (string) 當前 url 的路徑 path.
    • search - (string) 當前 url 的查詢參數 query string.
    • hash - (string) 當前 url 的哈希值 hash.
    • state - - (object) 存儲棧的內容。僅存在瀏覽器歷史和內存歷史中。
  • block 阻止瀏覽器的默認導航。用於在用戶離開頁面前彈窗提示用戶相應內容。the history docs

  • 其中,go/goBack/goForward 是對原生 history.go 的簡單封裝。

  • 剩下的方法相對複雜些,所以在介紹 push, replace 等方法以前,先來了解下 transitionManager. 由於下面的不少實現,都用到了這個對象所提供的方法。

transitionManager方法介紹

  • 首先看下該對象返回了哪些方法:

    const transitionManager = {
      setPrompt,
      confirmTransitionTo,
      appendListener,
      notifyListeners
    }
    複製代碼
  • 在後續 popstate 相關的方法中,它就應用了 appendListener 和與之有關的 notifyListeners 方法,咱們就先從這些方法看起。

  • 它們的設計體現了常見的訂閱-發佈模式,前者負責實現訂閱事件邏輯,後者負責最終發佈邏輯。

    let listeners = [];
      /** * [description 訂閱事件] * @param {Function} fn [description] * @return {Function} [description] */
      const appendListener = fn => {
        let isActive = true;
        // 訂閱事件,作了函數柯里化處理,它實際上至關於運行了 `fn.apply(this, ...args)`
        const listener = (...args) => {
          if (isActive) fn(...args);
        };
        // 將監聽函數一一保存
        listeners.push(listener);
        return () => {
          isActive = false;
          listeners = listeners.filter(item => item !== listener);
        };
      };
      /** * [發佈邏輯] * @param {[type]} ..args [description] */
      const notifyListeners = (..args) => {
        listeners.forEach(listener => listener(..args))
      }
    複製代碼
  • 介紹了上面兩個方法的定義,先別急。後續再介紹它們的具體應用。

  • 而後來看看另外一個使用的較多的方法 confirmTransitionTo.

    const confirmTransitionTo = (
        location,
        action,
        getUserConfirmation,
        callback
      ) => {
        if (prompt != null) {
          const result =
            typeof prompt === "function" ? prompt(location, action) : prompt;
          if (typeof result === "string") {
            if (typeof getUserConfirmation === "function") {
              getUserConfirmation(result, callback);
            } else {
              callback(true);
            }
          } else {
            // Return false from a transition hook to cancel the transition.
            // 若是已經在執行,則暫時中止執行
            callback(result !== false);
          }
        } else {
          callback(true);
        }
      };
    複製代碼
  • 實際上執行的就是從外部傳進來的 callback 方法,只是多了幾層判斷來作校驗,並且傳入了布爾值來控制是否須要真的執行回調函數。

transitionManager 調用

  • 再而後咱們來看看上述方法appendListener, notifyListeners 的具體應用。前者體如今了 popstate 事件的訂閱中。

  • 那麼就先簡單談談 popstate 事件。

    • 當作出瀏覽器動做時,會觸發 popstate 事件, 也就是說,popstate 自己並非像 pushStatereplaceState 同樣是 history 的方法。
    • 不能使用 history.popState 這樣的方式來調用。
    • 並且,直接調用 history.pushStatehistory.replaceState 不會觸發 popstate 事件。
  • 在事件監聽方法 listen 中涉及了 popstate 的使用,在源碼中能夠看到如下兩個方法 listencheckDOMListeners.

  • 它們就是上述訂閱事件的具體調用方。

    // 首先天然是初始化
      const transitionManager = createTransitionManager();
      const PopStateEvent = "popstate";
      const HashChangeEvent = "hashchange";
    
      // 當 URL 的片斷標識符更改時,將觸發 hashchange 事件(跟在 # 後面的部分,包括 # 符號)
      // https://developer.mozilla.org/zh-CN/docs/Web/Events/hashchange
      // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/onhashchange
      const checkDOMListeners = delta => {
        listenerCount += delta;
        if (listenerCount === 1) {
          // 其實也是最多見最簡單的訂閱事件, handlePopState 對應的內容在下文有說明
          window.addEventListener(PopStateEvent, handlePopState);
          if (needsHashChangeListener)
            window.addEventListener(HashChangeEvent, handleHashChange);
        } else if (listenerCount === 0) {
          window.removeEventListener(PopStateEvent, handlePopState);
          if (needsHashChangeListener)
            window.removeEventListener(HashChangeEvent, handleHashChange);
        }
      };
    
      /** * [訂閱事件的具體調用方] * @param {Function} listener [description] * @return {Function} [description] */
      const listen = listener => {
        // 返回一個解綁函數
        const unlisten = transitionManager.appendListener(listener);
        checkDOMListeners(1);
        // 返回的函數負責取消
        return () => {
          checkDOMListeners(-1);
          unlisten();
        };
      };
    複製代碼
  • 簡言之,調用 listen 就是給 window 綁定了相應方法,再次調用以前 listen 返回的函數則是取消。

  • 而後來看看發佈事件的具體調用方 setState它在 createBrowserHistory.js 中定義,在 popstatepushreplace 中均有調用。

    /** * 在該方法中發佈 * @param {*} nextState [入參合併到 history] */
    const setState = nextState => {
      Object.assign(history, nextState);
      history.length = globalHistory.length;
      // 執行全部的監聽函數
      transitionManager.notifyListeners(history.location, history.action);
    };
    複製代碼
  • 以上是 setState 的定義。咱們來看看它在 popstate 中的使用。

    • 上文有許多代碼,以此關鍵代碼爲例:
    • window.addEventListener(PopStateEvent, handlePopState);
    const handlePopState = (event) => {
      handlePop(getDOMLocation(event.state))
    }
    
    let forceNextPop = false
    
    const handlePop = (location) => {
      if (forceNextPop) {
        forceNextPop = false
        setState()
      } else {
        const action = 'POP'
    
        transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
          if (ok) {
            setState({ action, location })
          } else {
            revertPop(location)
          }
        })
      }
    }
    複製代碼
  • 瀏覽器註冊了 popstate 事件,對應的 handlePopState 的方法則最終調用了 setState 方法。

  • 翻譯成白話就是瀏覽器回退操做的時候,會觸發 setState 方法。它將在下文以及後一篇博文裏起到重要做用。

下面的方法則應用了 confirmTransitionTo.
  • push, replace 這兩個上文提到的重要方法,是原生方法的擴展。它們都用到了上述分析過的方法,都負責實現跳轉,所以內部有較多邏輯相同。

  • 後面會以 push 爲例, 它其實就是對原生的 history.pushState 的強化。

  • 那麼這裏就先從原生的 history.pushState 開始熟悉瞭解。

  • history.pushState 接收三個參數,第一個爲狀態對象,第二個爲標題,第三個爲 Url.

    • 狀態對象:一個可序列化的對象,且序列化後小於 640k. 不然該方法會拋出異常。(暫時不知這對象能夠拿來作什麼用,或許 react-router 用來標識頁面的變化,以此渲染組件)
    • 標題(目前被忽略):給頁面添加標題。目前使用空字符串做爲參數是安全的,將來則是不安全的。Firefox 目前還未實現它。
    • URL(可選):新的歷史 URL 記錄。直接調用並不會加載它,但在其餘狀況下,從新打開瀏覽器或者刷新時會加載新頁面。
    • 一個常見的調用是 history.pushState({ foo: 'bar'}, 'page1', 'bar.html').
    • 調用後瀏覽器的 url 會當即更新,但頁面並不會從新加載。例如 www.google.com 變動爲 www.google.com/bar.html. 但頁面不會刷新。
    • 注意,此時並不會調用 popstate 事件。只有在上述操做後,訪問了其餘頁面,而後點擊返回,或者調用 history.go(-1)/history.back() 時,popstate 會被觸發。
    • 讓咱們在代碼中更直觀的看吧。
    // 定義一個 popstate 事件
    window.onpopstate = function(event) {
      console.info(event.state)
    }
    const page1 = { page: 'page1' }
    const page2 = { page: 'page2' }
    history.pushState(page1, 'page1', 'page1.html')
    // 頁面地址由 www.google.com => www.google.com/page1.html
    // 但不會刷新或從新渲染
    history.pushState(page2, 'page2', 'page2.html')
    // 頁面地址由 www.google.com/page2.html => www.google.com/page2.html
    // 但不會刷新或從新渲染
    // 此時執行
    history.back() // history.go(-1)
    // 會觸發 popstate 事件, 打印出 page1 對象
    // { page: 'page1' }
    複製代碼
  • 介紹完 pushState 後,看看 history 中是怎樣實現它的。

    const push = (path, state) => {
        const action = "PUSH";
        const location = createLocation(path, state, createKey(), history.location);
        // 過渡方法的應用
        transitionManager.confirmTransitionTo(
          location,
          action,
          getUserConfirmation,
          ok => {
             // 布爾值,用於判斷是否須要執行
            if (!ok) return;
            const href = createHref(location);
            const { key, state } = location;
            // 在支持 history 的地方則使用 history.pushState 方法實現
            if (canUseHistory) {
              globalHistory.pushState({ key, state }, null, href);
              if (forceRefresh) {
                window.location.href = href
              } else {
                // 若是是非強制刷新時,會更新狀態,後續在 react-router 中起到重要做用
                // 上文提到過的發佈事件調用處
                setState({ action, location })
              }
            } else {
              window.location.href = href;
            }
          }
        );
      };
    複製代碼
  • 關鍵代碼:globalHistory.pushState({ key, state }, null, href); 和上文分析的一致。

  • pushStatepush 方法講完,replaceStatereplace 也就很好理解了。

  • replaceState 只是把推動棧的方式改成替換棧的行爲。它接收的參數與 pushState 徹底相同。只是方法調用後執行的效果不一樣。

  • 補:原本若是僅僅是介紹當前的 history. 我以前覺得找到 pushState 這個核心就已經足夠了。但當我繼續深刻,探究 react-router 原理的時候,才發現這裏遺漏了重要的一點。那就是 setState 方法。

  • 那麼這個方法具體作了什麼呢。在上文中已經作了簡單介紹,這裏再重申一遍:就是將當前 state 存入 history, 同時發佈事件,也就是調用以前訂閱時的保存的全部方法。參數則是 [history.location, history.action]. 或許如今,咱們可能對該方法的重要性沒有那麼深的理解,當你再結合後一篇分析 react-router 的文章,就知道它起的做用了。

history在react-router中

  • 這篇文章快完成的時候,我才發現 react-router 倉庫裏是有 history 的介紹的。此時我一臉茫然。這裏面內容雖然很少,卻很是值得參考。這裏作部分翻譯和理解,看成對上文的補充。
  • 原地址
history is mutable
  • 在原文檔中,說明了 history 對象是可變的。所以建議在 react-router 中獲取 location 時可使用 Routeprops 的方式來替代 history.location 的方式。這樣的方式會確保你的流程處於 React 的生命週期中。例如:

    class Comp extends React.Component {
      componentWillReceiveProps(nextProps) {
        // 正確的打開方式
        const locationChanged = nextProps.location !== this.props.location
    
        // 錯誤的打開方式,由於 history 是可變的,因此這裏老是不等的 // will *always* be false because history is mutable.
        const locationChanged = nextProps.history.location !== this.props.history.location
      }
    }
    
    <Route component={Comp}/>
    複製代碼
  • 更多內容請查看the history documentation.

小結

  • 一句話形容 history 這個庫。它是一個對 HTML5 原生 history 的拓展,它對外輸出三個方法,用以在支持原生 api 的環境和不兼容的環境,還有 node 環境中調用。而該方法返回的就是一個加強的 history api.
  • 寫這篇文章的時候,第一次有感覺到技術棧拓展的無窮魅力。從最初試圖分析 react-router,到發現它依賴的主要的庫 history. 再進行細化,到 history 主要提供的對象方法。裏面涉及的發佈訂閱設計模式、思路、以及具體的實現使用了柯里化方式。一步一步探究下去能夠發現不少有趣的地方。彷佛又喚起往日的技術熱情。
  • 下一篇文章將會繼續介紹 react-router.

佔位坑

  • 下面兩個方法返回的內容和 createBrowserHistory 基本一致,只是具體的實現有部分差異。有時間補上。
  • createHashHistory
  • createMemoryHistory

參考

react-router 的實現原理

react-router 源代碼學習筆記

Javascript設計模式之發佈-訂閱模式

我的網站原文

相關文章
相關標籤/搜索