history源碼解析-管理會話歷史記錄

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

1.前言

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

本篇讀後感分爲五部分,分別爲前言、使用、解析、demo、總結,五部分互不相連可根據須要分開看。java

前言爲介紹、使用爲庫的使用、解析爲源碼的解析、demo是抽取源碼的核心實現的小demo,總結爲吹水,學以至用。react

建議跟着源碼結合本文閱讀,這樣更加容易理解!git

  1. history
  2. history解析的Github地址
  3. 手把手帶你上react-router的history車(掘金)

2.使用

history有三種不一樣的方法建立history對象,取決於你的代碼環境:github

  1. createBrowserHistory:支持HTML5 history api的現代瀏覽器(例如:/index);
  2. createHashHistory:傳統瀏覽器(例如:/#/index);
  3. createMemoryHistory:沒有Dom的環境(例如:NodeReact Native)。

注意:本片文章只解析createBrowserHistory,其實三種構造原理都是差很少的api

<!DOCTYPE html>
<html>
  <head>
    <script src="./umd/history.js"></script>
    <script> var createHistory = History.createBrowserHistory // var createHistory = History.createHashHistory 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="page++; h.push('/#/' + page)">history.push</button> -->

      <button onclick="h.goBack()">history.goBack</button>
    </p>
  </body>
</html>
複製代碼

block用於地址改變以前的截取,listener用於監聽地址欄的改變,pushreplacego(n)等用於跳轉,用法簡單明瞭數組

3.解析

貼出來的源碼我會刪減對理解原理不重要的部分!!!若是想看完整的請下載源碼看哈瀏覽器

從history的源碼庫目錄能夠看到modules文件夾,包含了幾個文件:react-router

  1. createBrowserHistory.js 建立createBrowserHistory的history對象;
  2. createHashHistory.js 建立createHashHistory的history對象;
  3. createMemoryHistory.js 建立createMemoryHistory的history對象;
  4. createTransitionManager.js 過渡管理(例如:處理block函數中的彈框、處理listener的隊列);
  5. DOMUtils.js Dom工具類(例如彈框、判斷瀏覽器兼容性);
  6. index.js 入口文件;
  7. LocationUtils.js 處理Location工具;
  8. PathUtils.js 處理Path工具。

入口文件index.js

export { default as createBrowserHistory } from "./createBrowserHistory";
export { default as createHashHistory } from "./createHashHistory";
export { default as createMemoryHistory } from "./createMemoryHistory";
export { createLocation, locationsAreEqual } from "./LocationUtils";
export { parsePath, createPath } from "./PathUtils";
複製代碼

把全部須要暴露的方法根據文件名區分開,咱們先看history的構造函數createBrowserHistory

3.1 createBrowserHistory

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  // 瀏覽器的history
  const globalHistory = window.history;
  // 初始化location
  const initialLocation = getDOMLocation(window.history.state);
  // 建立地址
  function createHref(location) {
    return basename + createPath(location);
  }

  ...

  const history = {
    // window.history屬性長度
    length: globalHistory.length,

    // history 當前行爲(包含PUSH-進入、POP-彈出、REPLACE-替換)
    action: "POP",

    // location對象(與地址有關)
    location: initialLocation,

    // 當前地址(包含pathname)
    createHref,

    // 跳轉的方法
    push,
    replace,
    go,
    goBack,
    goForward,

    // 截取
    block,

    // 監聽
    listen
  };

  return history;
}

export default createBrowserHistory;
複製代碼

不管是從代碼仍是從用法上咱們也能夠看出,執行了createBrowserHistory後函數會返回history對象,history對象提供了不少屬性和方法,最大的疑問應該是initialLocation函數,即history.location。咱們的解析順序以下:

  1. location;
  2. createHref;
  3. block;
  4. listen;
  5. push;
  6. replace。

3.2 location

location屬性存儲了與地址欄有關的信息,咱們對比下createBrowserHistory的返回值history.locationwindow.location

// history.location
history.location = {
  hash: ""
  pathname: "/history/index.html"
  search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  state: undefined
}

