preface

The React-Router V6 stable version has been available for some time now, with major API changes, a code package size that has been reduced by more than half (20K => 8K), and 1600 lines of source code. Since the concepts of V5 version cannot be used in V6 version, it is also a good opportunity to learn the source code. Therefore, the author in-depth the react-Router and its surrounding ecology, and roughly compiled two articles (which rely on the source code parsing of the history library and the React-Router core repository).

This is the first article to make an in-depth analysis of the warehouse of History.

history

This article is based on History V5.2.0

History is a library for managing session history, including browser history.

React-router currently relies on the react-Router version of History 5.x. The History library is also developed by the React-Router team. It contains a series of functions to operate the browser history stack and provides three different methods for creating the history navigation:

  • createBrowserHistoryBrowser-basedhistoryObject latest API.
  • createHashHistory: Hash parameter based on the browser URL.
  • createMemoryHistory: Based on memory stack, independent of any platform.

The history object created by the above three methods is used in the React-Router as a navigator for the three main routes:

  • BrowserRouterThe correspondingcreateBrowserHistoryBy thereact-router-domTo provide.
  • HashRouterThe correspondingcreateHashHistoryBy thereact-router-domTo provide.
  • MemoryRouterThe correspondingcreateMemoryHistoryBy thereact-routerProvided primarily forreact-nativeAnd other memory-based routing systems.

Note:

  • In fact withreact-nativeCorresponding packagereact-router-nativeUsing theNativeRouter, but in factNativeRouterisMemoryRouterSimple encapsulation of (changed name).
    export interface NativeRouterProps extends MemoryRouterProps {}
    
    /** * A < router > that runs on react Native. */
    export function NativeRouter(props: NativeRouterProps) {
      return <MemoryRouter {. props} / >;
    }
    Copy the code
  • inreact-router-domThere is actually another route inStaticRouterBut it is used inssrIn, no dependencyhistoryLibrary, just checking the props passed in.
    // Specify the import mode
    import { StaticRouter } from 'react-router-dom/server'
    Copy the code

    react-router-domIt was added in V6.1.1HistoryRouter, but theRouterMainly to help us pass in manuallyhistoryExamples, I won’t mention them here, but I’ll talk about them laterreacr-router-domWill elaborate.

Action for route switchover

History defines a specific action for each route switch, which is divided into three categories:

  • POP:
  • PUSH:
  • REPLACE:

An enumeration variable is used in the source code, which will be used later in the function wrapper:

export enum Action {
  Pop = 'POP',
  Push = 'PUSH',
  Replace = 'REPLACE'
}
Copy the code

Abstract Path and Location

For the URL of a route jump, History abstracts it at two levels:

  • The first layer is a URL only Path object, which is the result of parsing the URL into Path, Query, and hash parts.

    Here is the definition of the Path object:

    // The following three are the type aliases for the PATH, query, and hash parts of the URL
    export type Pathname = string;
    export type Search = string;
    export type Hash = string;
    
    // Jump to the object corresponding to the URL once
    export interface Path {
      pathname: Pathname;
      search: Search;
      hash: Hash;
    }
    Copy the code

    Here is how to convert a URL to a Path object as provided in History:

    /** * pathName + search + hash creates a full URL */
    export function createPath({
      pathname = '/',
      search = ' ',
      hash = ' '
    }: Partial<Path>) {
      if(search && search ! = ='? ')
        pathname += search.charAt(0) = = ='? ' ? search : '? ' + search;
      if(hash && hash ! = =The '#')
        pathname += hash.charAt(0) = = =The '#' ? hash : The '#' + hash;
      return pathname;
    }
    
    /** * parses the URL and converts it to the Path object */
    export function parsePath(path: string) :Partial<Path> {
      let parsedPath: Partial<Path> = {};
    
      if (path) {
        let hashIndex = path.indexOf(The '#');
        if (hashIndex >= 0) {
          parsedPath.hash = path.substr(hashIndex);
          path = path.substr(0, hashIndex);
        }
    
        let searchIndex = path.indexOf('? ');
        if (searchIndex >= 0) {
          parsedPath.search = path.substr(searchIndex);
          path = path.substr(0, searchIndex);
        }
    
        if(path) { parsedPath.pathname = path; }}return parsedPath;
    }
    Copy the code
  • Another layer is extended from the Path object, which abstracts the behavior of route navigation to form the Location object. In addition to containing the attributes of the Path object, this object also has the context information state associated with each navigation and the unique key value corresponding to this navigation.

    Here is the definition of the Location object:

    // A unique string that matches the location of each jump
    export type Key = string;
    
    // Redirect to an abstract navigation object
    export interface Location extends Path {
      // The state value associated with the current location can be any value passed in manually
      state: unknown;
      // The unique key of the current location is usually automatically generated
      key: Key;
    }
    Copy the code

    Create a unique key internally:

    /** * create a unique key */
    function createKey() {
      return Math.random().toString(36).substr(2.8);
    }
    Copy the code

