Why should I learn the react-Router source code? Why figure out the entire routing process? In my personal opinion, learning React-Router helps us learn the route jump principle of SINGLE page application (SPA), enables us to understand the whole process from history. Push to component page switching, and makes us no longer feel nervous about routing problems during the interview. Let’s take a deep dive into the react-Router source code.

A correct understanding of react-router

1 Understand single-page applications

What is a single page app?

From my personal understanding, single-page application is to use an HTML, load js, CSS and other resources at one time, and all pages are under a container page. The essence of page switching is to switch components.

2. Preliminary study of React-Router, expose the veil of routing principle

1.react-router-domandreact-routerandhistoryWhat is the relationship between the library three

History can be understood as the react – the core of the router, is the core of routing principle and popState, integration in the history. PushState etc. The principle of the underlying routing implementation method, then we’ll explain one by one.

The react-router is the core of the React-router-dom. Core components such as Switch realize core functions from route change to component update. In our project, we only need to introduce React-router-DOM once.

React-router-dom, based on the react-router core, adds a Link component for jump, BrowserRouter in HISTOy mode and HashRouter in hash mode, etc. BrowserRouter and HashRouter are just the createBrowserHistory and createHashHistory methods of the History library

Instead of talking about react-router-dom, let’s focus on react-router.

② A small demo?

import { BrowserRouter as Router, Switch, Route, Redirect,Link } from 'react-router-dom'

import Detail from '.. /src/page/detail'
import List from '.. /src/page/list'
import Index from '.. /src/page/home/index'

const menusList = [
  {
    name: 'home'.path: '/index'
  },
  {
    name: 'list'.path: '/list'
  },
  {
    name: 'details'.path: '/detail'},]const index = () = > {
  return <div >
    <div >
     
      <Router  >
      <div>{
        /* link 路由跳转 */
         menusList.map(router=><Link key={router.path} to={ router.path } >
           <span className="routerLink" >{router.name}</span>
         </Link>)}</div>
        <Switch>
          <Route path={'/index'} component={Index} ></Route>
          <Route path={'/list'} component={List} ></Route>
          <Route path={'/detail'} component={Detail} ></Route>{/* route mismatch, redirection to /index */}<Redirect from='/ *' to='/index' />
        </Switch>
      </Router>
    </div>
  </div>
}
Copy the code

Results the following

Two single page to achieve the core principle

Single-page application routing is implemented by switching urls and listening for URL changes to render different page components.

The main modes are history mode and hash mode.

1 History mode principle

① Changing routes

history.pushState

history.pushState(state,title,path)
Copy the code

1 State: a state object associated with the specified url, which is passed to the callback function when the popState event is triggered. Null if not required.

2 title: the title of the new page, but all browsers currently ignore this value and can be null.

3 path: New url, which must be in the same field as the current page. The browser’s address bar will display the address.

history.replaceState

history.replaceState(state,title,path)
Copy the code

As with pushState, this method will modify the current history object record. The length of history.length will not change.

② Listen for routes

Popstate event

window.addEventListener('popstate'.function(e){
    /* Listen for changes */
})
Copy the code

When the history object of the same document changes, the popState event history.pushState can cause the browser address to change without refreshing the page. Note that ⚠️ : using history.pushState() or history.replacestate () does not trigger popState events. Popstate events are only triggered by browser actions such as clicking the back or forward buttons or calling the history.back(), history.forward(), or history.go() methods.

2 Hash mode Principle

① Changing routes

window.location.hash

Get and set the hash value using the window.location.hash property.

② Listen for routes

onhashchange

window.addEventListener('hashchange'.function(e){
    /* Listen for changes */
})
Copy the code

Understand the History library

The React-router is a history library. The history library is focused on recording the history state of the route and what we should write when the path changes. In history mode, we use popState to listen for route changes. Use hashChange to listen for route changes in hash mode.

Next we look at the createBrowserHistory method in Browser mode and the createHashHistory method in Hash mode.

1 createBrowserHistory