// window.location
window.location = {
  hash: ""
  host: "localhost:63342"
  hostname: "localhost"
  href: "http://localhost:63342/history/index.html?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  origin: "http://localhost:63342"
  pathname: "/history/index.html"
  port: "63342"
  protocol: "http:"
  reload: ƒ reload()
  replace: ƒ ()
  search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
}
複製代碼

結論是history.location是window.location的兒砸!咱們來研究研究做者是怎麼處理的。

const initialLocation = getDOMLocation(window.history.state)
複製代碼

initialLocation函數等於getDOMLocation函數的返回值(getDOMLocationhistory中會常常調用,理解好這個函數比較重要)。

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  // 處理basename(相對地址,例如:首頁爲index,假如設置了basename爲/the/base,那麼首頁爲/the/base/index)
  const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : "";
  
  const initialLocation = getDOMLocation(window.history.state);

  // 處理state參數和window.location
  function getDOMLocation(historyState) {
    const { key, state } = historyState || {};
    const { pathname, search, hash } = window.location;

    let path = pathname + search + hash;

    // 保證path是不包含basename的
    if (basename) path = stripBasename(path, basename);

    // 建立history.location對象
    return createLocation(path, state, key);
  };

  const history = {
    // location對象(與地址有關)
    location: initialLocation,
    ...
  };

  return history;
}
複製代碼

通常大型的項目中都會把一個功能拆分紅至少兩個函數,一個專門處理參數的函數和一個接收處理參數實現功能的函數:

  1. 處理參數:getDOMLocation函數主要處理statewindow.location這兩參數,返回自定義的history.location對象,主要構造history.location對象是createLocation函數;
  2. 構造功能:createLocation實現具體構造location的邏輯。

接下來咱們看在LocationUtils.js文件中的createLocation函數

// LocationUtils.js
import { parsePath } from "./PathUtils";

export function createLocation(path, state, key, currentLocation) {
  let location;
  if (typeof path === "string") {
    // 兩個參數 例如: push(path, state)

    // parsePath函數用於拆解地址 例如:parsePath('www.aa.com/aa?b=bb') => {pathname: 'www.aa.com/aa', search: '?b=bb', hash: ''}
    location = parsePath(path);
    location.state = state;
  } else {
    // 一個參數 例如: push(location)
    location = { ...path };

    location.state = state;
  }

  if (key) location.key = key;

  // location = {
  // hash: ""
  // pathname: "/history/index.html"
  // search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  // state: undefined
  // }
  return location;
}

// PathUtils.js
export function parsePath(path) {
  let pathname = path || "/";
  let search = "";
  let hash = "";

  const hashIndex = pathname.indexOf("#");
  if (hashIndex !== -1) {
    hash = pathname.substr(hashIndex);
    pathname = pathname.substr(0, hashIndex);
  }

  const searchIndex = pathname.indexOf("?");
  if (searchIndex !== -1) {
    search = pathname.substr(searchIndex);
    pathname = pathname.substr(0, searchIndex);
  }

  return {
    pathname,
    search: search === "?" ? "" : search,
    hash: hash === "#" ? "" : hash
  };
}
複製代碼

createLocation根據傳遞進來的path或者location值,返回格式化好的location,代碼簡單。

3.3 createHref

createHref函數的做用是返回當前路徑名,例如地址http://localhost:63342/history/index.html?a=1,調用h.createHref(location)後返回/history/index.html?a=1

// createBrowserHistory.js
import {createPath} from "./PathUtils";

function createBrowserHistory(props = {}){
  // 處理basename(相對地址,例如:首頁爲index,假如設置了basename爲/the/base,那麼首頁爲/the/base/index)
  const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : "";

  function createHref(location) {
    return basename + createPath(location);
  }
  
  const history = {
    // 當前地址(包含pathname)
    createHref,
    ...
  };

  return history;
}

// PathUtils.js
function createPath(location) {
  const { pathname, search, hash } = location;

  let path = pathname || "/";
  
  if (search && search !== "?") path += search.charAt(0) === "?" ? search : `?${search}`;

  if (hash && hash !== "#") path += hash.charAt(0) === "#" ? hash : `#${hash}`;

  return path;
}
複製代碼

