React Router中的核心history庫的詳細分析

來源:互聯網
上載者:User
這篇文章給大家介紹的內容是關於React Router中的核心history庫的詳細分析,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所協助。

前言

使用React開發稍微複雜一點的應用,React Router幾乎是路由管理的唯一選擇。雖然React Router經曆了4個大版本的更新,功能也越來越豐富,但無論怎麼變,它的核心依賴history庫卻一直沒變。下面我們來瞭解下這個在github上有4k+星的庫到底提供了什麼功能。

HTML5 history對象

聊到history庫,是不是覺得這個單詞有點熟悉?不錯,HTML5規範裡面,也新增了一個同名的history對象。下面我們來看下這個history對象用來解決什麼問題。

在jQuery統治前端的年代,通過ajax請求無重新整理更新頁面是當時相當流行的頁面處理方式,SPA的雛形就是那時候演化出來的。為了標示頁面發生的變化,方便重新整理後依然能顯示正確的頁面元素,一般會通過改變url的hash值來唯一定位頁面。但這會帶來另一個問題:使用者無法使用前進/後退來切換頁面。

為瞭解決這個問題,history對象應運而生。當頁面的url或者hash發生變化的時候,瀏覽器會自動將新的url push到history對象中。history對象內部會維護一個state數組,記錄url的變化。在瀏覽器進行前進/後退操作的時候,實際上就是調用history對象的對應方法(forward/back),取出對應的state,從而進行頁面的切換。

除了操作url,history對象還提供2個不用通過操作url也能更新內部state的方法,分別是pushStatereplaceState。還能將額外的資料存到state中,然後在onpopstate事件中再通過event.state取出來。如果希望對history對象作更深入的理解,可以參考 這裡,和這裡。

history庫與HTML5 history對象的關係

我們再回過頭來看history庫。它本質上做了以下4件事情:

  1. 借鑒HTML5 history對象的理念,在其基礎上又擴充了一些功能

  2. 提供3種類型的history:browserHistory,hashHistory,memoryHistory,並保持統一的api

  3. 支援發布/訂閱功能,當history發生改變的時候,可以自動觸發訂閱的函數

  4. 提供跳轉攔截、跳轉確認和basename等實用功能

再對比一些兩者api的異同。以下是history庫的:

const history = {    length,        // 屬性,history中記錄的state的數量    action,        // 屬性,當前置航的action類型    location,      // 屬性,location對象,封裝了pathname、search和hash等屬性    push,          // 方法,導航到新的路由,並記錄在history中    replace,       // 方法,替換掉目前記錄在history中的路由資訊    go,            // 方法,前進或後退n個記錄    goBack,        // 方法,後退    goForward,     // 方法,前進    canGo,         // 方法,是否能前進或後退n個記錄    block,         // 方法,跳轉前讓使用者確定是否要跳轉    listen         // 方法,訂閱history變更事件  };

以下是HTML5 history對象的:

const history = {    length,         // 屬性,history中記錄的state的數量    state,          // 屬性,pushState和replaceState時傳入的對象    back,           // 方法,後退    forward,        // 方法,前進    go,             // 方法,前進或後退n個記錄    pushState,      // 方法,導航到新的路由,並記錄在history中    replaceState    // 方法,替換掉目前記錄在history中的路由資訊}// 訂閱history變更事件window.onpopstate = function (event) {    ...}

從對比中可以看出,兩者的關係是非常密切的,history庫可以說是history對象的超集,是功能更強大的history對象。

createHashHistory源碼分析

下面,我們以三種history類型中的一種,hashHistory為例,來分析下history的源碼,看看它都幹了些什麼。先看下它是怎麼處理hash變更的。

// 構造hashHistory對象const createHashHistory = (props = {}) => {    ...    const globalHistory = window.history;    // 引用HTML5 history對象    ...    // transitionManager負責控制是否進行跳轉,以及跳轉後要通知到的訂閱者,後面會詳細討論    const transitionManager = createTransitionManager();    ...    // 註冊history變更回調的訂閱者    const listen = listener => {        const unlisten = transitionManager.appendListener(listener);        checkDOMListeners(1);        return () => {            checkDOMListeners(-1);            unlisten();        };    };        // 監聽hashchange事件    const checkDOMListeners = delta => {        listenerCount += delta;        if (listenerCount === 1) {            window.addEventListener(HashChangeEvent, handleHashChange);        } else if (listenerCount === 0) {            window.removeEventListener(HashChangeEvent, handleHashChange);        }    };        // hashchange事件回調    const handleHashChange = () => {        ...        // 構造內部使用的location對象,包含pathname、search和hash等屬性        const location = getDOMLocation();            ...        handlePop(location);    };        // 處理hash變更邏輯    const handlePop = location => {        ...        const action = "POP";        // 給使用者展示確認跳轉的資訊(如果有的話),確認後通知訂閱者。如果使用者取消跳轉,則回退到之前狀態        transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => {            if (ok) {                setState({action, location});    // 確認後通知訂閱者            } else {                revertPop(location);             // 取消則回退到之前狀態            }        });    };        // 更新action,location和length屬性,並通知訂閱者    const setState = nextState => {        Object.assign(history, nextState);        history.length = globalHistory.length;        transitionManager.notifyListeners(history.location, history.action);    };    ...}

以上就是處理被動的hash變更的邏輯,一句話概括就是:訂閱hash變更事件,判斷是否確實要變更,如需變更則更新自己的屬性,通知訂閱者,不需變更則回退到之前的狀態。

下面再看下transitionManager做了什麼,重點看發布/訂閱相關內容,忽略使用者確認跳轉相關內容。

const createTransitionManager = () => {    ...    // 內部維護的訂閱者列表    let listeners = [];    // 註冊訂閱者    const appendListener = fn => {        let isActive = true;        const listener = (...args) => {            if (isActive) fn(...args);        };        listeners.push(listener);        return () => {            isActive = false;            listeners = listeners.filter(item => item !== listener);        };    };    //通知訂閱者    const notifyListeners = (...args) => {        listeners.forEach(listener => listener(...args));    };    ...}

這裡的代碼一目瞭然,就是維護一個訂閱者列表,當hash變更的時候通知到相關的函數。

以上是hash改變的時候被動更新相關的內容,下面再看下主動更新相關的代碼,以push為例,replace大同小異。

const push = (path, state) => {    ...    const action = "PUSH";    const location = createLocation(path, undefined, undefined, history.location);    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => {        if (!ok)     // 如果取消,則不跳轉            return;        ...        pushHashPath(encodedPath);        // 用新的hash替換到url當中        ...        setState({action, location});     // 更新action,location和length屬性,並通知訂閱者    });};// 用新的hash替換到url當中const pushHashPath = path => (window.location.hash = path);

在瀏覽器進行前進後退操作時,history庫實際上是通過操作HTML5 history對象實現的。

const globalHistory = window.history;const go = n => {    ...    globalHistory.go(n);};const goBack = () => go(-1);const goForward = () => go(1);

當調用window.history.go的時候,hash會發生變化,進而觸發hashchange事件,然後history庫再將變更通知到相關的訂閱者。

總結

本文對React Router核心依賴history庫進行了比較深入的介紹。從HTML5新增的history對象講起,對比了它跟history庫千絲萬縷的關係,並以hashHistory為例子詳細分析了其代碼的實現細節。

最後,我們再來回顧一下history庫做了哪些事情:

  1. 借鑒HTML5 history對象的理念,在其基礎上又擴充了一些功能

  2. 提供3種類型的history:browserHistory,hashHistory,memoryHistory,並保持統一的api

  3. 支援發布/訂閱功能,當history發生改變的時候,可以自動觸發訂閱的函數

  4. 提供跳轉攔截、跳轉確認和basename等實用功能

雖然history庫是React Router的核心依賴,但它跟React本身並沒有依賴關係。如果你的項目中有操作history的情境,也可以將其引入到項目中來。

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.