To run the route in Browser mode, everything starts with createBrowserHistory. Here we refer to the history-4.7.2 version. The API may be different in the latest version, but the principle is the same. When parsing history, we focus on setState,push,handlePopState, and Listen

const PopStateEvent = 'popstate'
const HashChangeEvent = 'hashchange'
/* This simplifies createBrowserHistory by listing several core apis and what they do */
function createBrowserHistory(){
    /* Global history */
    const globalHistory = window.history
    /* Listens to route translation, record the message. * /
    const transitionManager = createTransitionManager()
    /* Change the location object to notify the component to update */
    const setState = () = > { / *... * / }
    
    /* Handle the callback function that handles popstate changes when path changes */
    const handlePopState = () = > { / *... * / }
   
    /* the history. Push method changes the route, changes the URL through the global object history.pushState, notifies the router to trigger the update, and replace the component */
    const push=() = > { / *... * / }
    
    /* The bottom layer applies an event listener to popState events */
    const listen=() = >{ / *... * / } 
    return {
       push,
       listen,
       /* .... */}}Copy the code

Here’s a look at each API and their previous interactions

const PopStateEvent = 'popstate'
const HashChangeEvent = 'hashchange'
Copy the code

Popstate and HashChange are the underlying methods of listening for route changes.

1) setState

const setState = (nextState) = > {
    /* Merge information */
    Object.assign(history, nextState)
    history.length = globalHistory.length
    /* Notify each listens route that has changed */
    transitionManager.notifyListeners(
      history.location,
      history.action
    )
  }
Copy the code

The code is simple: unify eachtransitionManagerThe management oflistenerThe route status has been updated.

When to bind a Litener, we’ll see in the react-Router code that follows.

(2) listen

const listen = (listener) = > {
    /* Add listen */
    const unlisten = transitionManager.appendListener(listener)
    checkDOMListeners(1)

    return () = > {
      checkDOMListeners(-1)
      unlisten()
    }
}
Copy the code

checkDOMListeners

const checkDOMListeners = (delta) = > {
    listenerCount += delta
    if (listenerCount === 1) {
      addEventListener(window, PopStateEvent, handlePopState)
      if (needsHashChangeListener)
        addEventListener(window, HashChangeEvent, handleHashChange)
    } else if (listenerCount === 0) {
      removeEventListener(window, PopStateEvent, handlePopState)
      if (needsHashChangeListener)
        removeEventListener(window, HashChangeEvent, handleHashChange)
    }
  }
Copy the code

The LISTEN essence binds/unbinds popState events with the checkDOMListeners parameter 1 or -1, and calls the handler handlePopState when the route changes.

Now let’s look at the push method.

(3) push

 const push = (path, state) = > {
    const action = 'PUSH'
    /* 1 Create location object */
    const location = createLocation(path, state, createKey(), history.location)
    /* Determine whether the route transformation can be performed, and while doing so, start another transformation, which may cause an exception */
    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) = > {
      if(! ok)return
      const href = createHref(location)
      const { key, state } = location
      if (canUseHistory) {
        /* Change url */
        globalHistory.pushState({ key, state }, null, href)
        if (forceRefresh) {
          window.location.href = href
        } else {
          /* Change the react-router location object to create an update environment */
          setState({ action, location })
        }
      } else {
        window.location.href = href
      }
    })
  }
Copy the code

The push (history.push) process roughly generates the latest Location object first, The react-Router is notified of the update by the setState method. The current location object is passed to the react-router. PushState does not trigger popState, so you need to manually setState to trigger component updates.

(4) handlePopState

Finally, let’s look at what happens when the popState listens to a function, when the path changes,

/* Let's simplify handlePopState
const handlePopState = (event) = >{
     /* Get the current location object */
    const location = getDOMLocation(event.state)
    const action = 'POP'

    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) = > {
        if (ok) {
          setState({ action, location })
        } else {
          revertPop(location)
        }
    })
}
Copy the code

The handlePopState code is simple. It determines that the action type is pop, and then sets State to reload the component.

2 createHashHistory

The hash mode is similar to the History API, so let’s focus on how the hash mode listens for routes, and how the push and replace methods change paths.