3.4 listen

在這裏咱們能夠想象下大概的 監聽 流程:

  1. 綁定咱們設置的監聽函數;
  2. 監聽歷史記錄條目的改變,觸發監聽函數。

第二章使用代碼中,建立了History對象後使用了h.listen函數。

// index.html
h.listen(function (location) {
  console.log(location, 'lis-1')
})
h.listen(function (location) {
  console.log(location, 'lis-2')
})
複製代碼

可見listen能夠綁定多個監聽函數,咱們先看做者的createTransitionManager.js是如何實現綁定多個監聽函數的。

createTransitionManager是過渡管理(例如:處理block函數中的彈框、處理listener的隊列)。代碼風格跟createBrowserHistory幾乎一致,暴露全局函數,調用後返回對象便可使用。

// createTransitionManager.js
function createTransitionManager() {
  let listeners = [];

  // 設置監聽函數
  function appendListener(fn) {
    let isActive = true;

    function listener(...args) {
      // good
      if (isActive) fn(...args);
    }

    listeners.push(listener);

    // 解除
    return () => {
      isActive = false;
      listeners = listeners.filter(item => item !== listener);
    };
  }

  // 執行監聽函數
  function notifyListeners(...args) {
    listeners.forEach(listener => listener(...args));
  }

  return {
    appendListener,
    notifyListeners
  };
}
複製代碼
  1. 設置監聽函數appendListenerfn就是用戶設置的監聽函數,把全部的監聽函數存儲在listeners數組中;
  2. 執行監聽函數notifyListeners:執行的時候僅僅須要循環依次執行便可。

這裏感受有值得借鑑的地方:添加隊列函數時,增長狀態管理(如上面代碼的isActive),決定是否啓用。

有了上面的理解,下面看listen源碼。

// createBrowserHistory.js
import createTransitionManager from "./createTransitionManager";
const transitionManager = createTransitionManager();

function createBrowserHistory(props = {}){
  function listen(listener) {
    // 添加 監聽函數 到 隊列
    const unlisten = transitionManager.appendListener(listener);

    // 添加 歷史記錄條目 的監聽
    checkDOMListeners(1);

    // 解除監聽
    return () => {
      checkDOMListeners(-1);
      unlisten();
    };
  }

  const history = {
    // 監聽
    listen
    ...
  };

  return history;
}


複製代碼

history.listen是當歷史記錄條目改變時,觸發回調監聽函數。因此這裏有兩步:

  1. transitionManager.appendListener(listener)把回調的監聽函數添加到隊列裏;
  2. checkDOMListeners監聽歷史記錄條目的改變;

下面看看如何歷史記錄條目的改變checkDOMListeners(1)

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  let listenerCount = 0;

  function checkDOMListeners(delta) {
    listenerCount += delta;
    
    // 是否已經添加
    if (listenerCount === 1 && delta === 1) {
      // 添加綁定,當歷史記錄條目改變的時候
      window.addEventListener('popstate', handlePopState);
    } else if (listenerCount === 0) {
      // 解除綁定
      window.removeEventListener('popstate', handlePopState);
    }
  }
  
  // getDOMLocation(event.state) = location = {
  // hash: ""
  // pathname: "/history/index.html"
  // search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  // state: undefined
  // }
  function handlePopState(event) {
    handlePop(getDOMLocation(event.state));
  }
  
  function handlePop(location) {
    const action = "POP";
    setState({ action, location })
  }
}
複製代碼

雖然做者寫了不少很細的回調函數,可能會致使有些很差理解,但細細看仍是有它道理的:

  1. checkDOMListeners:全局只能有一個監聽歷史記錄條目的函數(listenerCount來控制);
  2. handlePopState:必須把監聽函數提取出來,否則不能解綁;
  3. handlePop:監聽歷史記錄條目的核心函數,監聽成功後執行setState

setState({ action, location })做用是根據當前地址信息(location)更新history。

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  function setState(nextState) {
    // 更新history
    Object.assign(history, nextState);
    history.length = globalHistory.length;

    // 執行監聽函數listen
    transitionManager.notifyListeners(history.location, history.action);
  }

  const history = {
    // 監聽
    listen
    ...
  };

  return history;
}
複製代碼