Understand the History object in depth

Earlier we mentioned that the three navigation methods inside History create three history objects with different properties, but the apis exposed by these History objects are generally the same, so we can start with their common API definitions.

A basic history object contains:

  • Two attributes: the jump behavior corresponding to the current route (action) and navigation objects (location)
  • A tool method:createHrefFor the user tohistoryInternally definedPathObject is converted to the original URL.
    history.createHref({
        pathname: '/home'.search: 'the=query'.hash:'hash'
    }) // Output: /home? the=query#hash
    Copy the code
  • Five route hop methods:push,replace,go,backwithforwardUsed to jump routes in the routing stack.
    // Push a new history navigation onto the history stack and move the current pointer to the history navigation
    history.push('/home');
    // Replace the current route with the newly passed history navigation
    history.replace('/home');
    
    // This method can pass in a Path object and also receive a second argument, state, which can be used to store historical navigation context information in memory
    history.push({
      pathname: '/home'.search: '? the=query'
    }, {
      some: state
    });
    
    // replace as above
    history.replace({
      pathname: '/home'.search: '? the=query'
    }, {
      some: state
    });
    
    // Return to the previous history navigation
    history.go(-1);
    history.back();
    
    // Go to the next historical navigation
    history.go(1);
    history.forward();
    Copy the code
  • Two route listening methods: hooks that listen for route jumps (similar to post guards)listenAnd a hook to block route redirects (if you want to redirects properly must cancel the listening, can be encapsulated as a similar function to the front hook)block.
    // Start listening for route redirects
    let unlisten = history.listen(({ action, location }) = > {
      // The current location changed.
    });
    
    // Cancel the listener
    unlisten();
    
    // Start blocking route redirection
    let unblock = history.block(({ action, location, retry }) = > {
      The // retry method lets us re-enter the route that was blocked from jumping
      For retry to take effect, all block listeners must be cancelled first. Otherwise, block listeners will still be blocked after retry
      unblock();
      retry();
    });
    Copy the code

    aboutblockListening prevents route redirects

    Go (index-nextIndex) is enforced. Index is the index of the original route in the routing stack. NextIndex is the index of a forward route in the routing stack.

The interface for the entire History object is defined in the source code as follows:

// Arguments to the listen callback, which contains the updated Action and Location objects
export interface Update {
  action: Action; // The Action mentioned above
  location: Location; // The Location mentioned above
}

// The definition of the listener function
export interface Listener {
  (update: Update): void;
}


// The block callback argument contains all the values of the LISTEN callback and a retry method
// If you have blocked the page redirect (Blocker listening), use Retry to reenter the page
export interface Transition extends Update {
  /** * re-enter the blocked page */
  retry(): void;
}

/** * Transition object */
export interface Blocker {
  (tx: Transition): void;
}

// Jump links, which can be full urls or Path objects
export type To = string | Partial<Path>;

export interface History {
  // The behavior of the last browser jump is mutable
  readonly action: Action;

  // Mount the current location variable
  readonly location: Location;