Listen for hash route changes

  const HashChangeEvent = 'hashchange'
  const checkDOMListeners = (delta) = > {
    listenerCount += delta
    if (listenerCount === 1) {
      addEventListener(window, HashChangeEvent, handleHashChange)
    } else if (listenerCount === 0) {
      removeEventListener(window, HashChangeEvent, handleHashChange)
    }
  }
Copy the code

As mentioned earlier, we use hashChange to listen for hash route changes.

The hash route was changed


/* Corresponds to the push method */
const pushHashPath = (path) = >
  window.location.hash = path

/* Corresponds to the replace method */
const replaceHashPath = (path) = > {
  const hashIndex = window.location.href.indexOf(The '#')

  window.location.replace(
    window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + The '#' + path
  )
}

Copy the code

In hash mode, the underlying call to history.push is window.location.href to change the route. The bottom layer of history.replace isto change the route with window.location.replace.

conclusion

Let’s use a picture to describe ithistoryLibrary overall process.

Four core API

1 Router- Receives the location change and dispatches the update stream

RouterAction is thehistory location Wait for routing information to be transmitted

Router

/* Router is used to transmit routing information such as history location */
class Router extends React.Component {
  static computeRootMatch(pathname) {
    return { path: '/'.url: '/'.params: {}, isExact: pathname === '/' };
  }
  constructor(props) {
    super(props);
    this.state = {
      location: props.history.location
    };
    // Record the pending location
    // If there is any 
      
       , the change is made in the constructor
      
    // At the initial render. If there are, they will be in
    // On a subcomponent, we might
    // Get a new location before installing 
      
       .
      
    this._isMounted = false;
    this._pendingLocation = null;
    /* This is the history object created by history */
    if(! props.staticContext) {}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
      this.unlisten = props.history.listen(location= > {
        /* Create listener */
        if (this._isMounted) {

          this.setState({ location });
        } else {
          this._pendingLocation = location; }}); }}componentDidMount() {
    this._isMounted = true;
    if (this._pendingLocation) {
      this.setState({ location: this._pendingLocation }); }}componentWillUnmount() {
    /* Unlisten */
    if (this.unlisten) this.unlisten();
  }
  render() {
    return (
      CreateContext Creates a context that holds basic router information. children */
      <RouterContext.Provider
          children={this.props.children || null}
          value={{
          history: this.props.history.location: this.state.location.match: Router.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext}} / >); }}Copy the code

Conclusion:

Initialize binding LISTEN, route changed, location changed, component changed. The React history route status is saved between the react.Content context and status updates.

A project should have a root Router that generates updates before switching routing components. If multiple routers exist, routes will be switched and the page will not be updated.

2 Switch- Matches the correct and unique route

Render the current component based on the Router update stream.

/* Switch component */
class Switch extends React.Component {
  render() {
    return(< RouterContext.consumer > {/* context with history location object */} {context => {context, 'You should not use <Switch> outside a <Router>'); const location = this.props.location || context.location; let element, match; // We use React.children.foreach instead of React.children.toarray ().find () // This is because toArray adds keys to all child elements, We don't want // to trigger unmount/reload // components at different urls for rendering the same two <Route>s. // Use match === null to React.children.foreach (this.props. Children, this.props. child => { if (match == null && React.isValidElement(child)) { element = child; / / child component Is access to the path of the Route or from rediect const path = child. Props. The path | | child. Props. The from; match = path ? matchPath(location.pathname, { ... child.props, path }) : context.match; }}); return match ? React.cloneElement(element, { location, computedMatch: match }) : null; }} </RouterContext.Consumer> ); }}Copy the code

Find the component that matches the current path and render it. Match the pathname with the path of the component. Find a router component that matches path.

matchPath

function matchPath(pathname, options = {}) {
  if (typeof options === "string" || Array.isArray(options)) {
    options = { path: options };
  }

  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;

    const { regexp, keys } = compilePath(path, {
      end: exact,
      strict,
      sensitive
    });
    const match = regexp.exec(pathname);
    /* Null */ is returned
    if(! match)return null;

    const [url, ...values] = match;
    const isExact = pathname === url;

    if(exact && ! isExact)return null;

    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];
        returnmemo; }, {}}; },null);
}
Copy the code