在這裏,當更改歷史記錄條目成功後:

  1. 更新history;
  2. 執行監聽函數listen;

這就是h.listen的主要流程了,是否是還挺簡單的。

3.5 block

history.block的功能是當歷史記錄條目改變時,觸發提示信息。在這裏咱們能夠想象下大概的 截取 流程:

  1. 綁定咱們設置的截取函數;
  2. 監聽歷史記錄條目的改變,觸發截取函數。

哈哈這裏是否是感受跟listen函數的套路差很少呢?其實h.listenh.block的監聽歷史記錄條目改變的代碼是公用同一套(固然拉只能綁定一個監聽歷史記錄條目改變的函數),3.1.3爲了方便理解我修改了部分代碼,下面是完整的源碼。


第二章使用代碼中,建立了History對象後使用了h.block函數(只能綁定一個block函數)。

// index.html
h.block(function (location, action) {
  return 'Are you sure you want to go to ' + location.path + '?'
})
複製代碼

一樣的咱們先看看做者的createTransitionManager.js是如何實現提示的。

createTransitionManager是過渡管理(例如:處理block函數中的彈框、處理listener的隊列)。代碼風格跟createBrowserHistory幾乎一致,暴露全局函數,調用後返回對象便可使用。

// createTransitionManager.js
function createTransitionManager() {
  let prompt = null;

  // 設置提示
  function setPrompt(nextPrompt) {
    prompt = nextPrompt;

    // 解除
    return () => {
      if (prompt === nextPrompt) prompt = null;
    };
  }

  /** * 實現提示 * @param location:地址 * @param action:行爲 * @param getUserConfirmation 設置彈框 * @param callback 回調函數:block函數的返回值做爲參數 */
  function confirmTransitionTo(location, action, getUserConfirmation, callback) {
    if (prompt != null) {
      const result = typeof prompt === "function" ? prompt(location, action) : prompt;

      if (typeof result === "string") {
        // 方便理解我把源碼getUserConfirmation(result, callback)直接替換成callback(window.confirm(result))
        callback(window.confirm(result))
      } else {
        callback(result !== false);
      }
    } else {
      callback(true);
    }
  }

  return {
    setPrompt,
    confirmTransitionTo
    ...
  };
}
複製代碼

setPromptconfirmTransitionTo的用意:

  1. 設置提示setPrompt:把用戶設置的提示信息函數存儲在prompt變量;
  2. 實現提示confirmTransitionTo:
    1. 獲得提示信息:執行prompt變量;
    2. 提示信息後的回調:執行callback把提示信息做爲結果返回出去。

下面看h.block源碼。

// createBrowserHistory.js
import createTransitionManager from "./createTransitionManager";
const transitionManager = createTransitionManager();

function createBrowserHistory(props = {}){
  let isBlocked = false;

  function block(prompt = false) {
    // 設置提示
    const unblock = transitionManager.setPrompt(prompt);

    // 是否設置了block
    if (!isBlocked) {
      checkDOMListeners(1);
      isBlocked = true;
    }

    // 解除block函數
    return () => {
      if (isBlocked) {
        isBlocked = false;
        checkDOMListeners(-1);
      }

      // 消除提示
      return unblock();
    };
  }

  const history = {
    // 截取
    block,
    ...
  };

  return history;
}
複製代碼

history.block的功能是當歷史記錄條目改變時,觸發提示信息。因此這裏有兩步:

  1. transitionManager.setPrompt(prompt) 設置提示;
  2. checkDOMListeners 監聽歷史記錄條目改變的改變。

這裏感受有值得借鑑的地方:調用history.block,它會返回一個解除監聽方法,只要調用一下返回函數便可解除監聽或者復原(有趣)。