  // The utility method converts the to object to a URL string, which internally encapsulates the createPath function mentioned earlier
  createHref(to: To): string;

  // Push a new route to the routing stackpush(to: To, state? :any) :void;
  // Replace the current routereplace(to: To, state? :any) :void;
  // Point the current route to the route at the delta position in the routing stack
  go(delta: number) :void;
  // Point the current route to the previous route
  back(): void;
  // Point the current route to the next route
  forward(): void;

  // Trigger after a page jump, equivalent to a post-hook
  listen(listener: Listener): () = > void;

  // If the history object is not the same as the current history object, it cannot be intercepted
  block(blocker: Blocker): () = > void;
}
Copy the code

Creation of the History object

Earlier we mentioned three methods for creating a History object: createBrowserHistory, createHashHistory, and createMemoryHistory. Among them:

  • createBrowserHistoryUsed to provide users with the creation of a browser-based history APIHistoryObject for most modern browsers (except for a few that don’t support HTML5’s newly added History API, that is, the browser’shistoryObject needs to havepushState,replaceStateandstateIn the production environment, redirection configuration on the server is required for normal use.
  • createHashHistoryUsed to provide the user with a hash value based on the browser URLHistoryObject is generally compatible with almost all browsers using this method, but considering the current development of browsers, in5.xThe internal version is the samecreateBrowserHistory, also uses the latest History API for route jumping (if you really want compatibility with older browsers, you should use this option4.xVersion), and because the browser does not send the HASH value of the URL to the server, the route URL sent by the front end is the same, so the server does not need to do additional configuration.
  • createMemoryHistoryUsed to provide the user with a memory-based systemHistoryObject for any environment where JavaScript can be run (including Node), and the internal routing system is completely at the user’s disposal.

In History, the internal process of creating history objects in these three methods is basically the same, and they only rely on different APIS to realize it internally. Therefore, the internal process of creating history objects in these three methods is analyzed here.

We can start by looking at the types of History objects created by these methods:

export interface BrowserHistory extends History {}
export interface HashHistory extends History {}
export interface MemoryHistory extends History {
  readonly index: number;
}
Copy the code

The types of BrowserHistory and HashHistory are the same as the History objects mentioned earlier, and MemoryHistory has an additional index attribute. MemoryHistory is a memory-based routing system, so we know exactly where the current route is in the History stack. This property tells the user the current memory history stack index (the other two routing objects also have an internal index, but this index does not correspond to the browser history stack index, so it is not exposed to the user, but is used for internal differentiation).

Let’s look at each of the three creation methods themselves:

  • CreateBrowserHistory:
    // The specified window object can be passed as an argument, which defaults to the current window object
    export type BrowserHistoryOptions = { window? : Window };export function createBrowserHistory(
      options: BrowserHistoryOptions = {}
    ) :BrowserHistory {
      let { window = document.defaultView! } = options;
      // Get the browser's history object, based on which methods will be wrapped
      let globalHistory = window.history;
      // Initialize action and location
      let action = Action.Pop;
      let [index, location] = getIndexAndLocation(); // Get the index and location of the current route
    
      // Omit the rest of the code
      
      let history: BrowserHistory = {
        get action() {
          return action;
        },
        get location() {
          return location;
        },
        createHref,
        push,
        replace,
        go,
        back() {
          go(-1);
        },
        forward() {
          go(1);
        },
        listen(listener) {
           // Omit the rest of the code
        },
        block(blocker) {
           // Omit the rest of the code}};return history;
    }
    Copy the code
  • CreateHashHistory:
    // Same as BrowserRouter
    export type HashHistoryOptions = { window? : Window };export function createHashHistory(
      options: HashHistoryOptions = {}
    ) :HashHistory {
      let { window = document.defaultView! } = options;
      // Browsers already have history objects, but HTML5 has added several new apis for state
      let globalHistory = window.history;
      let action = Action.Pop;
      let [index, location] = getIndexAndLocation();
    
      // Omit the rest of the code
      
      let history: HashHistory = {
        get action() {
          return action;
        },
        get location() {
          return location;
        },
        createHref,
        push,
        replace,
        go,
        back() {
          go(-1);
        },
        forward() {
          go(1);
        },
        listen(listener) {
           // Omit the rest of the code
        },
        block(blocker) {
          // Omit the rest of the code}};return history;
    }
    Copy the code
  • CreateMemoryHistory:
    // This is slightly different from BrowserRouter and HashRouter because there is no browser involved, so we need to emulate the history stack
    // A user-supplied object describing the history stack
    export type InitialEntry = string | Partial<Location>;// The Location mentioned above
    
    // Since it is not a real route, the window object is not needed
    export type MemoryHistoryOptions = {
      // Initialize the history stackinitialEntries? : InitialEntry[];// Initialize indexinitialIndex? :number;
    };
    
    
    // Determine the upper and lower limits
    function clamp(n: number, lowerBound: number, upperBound: number) {
      return Math.min(Math.max(n, lowerBound), upperBound);
    }
    
    export function createMemoryHistory(
      options: MemoryHistoryOptions = {}
    ) :MemoryHistory {
      let { initialEntries = ['/'], initialIndex } = options;
      // Convert the initialEntries passed in by the user to an array containing Location objects, which will be used later
      let entries: Location[] = initialEntries.map((entry) = > {
        Freeze (Object. Freeze); // Freeze (Object. Freeze)
        let location = readOnly<Location>({
          pathname: '/'.search: ' '.hash: ' '.state: null.key: createKey(), ... (typeof entry === 'string' ? parsePath(entry) : entry)
        });
        return location;
      });
      // Location is retrieved directly from initialized entries, unlike index
      let action = Action.Pop;
      let location = entries[index];
      The clamp function is used to take the upper and lower values, default to the last location if initialIndex is not passed
      // This is called to normalize the value of initialIndex
      let index = clamp(
        initialIndex == null ? entries.length - 1 : initialIndex,
        0,
        entries.length - 1
      );
    
      // Omit the rest of the code
      let history: MemoryHistory = {
        get index() {
          return index;
        },
        get action() {
          return action;
        },
        get location() {
          return location;
        },
        createHref,
        push,
        replace,
        go,
        back() {
          go(-1);
        },
        forward() {
          go(1);
        },
        listen(listener) {
           // Omit the rest of the code
        },
        block(blocker) {
           // Omit the rest of the code}};return history;
    }
    Copy the code

Ok, we should now be able to see that, internally, these methods encapsulate the methods on the History object and return a History object that meets the criteria we defined earlier, so now we just need to parse each property or method individually.

Action, Location, and Index (MemoryRouter only)

Back in the code above, we can see that the above three properties are set by the GET method. Values defined in this way will call the corresponding GET method when they are fetched, which means that the values of these three properties are fetched in real time and will change indirectly when we call the methods of the History object.

Except for createMemoryHistory, the index and location of the other two methods are obtained by getIndexAndLocation(). Here is the internal logic of the getIndexAndLocation() method:

  • CreateBrowserHistory:
    export function createBrowserHistory(
      options: BrowserHistoryOptions = {}
    ) :BrowserHistory {
      let { window = document.defaultView! } = options;
      let globalHistory = window.history;
    
      /** * get the idX of the current state and location object */
      function getIndexAndLocation() :number.Location] {
        let { pathname, search, hash } = window.location;
        // Get the state of the current browser
        let state = globalHistory.state || {};
        // You can see that many of the following attributes are stored in the state of the history API
        return [
          state.idx,
          readOnly<Location>({
            pathname,
            search,
            hash,
            state: state.usr || null.key: state.key || 'default'})]; }let action = Action.Pop;
      let [index, location] = getIndexAndLocation();
      
      // Initialize index
      if (index == null) {
        index = 0;
        // Call the replaceState method provided by the history API to pass in the index. This is just to initialize the state saved in the browser, without changing the URLglobalHistory.replaceState({ ... globalHistory.state,idx: index }, ' ');
      }
    
      / /...
    
      let history: BrowserHistory = {
        get action() {
          return action;
        },
        get location() {
          return location;
        }
        // ...
      };
    
      return history;
    }
    Copy the code
  • CreateHashHistory:
    export function createHashHistory(
      options: HashHistoryOptions = {}
    ) :HashHistory {
      let { window = document.defaultView! } = options;
      let globalHistory = window.history;
    
     function getIndexAndLocation() :number.Location] {
        // Notice that this is different from browserHistory, it takes hash, and the rest of the logic is the same
        // The parsePath method, as described earlier, parses the URL as a Path object
        let {
          pathname = '/',
          search = ' ',
          hash = ' '
        } = parsePath(window.location.hash.substr(1));
        let state = globalHistory.state || {};
        return [
          state.idx,
          readOnly<Location>({
            pathname,
            search,
            hash,
            state: state.usr || null.key: state.key || 'default'})]; }let action = Action.Pop;
      let [index, location] = getIndexAndLocation();
    
     if (index == null) {
        index = 0; globalHistory.replaceState({ ... globalHistory.state,idx: index }, ' ');
      }
      / /...
    
      let history: HashHistory = {
        get action() {
          return action;
        },
        get location() {
          return location;
        }
        // ...
      };
    
      return history;
    }
    Copy the code

createHref

Function of createHref method is simple, it is mainly used in the history of the definition of internal To the object (the type To = string | Partial < Path >) back To the url string:

  • CreateBrowserHistory:
    export function createBrowserHistory(
      options: BrowserHistoryOptions = {}
    ) :BrowserHistory {
       // ...
       // BrowserHistory is just a simple type check
      function createHref(to: To) {
        return typeof to === 'string' ? to : createPath(to);
      }
      // ...
      let history: BrowserHistory = {
          // ...
          createHref
          // ...
      }
      return history
    }
    Copy the code
  • CreateHashHistory:
    export function createHashHistory(
      options: HashHistoryOptions = {}
    ) :HashHistory {
       // ...
       /** * check if there is a base tag, and if there is, get the BASE URL (not from the base tag, but from window.location.href) */
      function getBaseHref() {
        let base = document.querySelector('base');
        let href = ' ';
    
        if (base && base.getAttribute('href')) {
          let url = window.location.href;
          let hashIndex = url.indexOf(The '#');
          // get the url with the # removed
          href = hashIndex === -1 ? url : url.slice(0, hashIndex);
        }
    
        return href;
      }
      // HashHistory requires an additional base URL for the current page
      function createHref(to: To) {
        return getBaseHref() + The '#' + (typeof to === 'string' ? to : createPath(to));
      }
    
      // ...
    
      let history: HashHistory = {
          // ...
          createHref
          // ...
      }
      return history
    }
    Copy the code
  • CreateMemoryHistory:
    export function createMemoryHistory(
      options: MemoryHistoryOptions = {}
    ) :MemoryHistory {
       // ...
       / / with the BrowserHistory
      function createHref(to: To) {
        return typeof to === 'string' ? to : createPath(to);
      }
      // ...
    
      let history: MemoryHistory = {
          // ...
          createHref
          // ...
      }
      return history
    }
    Copy the code

listen

The Listen method is a listening method that essentially implements a publish-subscribe pattern. The publish and subscribe model is implemented in the source code as follows:

/** * Event object */
type Events<F> = {
  length: number;
  push: (fn: F) = > () = > void;
  call: (arg: any) = > void;
};

/** * Built-in publish/subscribe event model */
function createEvents<F extends Function> () :Events<F> {
  let handlers: F[] = [];

  return {
    get length() {
      return handlers.length;
    },
    // Return the corresponding clear statement when pushing
    push(fn: F) {
      handlers.push(fn);
      return function () {
        handlers = handlers.filter((handler) = >handler ! == fn); }; },call(arg) {
      handlers.forEach((fn) = >fn && fn(arg)); }}; }Copy the code

Once created, History takes the corresponding model object and calls the handlers that are passed in when the route changes.

Here’s the definition of the Listen method:

export interface Update {
  action: Action;
  location: Location;
}
// The Listener type was mentioned earlier, so you can look back
export interface Listener {
  (update: Update): void;
}

export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
) :BrowserHistory {
  // ...
  let listeners = createEvents<Listener>();
  // ...
  let history: BrowserHistory = {
    // ...
    listen(listener) {
      return listeners.push(listener);
    },
    // ...
  };

  return history;
}

export function createHashHistory(
  options: HashHistoryOptions = {}
) :HashHistory {
  // ...
  let listeners = createEvents<Listener>();
  // ...
  let history: HashHistory = {
    // ...
    listen(listener) {
      return listeners.push(listener);
    },
    // ...
  };

  return history;
}

export function createMemoryHistory(
  options: MemoryHistoryOptions = {}
) :MemoryHistory {
  // ...
  let listeners = createEvents<Listener>();
  // ...
  let history: MemoryHistory = {
    // ...
    listen(listener) {
      return listeners.push(listener);
    },
    // ...
  };

  return history;
}
Copy the code

block

The Block method is implemented in much the same way as the Listen method, except that BrowserHistory and HashHistory listen internally for the browser’s beforeUnload event.

  • CreateBrowserHistory and createHashHistory:
    export interface Update {
      action: Action;
      location: Location;
    }
    export interface Transition extends Update {
      retry(): void;
    }
    
    // Blocker we mentioned earlier
    export interface Blocker {
      (tx: Transition): void;
    }
    
    const BeforeUnloadEventType = 'beforeunload';
    
    export function createBrowserHistory(
      options: BrowserHistoryOptions = {}
    ) :BrowserHistory {
      // ...
      let blockers = createEvents<Blocker>();
      // ...
      let history: BrowserHistory = {
        // ...
        block(blocker) {
          let unblock = blockers.push(blocker);
    
          // We only join when we need to listen for the jump to fail, and only need an event to prevent the page from closing
          if (blockers.length === 1) {
            window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
          }
    
          return function () {
            unblock();
            // The beforeUnload event listener should be removed when there is no Blocker listener
            if(! blockers.length) {window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload); }}; }// ...
      };
    
      return history;
    }
    
    export function createHashHistory(
      options: HashHistoryOptions = {}
    ) :HashHistory {
      // ...
      let blockers = createEvents<Blocker>();
      // ...
      let history: HashHistory = {
        // ...
        block(blocker) {
          let unblock = blockers.push(blocker);
    
          if (blockers.length === 1) {
            window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
          }
    
          return function () {
            unblock();
            if(! blockers.length) {window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload); }}; }// ...
      };
      return history;
    }
    Copy the code
  • CreateMemoryHistory:
    // MemoryHistory is the same here as the listen method
    export function createMemoryHistory(
      options: MemoryHistoryOptions = {}
    ) :MemoryHistory {
      // ...
      let blockers = createEvents<Blocker>();
      // ...
      let history: MemoryHistory = {
        // ...
        // There is no listening for the browser's beforeUnload event
        block(blocker) {
          return blockers.push(blocker);
        }
        // ...
      };
    
      return history;
    }
    Copy the code

Push and replace

The push and replace methods do a little more internally. In addition to encapsulating the ability to push the new navigation onto the history stack, you also need to change both the current action and location, and determine and invoke the appropriate listening methods.

  • createBrowserHistoryandcreateHashHistoryThe structure definition of these two methods is the same:
    function push(to: To, state? :any) {
        let nextAction = Action.Push;
        let nextLocation = getNextLocation(to, state);
    
        /** * re-execute the push operation */
        function retry() {
          push(to, state);
        }
    
        AllowTx returns true if there is no block listening, false otherwise, no new navigation is pushed
        if (allowTx(nextAction, nextLocation, retry)) {
          let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
    
          // try... Catch is because ios has a limit of 100 calls to pushState, otherwise an error will be reported
          try {
            globalHistory.pushState(historyState, ' ', url);
          } catch (error) {
            // If the push fails, there is no state
            window.location.assign(url); } applyTx(nextAction); }}function replace(to: To, state? :any) {
        let nextAction = Action.Replace;
        let nextLocation = getNextLocation(to, state);
        function retry() {
          replace(to, state);
        }
    
        // Same as push function, otherwise new navigation will not be replaced
        if (allowTx(nextAction, nextLocation, retry)) {
          let [historyState, url] = getHistoryStateAndUrl(nextLocation, index);
    
          globalHistory.replaceState(historyState, ' ', url); applyTx(nextAction); }}Copy the code
  • createMemoryHistoryIs to change the definedentriesArray:
      function push(to: To, state? :any) {
        let nextAction = Action.Push;
        let nextLocation = getNextLocation(to, state);
        function retry() {
          push(to, state);
        }
    
        if (allowTx(nextAction, nextLocation, retry)) {
          // Modify the index and entries history stack arrays
          index += 1;
          // Add a new location and delete the stack after indexentries.splice(index, entries.length, nextLocation); applyTx(nextAction, nextLocation); }}function replace(to: To, state? :any) {
        let nextAction = Action.Replace;
        let nextLocation = getNextLocation(to, state);
        function retry() {
          replace(to, state);
        }
    
        if (allowTx(nextAction, nextLocation, retry)) {
          // Override the original locationentries[index] = nextLocation; applyTx(nextAction, nextLocation); }}Copy the code

In the code wrapper above, we define the Action for the corresponding method and format the user-passed parameters into a Location object.

Where the getNextLocation() method for formatting parameters is defined as follows:

 function getNextLocation(to: To, state: any = null) :Location {
     // Simple formatting
    return readOnly<Location>({
      // The first three are actually default values
      pathname: location.pathname,
      hash: ' '.search: ' '.// Return an object with pathName, hash, and search. (typeof to === 'string' ? parsePath(to) : to),
      state,
      key: createKey()
    });
  }
Copy the code

A recursive retry method is defined to be passed in the block callback. Finally, allowTx method is used to determine whether the route jump condition is met. If so, the route jump code (including changing the action and location attributes, and invoking callback listeners) is invoked.

AllowTx () is defined as follows:

/** * All blockers are called. If there is no Blocker listening, this returns true, otherwise false */
function allowTx(action: Action, location: Location, retry: () => void) {
    return (
      !blockers.length || (blockers.call({ action, location, retry }), false)); }Copy the code

The getHistoryStateAndUrl() method (only createBrowserHistory and createHashHistory) :

type HistoryState = {
  usr: any; key? :string;
  idx: number;
};

 function getHistoryStateAndUrl(
    nextLocation: Location,
    index: number
  ) :HistoryState.string] {
    return[{usr: nextLocation.state,
        key: nextLocation.key,
        idx: index
      },
      createHref(nextLocation)
    ];
  }
Copy the code

ApplyTx () method:

/ / createBrowserHistory with createHashHistory
function applyTx(nextAction: Action) {
    action = nextAction;
    // The index and location methods are modified. The getIndexAndLocation method was mentioned earlier in the location attribute
    [index, location] = getIndexAndLocation();
    listeners.call({ action, location });
}

// createMemoryHistory
function applyTx(nextAction: Action, nextLocation: Location) {
    // Do not change index here. Unlike other routers, index changes are implemented in push and go functions
    action = nextAction;
    location = nextLocation;
    listeners.call({ action, location });
}
Copy the code

Listen for browser popState events

In the browser environment, in addition to manually calling history.push and history.replace, the user can also change the navigation history through the browser’s forward and back buttons. Such behavior corresponds to the Action POP in history. The browser also provides the corresponding event popState, so we need to handle this situation a little bit extra.

Only createBrowserHistory and createHashHistory are monitored for this event:

const HashChangeEventType = 'hashchange';
const PopStateEventType = 'popstate';

export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
) :BrowserHistory {
   / /...

  let blockedPopTx: Transition | null = null;
  /** * If you set blocker's listener, this function will execute twice, first to jump back to the original page, and second to execute all of the blockers' callbacks. Because the push function we wrapped is already intercepted by us */
  function handlePop() {
    if (blockedPopTx) {
      blockers.call(blockedPopTx);
      blockedPopTx = null;
    } else {
      let nextAction = Action.Pop;
      let [nextIndex, nextLocation] = getIndexAndLocation();

      // If there is a front hook
      if (blockers.length) {
        if(nextIndex ! =null) {
          // Count the number of hops
          let delta = index - nextIndex;
          if (delta) {
            // Revert the POP
            blockedPopTx = {
              action: nextAction,
              location: nextLocation,
              // Restore the page stack, that is, nextIndex page stack
              retry() {
                go(delta * -1); }};// Jump back to (index original page stack)go(delta); }}else {
          // asset
          If nextIndex is null, it will enter the branch with a warning message}}else {
        // Change the current action, calling all listenersapplyTx(nextAction); }}}// You can see that the listener was created when the History object was created
  window.addEventListener(PopStateEventType, handlePop);
  / /...
}


export function createHashHistory(
  options: HashHistoryOptions = {}
) :HashHistory {
   / /...

  // Same as createBrowserHistory
  let blockedPopTx: Transition | null = null;
  function handlePop() {
    / /...
  }
  
    
  // The hashchange event is listened for in addition below
  window.addEventListener(PopStateEventType, handlePop);
  // Low version compatible, listening for hashchange events
  // https://developer.mozilla.org/de/docs/Web/API/Window/popstate_event
  window.addEventListener(HashChangeEventType, () = > {
    let [, nextLocation] = getIndexAndLocation();

    // If popState events are supported, this will be equal because the popState callback is executed first
    if (createPath(nextLocation) !== createPath(location)) {
      handlePop();
    }
  });
  / /...
}
Copy the code

Go, back and forward

These methods are much easier to wrap than push and replace, and in BrowserHistory and HashHistory they are just wrappers around the History API, In MemoryHistory there are no routed POP listening events and history stack changes from outside the code, so the listening callbacks are moved directly into it.

  • CreateBrowserHistory and createHashHistory:
    export function createBrowserHistory(
      options: BrowserHistoryOptions = {}
    ) :BrowserHistory {
      // ...
      function go(delta: number) {
        globalHistory.go(delta);
      }
      // ...
      let history: BrowserHistory = {
        go,
        back() {
          go(-1);
        },
        forward() {
          go(1);
        },
        // ...
      };
    
      return history;
    }
    
    export function createHashHistory(
      options: HashHistoryOptions = {}
    ) :HashHistory {
      // ...
      function go(delta: number) {
        globalHistory.go(delta);
      }
      // ...
      let history: HashHistory = {
        go,
        back() {
          go(-1);
        },
        forward() {
          go(1);
        },
        // ...
      };
    
      return history;
    }
    Copy the code
  • CreateMemoryHistory:
export function createMemoryHistory(
  options: MemoryHistoryOptions = {}
) :HashHistory {
  // ...
  function go(delta: number) {
    // Jump to the original location
    let nextIndex = clamp(index + delta, 0, entries.length - 1);
    let nextAction = Action.Pop;
    let nextLocation = entries[nextIndex];
    function retry() {
      go(delta);
    }

    if(allowTx(nextAction, nextLocation, retry)) { index = nextIndex; applyTx(nextAction, nextLocation); }}// ...
  let history: MemoryHistory = {
    go,
    back() {
      go(-1);
    },
    forward() {
      go(1);
    },
    // ...
  };

  return history;
}
Copy the code

conclusion

History as a whole is a secondary encapsulation of the browser API, but it doesn’t go too far. It just abstracts every time a page jumps, and adds extra listening and special jump blocking.

In the next article we will dive into the topic of route encapsulation in React-Router V6.

The source analysis repository for this article is available on Github.