The route is matched. Procedure

3 Route-Host container for component pages

/** * The public API for matching a single path and rendering. */
class Route extends React.Component {
  render() {
    return(< routerContext. Consumer> {context => {/* router/route will give warning warning */ invariant(context, "You should not use <Route> outside a <Router>"); / / computedMatch for after dealing with the swich path const location = this. Props. The location | | context. The location; const match = this.props.computedMatch ? this.props.computedMatch // <Switch> already computed the match for us : this.props.path ? matchPath(location.pathname, this.props) : context.match; const props = { ... context, location, match }; let { children, component, render } = this.props; if (Array.isArray(children) && children.length === 0) { children = null; } return ( <RouterContext.Provider value={props}> {props.match ? children ? typeof children === "function" ? __DEV__ ? evalChildrenDev(children, props, this.props.path) : children(props) : children : component ? React.createElement(component, props) : render ? render(props) : null : typeof children === "function" ? __DEV__ ? evalChildrenDev(children, props, this.props.path) : children(props) : null} </RouterContext.Provider> ); }} </RouterContext.Consumer> ); }}Copy the code

Match path, render component. As a container for routing components, the actual components can be rendered according to. Consume obtains the location,match and other information of the current level through RouterContext.Consume. Pass to the page component as a Prop. This allows us to retrieve information such as location,match, and so on from props in the page component.

4 Redirect- There is no matching route, so Redirect

Redirection component. If the incoming route matches, it redirects the corresponding route.

function Redirect({ computedMatch, to, push = false }) {
  return (
    <RouterContext.Consumer>{context => { const { history, staticContext } = context; /* method is the route jump method. */ const method = push ? history.push : history.replace; */ const location = createLocation(computedMatch? typeof to === 'string' ? generatePath(to, computedMatch.params) : { ... to, pathname: generatePath(to.pathname, computedMatch.params) } : To) /* When the component is initialized, the route jump is performed. When the component is initialized, mounted executes the push method. When the component is updated, the location is not equal. The history method is also redirected */ return (<Lifecycle
              onMount={()= >{ method(location); }} onUpdate={(self, prevProps) => { const prevLocation = createLocation(prevProps.to); if ( ! locationsAreEqual(prevLocation, { ... location, key: prevLocation.key }) ) { method(location); } }} to={to} /> ); }}</RouterContext.Consumer>
  );
}

Copy the code

When you initialize a route jump, when you initialize,mountedperformpushMethod when the component is updated iflocationIs not equal. Will also executehistoryMethod redirection.

Summary + process analysis

conclusion

History provides core apis, such as listening to routes, ways to change routes, and saving route state.

React-router provides a route rendering component, route uniqueness matching component, redirection component, and other functional components.

Process analysis

What happens to component update rendering when the address bar changes the URL? 😊😊😊 Use the history mode as a reference. When the URL changes, it first triggers a histoy, calls an event that listens for a popState event, triggers a callback function called handlePopState, triggers a setState method under history, and generates a new location object. Then the Router component is notified to update the location and passed through the context. The Switch matches the corresponding Route component to render through the passed update flow. Finally, the Route component takes out the context content and passes it to the rendering page, rendering the update.

When we callhistory.pushWhat happened to methods, switching routes, updating renderings of components?

When we call history.push, we first call history’s push method, which changes the current URL with history.pushState, and then we trigger the setState method under history. The next steps are exactly the same as above, so I won’t talk about them here.

We use a diagram to show the relationship between the various routing components.

I hope those who have read this article can understand the whole process of React-Router, and the code logic is not difficult to understand. The whole process I give you analysis again, I hope that students can take the initiative to see a wave of source code, to understand the whole process. On paper come zhongjue shallow, must know this to practice.

Write in the end, thank you for your encouragement and support 🌹🌹🌹, like can give the author like attention, public number: front-end Sharing