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:
createBrowserHistory
Browser-basedhistory
Object 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:
BrowserRouter
The correspondingcreateBrowserHistory
By thereact-router-dom
To provide.HashRouter
The correspondingcreateHashHistory
By thereact-router-dom
To provide.MemoryRouter
The correspondingcreateMemoryHistory
By thereact-router
Provided primarily forreact-native
And other memory-based routing systems.
Note:
- In fact with
react-native
Corresponding packagereact-router-native
Using theNativeRouter
, but in factNativeRouter
isMemoryRouter
Simple 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
- in
react-router-dom
There is actually another route inStaticRouter
But it is used inssr
In, no dependencyhistory
Library, just checking the props passed in.// Specify the import mode import { StaticRouter } from 'react-router-dom/server' Copy the code
在
react-router-dom
It was added in V6.1.1HistoryRouter
, but theRouter
Mainly to help us pass in manuallyhistory
Examples, I won’t mention them here, but I’ll talk about them laterreacr-router-dom
Will 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:
createHref
For the user tohistory
Internally definedPath
Object 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
,back
withforward
Used 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)
listen
And 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
about
block
Listening prevents route redirectsGo (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:
createBrowserHistory
Used to provide users with the creation of a browser-based history APIHistory
Object for most modern browsers (except for a few that don’t support HTML5’s newly added History API, that is, the browser’shistory
Object needs to havepushState
,replaceState
andstate
In the production environment, redirection configuration on the server is required for normal use.createHashHistory
Used to provide the user with a hash value based on the browser URLHistory
Object is generally compatible with almost all browsers using this method, but considering the current development of browsers, in5.x
The 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.x
Version), 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.createMemoryHistory
Used to provide the user with a memory-based systemHistory
Object 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.
createBrowserHistory
andcreateHashHistory
The 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
createMemoryHistory
Is to change the definedentries
Array: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.