咱們看看監聽歷史記錄條目改變函數checkDOMListeners(1)(注意:transitionManager.confirmTransitionTo)。

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  function block(prompt = false) {
    // 設置提示
    const unblock = transitionManager.setPrompt(prompt);

    // 是否設置了block
    if (!isBlocked) {
      checkDOMListeners(1);
      isBlocked = true;
    }

    // 解除block函數
    return () => {
      if (isBlocked) {
        isBlocked = false;
        checkDOMListeners(-1);
      }

      // 消除提示
      return unblock();
    };
  }

  let listenerCount = 0;

  function checkDOMListeners(delta) {
    listenerCount += delta;
    
    // 是否已經添加
    if (listenerCount === 1 && delta === 1) {
      // 添加綁定,當地址欄改變的時候
      window.addEventListener('popstate', handlePopState);
    } else if (listenerCount === 0) {
      // 解除綁定
      window.removeEventListener('popstate', handlePopState);
    }
  }
  
  // getDOMLocation(event.state) = location = {
  // hash: ""
  // pathname: "/history/index.html"
  // search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  // state: undefined
  // }
  function handlePopState(event) {
    handlePop(getDOMLocation(event.state));
  }
  
  function handlePop(location) {
    // 不須要刷新頁面
    const action = "POP";

    // 實現提示
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if (ok) {
          // 肯定
          setState({ action, location });
        } else {
          // 取消
          revertPop(location);
        }
      }
    );
  }

  const history = {
    // 截取
    block
    ...
  };

  return history;
}
複製代碼

就是在handlePop函數觸發transitionManager.confirmTransitionTo的(3.1.3我對這裏作了修改成了方便理解)。


transitionManager.confirmTransitionTo的回調函數callback有兩條分支,用戶點擊提示框的肯定按鈕或者取消按鈕:

  1. 當用戶點擊提示框的肯定後,執行setState({ action, location })
  2. 當用戶點擊提示框的取消後,執行revertPop(location)(忽略)。

到這裏已經瞭解完h.block函數、h.listencreateTransitionManager.js。接下來咱們繼續看另外一個重要的函數h.push

3.6 push

function createBrowserHistory(props = {}){
  function push(path, state) {
    const action = "PUSH";
    // 構造location
    const location = createLocation(path, state, createKey(), history.location);

    // 執行block函數,彈出框
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if (!ok) return;

        // 獲取當前路徑名
        const href = createHref(location);
        const { key, state } = location;

        // 添加歷史條目
        globalHistory.pushState({ key, state }, null, href);
        
        if (forceRefresh) {
          // 強制刷新
          window.location.href = href;
        } else {
          // 更新history
          setState({ action, location });
        }
      }
    );
  }

  const history = {
    // 跳轉
    push,
    ...
  };

  return history;
}
複製代碼

這裏最重要的是globalHistory.pushState函數,它直接添加新的歷史條目。

3.7 replace

function createBrowserHistory(props = {}){
  function replace(path, state) {
    const action = "REPLACE";
    // 構造location
    const location = createLocation(path, state, createKey(), history.location);

    // 執行block函數,彈出框
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if (!ok) return;
        // 獲取當前路徑名
        const href = createHref(location);
        const { key, state } = location;

        globalHistory.replaceState({ key, state }, null, href);

        if (forceRefresh) {
          window.location.replace(href);
        } else {
          setState({ action, location });
        }
      }
    );
  }

  const history = {
    // 跳轉
    replace,
    ...
  };

  return history;
}
複製代碼

其實pushreplace的區別就是history.pushStatehistory.replaceState的區別。

3.8 go

function createBrowserHistory(props = {}){
   function go(n) {
    globalHistory.go(n);
  }

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

  function goForward() {
    go(1);
  }

  const history = {
    // 跳轉
    go,
    goBack,
    goForward,
    ...
  };

  return history;
}
複製代碼

其實就是history.go的運用。

4.demo

手把手帶你上react-router的history車(掘金)

5.總結

總的來講,若是不須要block的話,原生方法能夠知足。最主要仍是對history.pushStatehistory.replaceStatehistory.go(n)popstate方法的運用。公司加班嚴重,利用僅剩的時間擴充下本身的知識面,最好的方法那就是閱讀源碼了哈哈。開始總會有點困難,第一次讀一臉懵逼,第二次讀二臉懵逼,第三次讀有點懵逼,第四次讀這b牛逼~。只要堅持下多寫點測試用例慢慢理解就行了,加油!

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