The source version of this analysis: V5.2.0
1 overview
The React Router is the official Routing library for React. It can be used on the Web, Node. js server, and React Native. At the code level, it is a collection of React components, hooks, and utility functions.
The React Router is a dynamic route that can be changed during rendering; In contrast, static routing is a configuration that is separate from the running application. It needs to be defined in advance and cannot be changed dynamically. This means that almost everything in the React Router is a component.
The React Router consists of three parts:
- React-router: Contains most of the core features of the React Router, including the routing matching algorithm and most of the core components and hooks.
- React-router-dom: In addition to the react-router content, some DOM related apis are added, such as
<BrowserRouter>
.<HashRouter>
.<Link>
And so on. - React-router-native: In addition to the react-router content, some react Native related apis are added, such as
<NativeRouter>
And the native version<Link>
.
When installing the react-router-dom and react-router-native, the react-Router is automatically included as a dependency and everything in the React-Router is automatically exported. Therefore, you only need to install react-router-dom or react-router-native. Do not use react-router directly.
In summary, for Web browsers (including server-side rendering), use react-router-dom; For React Native apps, use react-router-native.
The React Router contains three basic types of components:
-
Routers
<BrowserRouter>
<HashRouter>
-
Route Matching Components (Route Matchers)
<Route>
<Switch>
-
Navigation/Route Changers
<Link>
<NavLink>
<Redirect>
Here is a typical use example:
2 Basic Knowledge
Before we understand how this works, let’s look at some of the browser global objects and events that are involved at the bottom.
2.1 Global Objects
2.1.1 Location object
Represents the location (URL) to which the link is linked, which can be accessed through window.location.
Its main properties include:
- Href: The entire URL.
- Protocol: indicates the PROTOCOL of the URL, with a colon at the end.
- Host: indicates the domain name, which may end with a colon and a port number.
- Hostname: indicates the domain name excluding the port number.
- Port: indicates the port number.
- Pathname: Part of the URL path with a “/” at the beginning.
- Search: URL parameter, starting with a “?” .
- Hash: Fragment identifier, starting with a “#”.
The main methods include:
- Assign (URL) : loads the given URL.
- Reload () : refreshes the current page.
- Replace (URL) : Loads the given URL to replace the current page. Unlike assign, it is not saved to the session history and therefore cannot be backlogged to the previous page.
- ToString () : Returns the entire string, a read-only version of location.href.
2.1.2 the History object
Represents the session history of a browser TAB, accessible through window.history.
Key attributes include:
- Length Read-only: Returns the number of entries in the session history, including the current loaded page.
- State Read-only: Indicates the state of the current entry in the Session History Stack.
Methods include:
- Back () : Goes to the previous page, which has the same effect as the back button in the upper left corner of the browser, equivalent to history.go(-1).
- Forward () : To go to the next page. This has the same effect as the forward button in the upper left corner of the browser, equivalent to history.go(1).
- Go (delta) : How many pages forward or backward from the current page.
pushState(state, title[, url])
: Adds a state to the session history stack.replaceState(state, title[, url])
: replaces the state of the current entry in the session history stack.
In the example below, right-click on the back or forward buttons in the upper left corner of the browser to display a list of items saved in History from the current TAB. The image on the right accesses the state of the current entry through history.state.
2.2 Global Events
2.2.1 hashchange events
This is the event that fires when the hash part of the URL changes.
window.addEventListener('hashchange'.function() {
if (location.hash === '#cool-feature') {
console.log("You're visiting a cool feature!"); }},false);
Copy the code
2.2.2 popstate event
This event is triggered when an active entry in history changes. For a very simple example, in a TAB page, open the console and write the listening code. Then click the forward or back button in the upper left corner of the browser to see that the event is triggered.
Note that history.pushState() and history.replacestate () do not trigger this event, but only when a browser action is taken. If the user clicks the browser’s back button (or calls methods like history.back() or history.forward())
If the active entry was created or modified by history.pushState() or history.replacEstate (), the state property of the popState event contains a copy of the state.
window.addEventListener('popstate'.(event) = > {
console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
});
history.pushState({page: 1}, "title 1"."? page=1");
history.pushState({page: 2}, "title 2"."? page=2");
history.replaceState({page: 3}, "title 3"."? page=3");
history.back(); // Logs "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // Logs "location: http://example.com/example.html, state: null
history.go(2); // Logs "location: http://example.com/example.html?page=3, state: {"page":3}
Copy the code
2.2.3 beforeunload events
Triggered when the browser window closes or refreshes.
Calling preventDefault() in the event handler will trigger a confirmation dialog asking the user if they really want to leave the page. If the user confirms, the browser navigates to the new page, otherwise the navigation is cancelled.
Note, however, that not all browsers support this method, and some browsers require event handlers to implement one of the two legacy methods instead:
- Assigns a string to the returnValue property of the event
- Returns a string from the event handler.
window.addEventListener('beforeunload'.(event) = > {
// Cancel the event as stated by the standard.
event.preventDefault();
// Chrome requires returnValue to be set.
event.returnValue = ' ';
});
Copy the code
3 Source code Analysis
3.1 history-5.0.0 source code analysis
History is a core dependency within the React Router, which contains the core logic for routing management.
As mentioned earlier, the React Router provides three types of routers: BrowserRouter, HashRouter, and NativeRouter. The difference between them is the underlying history. The History library provides ways to create each of these histories.
3.1.1 createHashHistory
export function createHashHistory(options: HashHistoryOptions = {}) :HashHistory {
// 1. Get the window object and cache the global history object
// defaultView returns the window object associated with the current Document object
let { window = document.defaultView! } = options;
let globalHistory = window.history;
let blockedPopTx: Transition | null = null;
// 2. Listen for popState events, old browsers listen for hashchange
window.addEventListener(PopStateEventType, handlePop); // popstate
// popstate does not fire on hashchange in IE 11 and old (trident) Edge
window.addEventListener(HashChangeEventType, () = > { // hashchange
let [, nextLocation] = getIndexAndLocation();
// Ignore extraneous hashchange events.
if (createPath(nextLocation) !== createPath(location)) {
handlePop();
}
});
let action = Action.Pop;
// 3. Get the current state subscript and location
let [index, location] = getIndexAndLocation();
// 4. Initialize two callback functions: collector and interceptor
let listeners = createEvents<Listener>();
let blockers = createEvents<Blocker>();
// 5. Add an initial state subscript 0 to the history stack
if (index == null) {
index = 0; globalHistory.replaceState({ ... globalHistory.state,idx: index }, ' ');
}
// 6. Create and return a history object
let history: HashHistory = {
get action() {
return action; // POP PUSH REPLACE
},
get location() {
return location;
},
createHref, // Return pathName + search + hash
push,
replace,
go,
back() {
go(-1);
},
forward() {
go(1);
},
listen(listener) {
return listeners.push(listener);
},
block(blocker) {
let unblock = blockers.push(blocker);
if (blockers.length === 1) {
// beforeUnload Triggers a confirmation dialog when a browser window closes or refreshes
window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
}
return function() {
unblock();
// Remove the beforeunload listener so the document may
// still be salvageable in the pagehide event.
// See https://html.spec.whatwg.org/#unloading-documents
if(! blockers.length) {window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload); }}; }};return history;
}
Copy the code
Take a look at the implementation inside handlePop:
function handlePop() {
if (blockedPopTx) {
blockers.call(blockedPopTx);
blockedPopTx = null;
} else {
// 1. When popState is triggered, the URL has changed, so the target location is retrieved
let nextAction = Action.Pop;
let [nextIndex, nextLocation] = getIndexAndLocation();
// 2. If the interceptor has a callback function, execute the callback function before jumping
if (blockers.length) {
if(nextIndex ! =null) {
let delta = index - nextIndex;
if (delta) {
blockedPopTx = {
action: nextAction,
location: nextLocation,
retry() {
go(delta * -1); }}; go(delta); }}}else {
// 3. Navigate directly if there is no interceptorapplyTx(nextAction); }}}function applyTx(nextAction: Action) {
action = nextAction;
// 4. Update the current index and location values
[index, location] = getIndexAndLocation();
// 5. Call the collected callback function
listeners.call(location);
}
Copy the code
In step 4, getIndexAndLocation is called to update the current index and location values. This method internally calls parsePath to get pathName, search, and hash. Why bother? Why not just call the corresponding property in window.location? Note that for applications that use HashHistory, the first thing after the # in the URL is not a hash semantically (although it is for browsers), and the long list of things that follow contains a lot of key routing information, such as pathName, search, The last (and therefore second) hash after # is a meaningful hash. For example, https://www.example.com/#/page1?name=react#first.
function getIndexAndLocation() :number.Location] {
let { pathname = '/', search = ' ', hash = ' ' } = parsePath(
window.location.hash.substr(1) // str.substr(start[, length])
);
let state = globalHistory.state || {};
return [
state.idx,
readOnly<Location>({
pathname,
search,
hash,
state: state.usr || null.key: state.key || 'default'})]; }Copy the code
To summarize, createHashHistory does two things:
- Create a history that contains many useful methods;
- What you need to do to define a route change is call the collected callback function
3.1.2 createBrowserHistory
The only difference from createHashHistory support is that only popState is listened on.
export function createBrowserHistory(
options: BrowserHistoryOptions = {}
) :BrowserHistory {
// ...
window.addEventListener(PopStateEventType, handlePop); // popstate
// ...
Copy the code
In addition, only the related attributes need to be accessed when obtaining routing information.
function getIndexAndLocation() :number.Location] {
let { pathname, search, hash } = window.location;
// ...
}
Copy the code
3.2 React-router-5.2.0 source code analysis
3.2.1 < the Router >
BrowserRouter and HashRouter simply call createBrowserHistory and createHashHistory to create a history object and return the same Router component, Pass in their history as props.
class BrowserRouter extends React.Component {
history = createBrowserHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />; }}class HashRouter extends React.Component {
history = createHashHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />; }}Copy the code
Inside the Router component
class Router extends React.Component {
1. Create a default match object
static computeRootMatch(pathname) {
return { path: "/".url: "/".params: {}, isExact: pathname === "/" };
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
this._isMounted = false;
this._pendingLocation = null;
// 2. Call the listen method in history to add the callback function
this.unlisten = props.history.listen(location= > {
if (this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location; }}); }componentDidMount() {
this._isMounted = true;
// 3. Unsubscribe the component before it is unmounted
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation }); }}componentWillUnmount() {
if (this.unlisten) this.unlisten();
}
// 4. Put the history location match and other key information into the context
render() {
return (
<RouterContext.Provider
value={{
history: this.props.history.location: this.state.location.match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
>
<HistoryContext.Provider
children={this.props.children || null}
value={this.props.history}
/>
</RouterContext.Provider>); }}Copy the code
The second step is particularly critical. In history, we talked about calling the collected callback function when listening for route changes. Here the location is updated with setState, which triggers a re-rendering of the entire Router. Take a look at how the Listen method subscribes:
listen(listener) {
return listeners.push(listener);
}
let listeners = createEvents<Listener>();
function createEvents<F extends Function> () :Events<F> {
let handlers: F[] = [];
return {
get length() {
return handlers.length;
},
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
History maintains an array of callbacks that it calls to execute one by one when the route changes.
3.2.2 < Switch >
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>{the context = > {/ / 1. Get the location const location = this. Props. The location | | context. The location; let element, match; // 2. Traverse the Route element and match location with props. Path child => { if (match == null && React.isValidElement(child)) { element = child; const path = child.props.path || child.props.from; match = path ? matchPath(location.pathname, { ... child.props, path }) : context.match; }}); // the React element will be cloned and the new React element will be returned. The new child element will replace the existing child element, keeping the original element's key and ref? React.cloneElement(element, { location, computedMatch: match }) : null; }}</RouterContext.Consumer>); }}Copy the code
Take a look at how the matchPath algorithm works:
function matchPath(pathname, options = {}) {
if (typeof options === "string" || Array.isArray(options)) {
options = { path: options };
}
// 1
const { path, exact = false, strict = false, sensitive = false } = options;
const paths = [].concat(path);
return paths.reduce((matched, path) = > {
if(! path && path ! = ="") return null;
if (matched) return matched;
// 2. Compile to re
const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
// 3
const match = regexp.exec(pathname);
if(! match)return null;
const [url, ...values] = match;
const isExact = pathname === url;
if(exact && ! isExact)return null;
// 4. Return match on success, null on failure
return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) = > {
memo[key.name] = values[index]; // {bar: '123'}
returnmemo; }, {}}; },null);
}
export default matchPath;
Copy the code
It can be found that once the Switch gets the first matching Route, it will no longer match further down. Therefore, in daily project development, it is better to write the pathname’s more specific Route in front to ensure smooth matching.
3.2.3 < Route >
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>{the context = > {/ / 1. Get the location const location = this. Props. The location | | context. The location; // 2. Get a match, if not, count yourself as const match = this.props.computedMatch? this.props.path ? matchPath(location.pathname, this.props) : context.match; // 3. New props const props = {... context, location, match }; let { children, component, render } = this.props; Return (// 4. Provide new props for the rendered component<RouterContext.Provider value={props}>// 5. Precedence of render methods: children (render regardless of whether they match), Component, render {props. Match? children ? typeof children === "function" ? children(props) : children : component ? React.createElement(component, props) : render ? render(props) : null : typeof children === "function" ? children(props) : null}</RouterContext.Provider>
);
}}
</RouterContext.Consumer>); }}Copy the code
3.2.4 < Link >
Link implementation is also quite simple, here put the picture, interested can go to read the source.
4 summarizes
Now, let’s summarize the process.
- Create history and listen for hashchange or popState events, executing all callbacks once both events are triggered;
- The Router adds a callback function to history that performs setState to update the location and rerender all components;
- Click the Link component or invoke
history.push(url, state)
; - PushState or replaceState to update the current active entry in browser history;
- Call all previously collected callback functions (
setState(location)
); - Router executes render;
- Switch traverses the subcomponent and selects the first matching Route.
- Render the Route so you can see the UI view that the current URL needs to display.