手把手帶你上react-router的history車

接上一篇history源碼解析-管理會話歷史記錄,本篇教你手寫history,重在理解其原理。javascript

history是一個JavaScript庫,可以讓你在JavaScript運行的任何地方輕鬆管理會話歷史記錄html

1.前言

history是由Facebook維護的,react-router依賴於history,區別於瀏覽器的window.historyhistory是包含window.history的,讓開發者能夠在任何環境都能使用history的api(例如NodeReact Native等)。本文中history指倉庫的對象,window.history指瀏覽器的對象。java

本篇讀後感分爲五部分,分別爲前言、使用、原理、上手、總結,推薦順序閱讀哈哈。react

附上地址git

  1. history
  2. history解析的Github地址
  3. history demo的Github地址
  4. history源碼解析-管理會話歷史記錄(掘金)

2.使用

<!DOCTYPE html>
<html>
  <head>
    <script src="history.js"></script>
    <script> var createHistory = History.createBrowserHistory var page = 0 // createHistory建立所須要的history對象 var h = createHistory() // h.block觸發在地址欄改變以前,用於告知用戶地址欄即將改變 h.block(function (location, action) { return 'Are you sure you want to go to ' + location.path + '?' }) // h.listen監聽當前地址欄的改變 h.listen(function (location) { console.log(location, 'lis-1') }) </script>
  </head>
  <body>
    <p>Use the two buttons below to test normal transitions.</p>
    <p>
      <!-- h.push用於跳轉 -->
      <button onclick="page++; h.push('/' + page, { page: page })">history.push</button>
      <button onclick="h.goBack()">history.goBack</button>
    </p>
  </body>
</html>
複製代碼

history用法:github

  1. block用於地址改變以前的截取;
  2. listener用於監聽地址欄的改變;
  3. push添加新歷史記錄;
  4. replace替換當前歷史記錄;
  5. go(n)跳轉到某條歷史記錄;
  6. goBack返回上一條歷史記錄。

3.原理

圖解

解釋:api

  1. pushreplacegogoBack:倉庫history的方法
  2. pushStatereplaceStatewindow.history的方法,用於修改window.history歷史記錄
  3. popstate:監聽歷史記錄的改變window.addEventListener('popstate', callback)
  4. forceNextPop:自定義變量,用於判斷是否跳過彈出框
  5. allKeys:自定義變量,它跟歷史記錄是同步的。每當修改歷史記錄,都會維護這個數組,用於當彈出框點擊取消時,能夠返回到上次歷史記錄
  6. go(toIndex - fromIndex):彈出框取消時,返回上一次歷史記錄

當活動歷史記錄條目更改時,將觸發popstate事件。須要注意的是調用history.pushState()或history.replaceState()不會觸發popstate事件。只有在作出瀏覽器動做時,纔會觸發該事件,如用戶點擊瀏覽器的回退按鈕(或者在Javascript代碼中調用history.back())數組

路線1(pushreplace瀏覽器

  1. 用戶調用push
  2. 彈出彈出框;
    1. 點肯定按鈕:
      1. 調用window.history.pushState添加歷史記錄,把key存儲到window.history中(注意這個時候不會觸發popstate監聽函數);
      2. 地址改變;
      3. 維護自定義變量allKeys,添加keyallKeys數組。
    2. 點取消按鈕:不操做。

線路2(gogoBackreact-router

  1. 用戶調用go
  2. 修改歷史記錄window.history,地址改變;
  3. 觸發popstate歷史記錄監聽函數(若是綁定了popstate監聽函數);
  4. 彈出彈出框;
    1. 點肯定按鈕:
      1. 更新history(保證history是最新的信息,例如history.location是當前地址信息)。
    2. 點取消按鈕(由於在第二步的時候地址已經跳轉了,點彈出框的取消意圖就須要回到以前的記錄):
      1. 計算toIndex:跳轉前的地址(取history.location的值,由於此時的history.location還沒有更新是舊值);
      2. 計算fromIndex:當前地址的key
      3. 計算二者的差值,調用go方法跳回去上次歷史記錄。

這基本是history的原理了,應該會有些同窗存在疑惑,調用彈出框這個能夠放在調用go以前,一樣能達到效果,並且代碼會更加簡潔且不須要維護allKeys這個數組。我以前也有這個疑問,但仔細想一想,go函數並不包含全部歷史記錄改變的操做,若是用戶左滑動返回上一個頁面呢,那樣就達不到效果了。因此必須在監聽歷史記錄改變後,才能觸發彈出框,當點擊彈出框的取消按鈕後,只能採用維護allKeys數組的方式來返回上一頁。

4.demo

代碼都有註釋,100多行代碼模仿history寫了個簡易閹割版,目的是爲了瞭解history的原理,應該很容易就看懂的。

(function(w){
  let History = {
    createBrowserHistory
  }

  function createBrowserHistory(){
    // key
    function createKey() {
      return Math.random().toString(36).substr(2, 6);
    }

    // 獲取地址信息
    function getDOMLocation(historyState = {}) {
      const { key, state } = historyState || {};
      const { pathname, search, hash } = window.location;

      return {pathname, search, hash, key};
    }
    // location地址信息
    let initialLocation = getDOMLocation()

    // 初始化allKeys
    let allKeys = [initialLocation.key]

    // listen數組
    let listener = []
    // 監聽
    function listen(fn){
      listener.push(fn)

      checkDOMListeners()
    }

    // 只能添加一個監聽歷史條目改變的函數
    let isListener = false
    function checkDOMListeners(){
      if (!isListener) {
        isListener = true
        window.addEventListener('popstate', handlePop)
      }
    }

    // 跳過block。由於當點擊彈出框的取消後,會執行go,而後會再一次執行handlePop函數,這次要跳過
    let forceNextPop = false
    // 監聽歷史條目改變
    function handlePop(event){
      let location = getDOMLocation(event.state)
      if (forceNextPop) {
        forceNextPop = false
      } else {
        // 彈出框
        let isComfirm = prompt && window.confirm(prompt(window.location)) && true

        if (isComfirm) {
          // 肯定
          // 更新history
          Object.assign(history, {location, length: history.length})
        } else {
          // 取消
          // 獲取當前的history.key和上一次的location.key比較,而後進行回跳
          let toIndex = allKeys.indexOf(history.location.key)
          toIndex = toIndex === -1 ? 0 : toIndex

          let fromIndex = allKeys.indexOf(location.key)
          fromIndex = fromIndex === -1 ? 0 : fromIndex

          // 差值
          let delta = toIndex - fromIndex

          // 差值爲0不跳
          if (delta) {
            forceNextPop = true;
            go(delta);
          }
        }
      }
    }

    // 截取函數
    let prompt = null
    function block(fn){
      prompt = fn
    }

    // push
    function push(href){
      let isComfirm = prompt && window.confirm(prompt(window.location)) && true

      if (isComfirm) {
        let key = createKey()
        // 更新allKeys數組
        allKeys.push(key)
        // 更新歷史條目
        w.history.pushState({key}, null, href)
        
        // 獲取當前最新的location信息
        let location = getDOMLocation({key})

        // 更新history
        Object.assign(history, {location, length: history.length})
      }
    }

    // go
    function go(n){
      w.history.go(n)
    }

    // goBack
    function goBack(){
      go(-1);
    }

    let history = {
      length: w.history.length,
      listen,
      block,
      push,
      go,
      goBack,
      location: initialLocation
    }
    return history
  }

  w.History = History
})(window)
複製代碼

5.總結

學代碼必須手寫,學英語必須開口,學習必須主動!

fafa
相關文章
相關標籤/搜索