preface

Before we learn the VueRouter source code, we need to know that path-to-regexp has dynamic routes like /foo/:id in VueRouter. This library is a regular expression installation that pars the routes and generates the corresponding regular expressions

npm install path-to-regexp --save
Copy the code

pathToRegexp

The pathToRegexp method receives three parameters: parameter 1: the destination address, parameter 2: an array filled with keys found in the path, and parameter 3 regular configuration item The configuration item is:

  • Sensitive (default: false)
  • Strict Specifies whether the trailing slash is exactly matched (default: false)
  • End global match (default: true)
  • Start Expands the match from the start position (default: true)
  • Delimiter Specifies another delimiter (default: ‘/’)
  • EndsWith Specifies the standard end character
  • Whitelist specifies the list of delimiters (default: undefined, any character)
const { pathToRegexp } = require("path-to-regexp");

let keys = []
let re = pathToRegexp('/foo/:bar', keys)
console.log(re);
console.log(keys);
///^\/foo\/([^\/]+?) (? : \ /)? $/i[{name: 'bar'.prefix: '/'.delimiter: '/'.optional: false.repeat: false.partial: false.pattern: '[^ \ \ /] +? '}]Copy the code

exec

Returns an array if the url matches the rule. Returns null if the url does not match the rule

var pathToRegexp = require('path-to-regexp')

var re = pathToRegexp('/foo/:bar');     // Matching rules
var match1 = re.exec('/test/route');    / / url path
var match2 = re.exec('/foo/route');     / / url path

// Match1 results in null
['/foo/route', 'route', index: 0, input: '/foo/route']

Copy the code

match

Function: Returns a function that converts a path to an argument

const { match } = require("path-to-regexp");
var match1 = match("/user/:id", { decode: decodeURIComponent });
match1("/user/123"); 
//{ path: '/user/123', index: 0, params: { id: '123' } }

Copy the code

parse

Function: Parses the parameter part of the URL string


const { parse } = require("path-to-regexp");
console.log(parse("/foo/:bar"))
// Return an array. The second item in the array gives the name of the property (name: bar) with the url address.
[
  '/foo',
  {
    name: 'bar'.// The name of the token (string for named, or number for unnamed index)
    prefix: '/'.// The prefix string in the prefix section (for example, "/")
    suffix: ' '.// Suffix string in the suffix section (for example, "")
    pattern: '/ # ^ \ \ / \ \? +? '.// Pattern matches the token's RegExp (string)
    modifier: ' ' // Modifiers used for segments (e.g.?)}]Copy the code

compile

Function: Quickly fill the parameter values of the URL string

const { compile } = require("path-to-regexp");

const url = '/foo/:bar'

const params = {bar: 123}

compile(url)(params)
/ / return/foo / 123

Copy the code

The overall analysis

Mode to distinguish

VueRouter supports three modes: Hash, History, and Abstract. Now let’s talk about the differences between the three modes. Hash uses the hash mode and this is what we see in the URL on the browser

http://localhost:8080/#/xxx     
Copy the code

The hash mode has the following characteristics:

  1. A # symbol will appear in the browser, and the browser will only send the link before the # to the server. The server does not need to do anything about it.
  2. We can use the HashChange event to listen for the change after #, so when it changes we can call the callback to do what we want.
  3. Good compatibility.

History uses the history mode and what we see in the url on the browser is this

http://localhost:8080/xxx   
Copy the code

History mode features:

  1. The url from the browser will be sent to the server in its entirety, and the server needs to process the URL or just throw in a 404.
  2. You can use popState events to listen for address changes
  3. Incompatible with some browsers

Abstract abstract is a routing method for javascript running outside of a browser.

The code structure

Let’s look at the directory structure and focus on the SRC path

│ ├─ Components │ ├─components │ ├─ route.js │ ├─components │ ├─components │ ├─components │ ├─components │ ├─components │ ├─components │ ├─components │ ├─components │ ├─components │ ├─components │ ├─components │ ├─components │ ├─components Router-link, router-view │ link.js │ view.js │ ├─history │ abstract.js │ base.js │ hash.js │ html5.js │ ├─history │ abstract ├ ─util // Proc, proc, proc, proc, proc, proc, proc, proc, proc, proc, proc, proc resolve-components.js route.js scroll.js state-key.js warn.jsCopy the code

The use of vuerouter

So before we read the code let’s review how do we use vuerouter in vue

// First introduce vuerouter and then install using the use method
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

// Define the route
const routes = [
  { path: '/foo'.component: Foo },
  { path: '/bar'.component: Bar }
]

// Create a router instance
const router = newVueRouter({routes}) mounts the instanceconst app = new Vue({
  router
}).$mount('#app')
Copy the code

The code analysis

To use VueRouter, we need to install vue.use (MyPlugin). This method will look in _installedPlugins to ensure that the plugin has been installed only once. Make sure that the first argument is Vue sample, if the argument is function, or if the object takes the install method.

src\install.js

// Provide the install method for vue
// Record whether it was installed by the installed if it was installed and not installed again
// Mount the routing method on the corresponding lifecycle by blending in
// Register RouterView and RouterLink components
// Register the routing lifecycle function
export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v= >v ! = =undefined

  const registerInstance = (vm, callVal) = > {
    let i = vm.$options._parentVnode // The _parentVnode attribute exists only when there is at least one VueComponent
    // If there is a VNode, VNode also initializes attrs: {}, hook, on listener event
    // registerRouteInstance is generated when the RouterView component is rendered
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    beforeCreate () {
    // If router is passed in the option, the root component will enter
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this) // Execute the init method on the router
        Vue.util.defineReactive(this.'_route'.this._router.history.current) // Vue provides global. Can not on the official website, the details see the vue source SRC \ core \ \ global - API index. The js position statement can be found, this is the water in the vue instance acting _route attribute, the attribute value is dependent on the current routing Dep management, triggering the get and set the trigger
      } else {
      }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this.this) // Register the instance
    },
    destroyed () {
      registerInstance(this) // Destroy the instance}})// Mount $router on the vue prototype so that it can be accessed via this.$router
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
// Mount $router on the vue prototype
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
// Register the RouterView and RouterLink components
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  // Route hook merge policy
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

Copy the code

So before we pass the router to vue, we need to initialize the new VueRouter to create an instance, and then pass that instance to vue

src\index.js


// This file is VueRouter's entry file

// The install method can refer to the error log in the script tag
VueRouter.install = install
VueRouter.version = '__VERSION__'
VueRouter.isNavigationFailure = isNavigationFailure
VueRouter.NavigationFailureType = NavigationFailureType
VueRouter.START_LOCATION = START

if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

// Let's see what VueRouter does
// The initial configuration item generates the routing mode available in the current environment, and then generates routing instances based on the mode
// Create a routing matcher
// Provides the init method mixed in with the use of vue.use
Init match onReady onError push replace go back forward getMatchedComponents resolve addRoutes addRoute
BeforeResolve (); afterEach ()
export default class VueRouter {
  static install: () = > void
  static version: string
  static isNavigationFailure: Function
  static NavigationFailureType: any
  static START_LOCATION: Route

  app: any
  apps: Array<any>
  ready: boolean
  readyCbs: Array<Function>
  options: RouterOptions
  mode: string
  history: HashHistory | HTML5History | AbstractHistory
  matcher: Matcher
  fallback: boolean
  beforeHooks: Array<? NavigationGuard> resolveHooks:Array<? NavigationGuard> afterHooks:Array<? AfterNavigationHook>// We can ignore the above code, only do a little type qualification, do not affect the principle of it

  constructor (options: RouterOptions = {}) { // Receive the passed options used
  // Initialize to save the passed options
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = [] 
    this.resolveHooks = []
    this.afterHooks = []
    // Create a routing matcher
    this.matcher = createMatcher(options.routes || [], this) 

    let mode = options.mode || 'hash' // Set mode to hash mode if nothing is passed
    this.fallback =
      mode === 'history'&&! supportsPushState && options.fallback ! = =false // If fallback is not passed or the fallback value is true, a compatibility is made
      // Determine if your mode is history, and if it is, determine if your current browser supports window.history and can call window.history.pushState.
      // If not, it is automatically compatible with the hash mode
    if (this.fallback) {
      mode = 'hash'
    }
    // If the window cannot be obtained, the server rendering default is to convert mode to abstract
    if(! inBrowser) { mode ='abstract'
    }
    this.mode = mode // Save the mode
    // The history class is instantiated according to the mode
    switch (mode) {
      case 'history':  / / the history
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':     / / the hash
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':    / / the abstract way
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if(process.env.NODE_ENV ! = ='production') {
          assert(false.`invalid mode: ${mode}`)}}}// The match method actually calls the method on the matchermatch (raw: RawLocation, current? : Route, redirectedFrom? : Location): Route {return this.matcher.match(raw, current, redirectedFrom)
  }
    // Getters to get the current routeget currentRoute (): ? Route {return this.history && this.history.current
  }
    // The initialization method is called during the beforeCreate declaration cycle
  init (app: any /* Vue component instance */) { process.env.NODE_ENV ! = ='production' &&
      assert(
        install.installed,
        `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
          `before creating root instance.`
      )

    this.apps.push(app) // Save the current component instance

   / / components in destroyed lifecycle callback of execution, the current component is destroyed, also delete the app from the app reset to null, destroy the function of the router
   $once('hook: Destroyed '); SRC \core\instance\events.js
    app.$once('hook:destroyed'.() = > {
        // If the app has been pushed, delete it from the apps after destruction, and then reset the app to make sure that the app is an instance or null is not undefined
      const index = this.apps.indexOf(app)
      if (index > -1) this.apps.splice(index, 1)
      if (this.app === app) this.app = this.apps[0] | |null
        // Execute the destruct function
      if (!this.app) this.history.teardown()
    })

     // this. App is not the root vue instance
    if (this.app) {
      return
    }
    // This instance is the root vue instance
    // Add a history and a route listener
    this.app = app

    const history = this.history
    // If the current mode is history mode or hash mode
    if (history instanceof HTML5History || history instanceof HashHistory) {
      const handleInitialScroll = routeOrError= > {
        const from = history.current // Record the current path
        const expectScroll = this.options.scrollBehavior // Pass the scrollBehavior to the router
        const supportsScroll = supportsPushState && expectScroll // Check whether the browser supports the History mode and whether the scrollBehavior event is passed in

        if (supportsScroll && 'fullPath' in routeOrError) {
          handleScroll(this, routeOrError, from.false)}}const setupListeners = routeOrError= > {
        history.setupListeners()
        handleInitialScroll(routeOrError)
      }
      history.transitionTo(   // Call the transitionTo method of the history instance
        history.getCurrentLocation(), // Hash value of the browser window address
        setupListeners, // Successful callback
        setupListeners  // Failed callback)}// Route global monitor, maintain the current route
    // Since _route is defined as a responsive property when install executes,
    // The _route is updated when the route changes, and the subsequent view update renders depend on the _route
    history.listen(route= > {
      this.apps.forEach(app= > {
        app._route = route
      })
    })
  }
    / / beforeEach hook
  beforeEach (fn: Function) :Function {
    return registerHook(this.beforeHooks, fn)
  }
    / / beforeResolve hook
  beforeResolve (fn: Function) :Function {
    return registerHook(this.resolveHooks, fn)
  }
    / / afterEach hook
  afterEach (fn: Function) :Function {
    return registerHook(this.afterHooks, fn)
  }
    // The following API calls a method on the history instance or a method on the Matcher instance
  onReady (cb: Function, errorCb? :Function) {
    this.history.onReady(cb, errorCb)
  }

  onError (errorCb: Function) {
    this.history.onError(errorCb) } push (location: RawLocation, onComplete? :Function, onAbort? :Function) {
    // $flow-disable-line
    if(! onComplete && ! onAbort &&typeof Promise! = ='undefined') {
      return new Promise((resolve, reject) = > {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort) } } replace (location: RawLocation, onComplete? :Function, onAbort? :Function) {
    // $flow-disable-line
    if(! onComplete && ! onAbort &&typeof Promise! = ='undefined') {
      return new Promise((resolve, reject) = > {
        this.history.replace(location, resolve, reject)
      })
    } else {
      this.history.replace(location, onComplete, onAbort)
    }
  }

  go (n: number) {
    this.history.go(n)
  }

  back () {
    this.go(-1)
  }

  forward () {
    this.go(1) } getMatchedComponents (to? : RawLocation | Route):Array<any> {
    const route: any = to
      ? to.matched
        ? to
        : this.resolve(to).route
      : this.currentRoute
    if(! route) {return[]}return [].concat.apply(
      [],
      route.matched.map(m= > {
        return Object.keys(m.components).map(key= > {
          return m.components[key]
        })
      })
    )
  }

  resolve (
    to: RawLocation,
    current?: Route,
    append?: boolean
  ): {
    location: Location,
    route: Route,
    href: string,
    // for backwards compat
    normalizedTo: Location,
    resolved: Route
  } {
    current = current || this.history.current
    const location = normalizeLocation(to, current, append, this)
    const route = this.match(location, current)
    const fullPath = route.redirectedFrom || route.fullPath
    const base = this.history.base
    const href = createHref(base, fullPath, this.mode)
    return {
      location,
      route,
      href,
      // for backwards compat
      normalizedTo: location,
      resolved: route
    }
  }

  getRoutes () {
    return this.matcher.getRoutes() } addRoute (parentOrRoute: string | RouteConfig, route? : RouteConfig) {this.matcher.addRoute(parentOrRoute, route)
    if (this.history.current ! == START) {this.history.transitionTo(this.history.getCurrentLocation())
    }
  }

  addRoutes (routes: Array<RouteConfig>) {
    if(process.env.NODE_ENV ! = ='production') {
      warn(false.'router.addRoutes() is deprecated and has been removed in Vue Router 4. Use router.addRoute() instead.')}this.matcher.addRoutes(routes)
    if (this.history.current ! == START) {this.history.transitionTo(this.history.getCurrentLocation())
    }
  }
}

Copy the code

src\create-matcher.js

// Create a routing matcher
// Collect the pathList route set, pathMap route path mapping, and nameMap route name mapping
// pathList: ['/foo','/bar']  
//pathMap:{'/foo': {" path ":"/foo ", "the regex" : {" keys ": []}," components ": {" default" : {" template ":" < div > this is foo < / div > "}}, "alias" : [], "instances" : {}, "e nteredCbs":{},"meta":{},"props":{}}}
/ / nameMap: {' foo ': {" path ":"/foo ", "the regex" : {" keys ": []}," components ": {" default" : {" template ":" < div > this is foo < / div > "}}, "alias" : []. "instances":{},"enteredCbs":{},"meta":{},"props":{}}}
// Returns the addRoutes, addRoute, getRoutes, and match methods
// Forward route by name > Forward route by path
export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
) :Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
// Add routes in batches add new mappings to the existing mappings
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
// Add a single route
  function addRoute (parentOrRoute, route) {
  // If parentOrRoute is a routerOPtions path, add it as normal; if parentOrRoute is a string path, find it from the nameMap mapping.
    const parent = (typeofparentOrRoute ! = ='object')? nameMap[parentOrRoute] :undefined
    // $flow-disable-line
    createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent)

    // If it can be found from the nameMap mapping table, fetch the route name corresponding to the name. If there is an alias option, regenerate a route mapping corresponding to the alias
    if (parent && parent.alias.length) {
      createRouteMap(
        // $flow-disable-line route is defined if parent is
        parent.alias.map(alias= > ({ path: alias, children: [route] })),
        pathList,
        pathMap,
        nameMap,
        parent
      )
    }
  }
    // Walk through the pathList to return the new array mapped by the pathMap
  function getRoutes () {
    return pathList.map(path= > pathMap[path])
  }
    // Match the route
  function match (raw: RawLocation, currentRoute? : Route, redirectedFrom? : Location) :Route {
  // Take out parameters such as path, params, query, and name in the route
    // Location is an object similar to
    // {"_normalized":true,"path":"/","query":{},"hash":""}
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location
    // If there is a name, it is fetched from the nameMap mapping table
    if (name) {
      const record = nameMap[name]
      if(process.env.NODE_ENV ! = ='production') {
        warn(record, `Route with name '${name}' does not exist`)}// Create a route object if there is no route mapping
      if(! record)return _createRoute(null, location)
      // Filter out the optional parameters and generate an array of parameter names
      const paramNames = record.regex.keys
        .filter(key= >! key.optional) .map(key= > key.name)
        
     // If the params parameter is not object, the type of the location is not reinitialized
      if (typeoflocation.params ! = ='object') {
        location.params = {}
      }
    // If the current route has params parameter
    // Iterate over the current route parameters, and attach the generated route parameters to location.params
      if (currentRoute && typeof currentRoute.params === 'object') {
        for (const key in currentRoute.params) {
          if(! (keyin location.params) && paramNames.indexOf(key) > -1) {
            location.params[key] = currentRoute.params[key]
          }
        }
      }
        // Finally return a path to convert the parameter to a valid path
      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      return _createRoute(record, location, redirectedFrom)
    } else if (location.path) {
    // Initialize parameters
      location.params = {}
      for (let i = 0; i < pathList.length; i++) {
        const path = pathList[i]
        const record = pathMap[path]
        // If the route matches, create a new route mapping
        if (matchRoute(record.regex, location.path, location.params)) {
          return _createRoute(record, location, redirectedFrom)
        }
      }
    }
    // What is not matched
    return _createRoute(null, location)
  }

  function redirect (record: RouteRecord, location: Location) :Route {
  // The redirection configuration in the route is not a function. If it is a function, generate the frozen route as the parameter of the function
    const originalRedirect = record.redirect
    let redirect = typeof originalRedirect === 'function'
      ? originalRedirect(createRoute(record, location, null, router))
      : originalRedirect
    // If redirect is configured to encapsulate strings into object mode
    if (typeof redirect === 'string') {
      redirect = { path: redirect }
    }

    // Handle error conditions
    if(! redirect ||typeofredirect ! = ='object') {
      if(process.env.NODE_ENV ! = ='production') {
        warn(
          false.`invalid redirect option: The ${JSON.stringify(redirect)}`)}return _createRoute(null, location)
    }

    const re: Object = redirect
    const { name, path } = re
    let { query, hash, params } = location
    // The processing parameter is used if redirect is available. If redirect is not available, the processed routing information is used
    query = re.hasOwnProperty('query')? re.query : query hash = re.hasOwnProperty('hash')? re.hash : hash params = re.hasOwnProperty('params')? re.params : paramsif (name) { // Match name redirection
      const targetRecord = nameMap[name]
      if(process.env.NODE_ENV ! = ='production') {
        assert(targetRecord, `redirect failed: named route "${name}" not found.`)}// Generate a matching route object
      return match({
        _normalized: true,
        name,
        query,
        hash,
        params
      }, undefined, location)
    } else if (path) { // Match path redirection
      // Resolve the redirected route
      const rawPath = resolveRecordPath(path, record)
      // Compile the parameters to the URL, using the compile method in path-to-regexp
      const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`)
      // Generate a matching route object
      return match({
        _normalized: true.path: resolvedPath,
        query,
        hash
      }, undefined, location)
    } else {
    // Handle error conditions
      if(process.env.NODE_ENV ! = ='production') {
        warn(false.`invalid redirect option: The ${JSON.stringify(redirect)}`)}return _createRoute(null, location)
    }
  }

  function alias (record: RouteRecord, location: Location, matchAs: string) :Route {
  // Get a url filled with parameters
    const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`)
    // Generate a matching route object
    const aliasedMatch = match({
      _normalized: true.path: aliasedPath
    })
    // If the match is successful
    if (aliasedMatch) {
    // Regenerate a frozen route object with processing parameters
      const matched = aliasedMatch.matched
      const aliasedRecord = matched[matched.length - 1]
      location.params = aliasedMatch.params
      return _createRoute(aliasedRecord, location)
    }
     // If the match fails
    return _createRoute(null, location)
  }

  function _createRoute (record: ? RouteRecord, location: Location, redirectedFrom? : Location) :Route {
   // If the route is redirected
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    // If the route is configured with an alias
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    // Return a frozen route object
    return createRoute(record, location, redirectedFrom, router)
  }

  return {
    match,
    addRoute,
    getRoutes,
    addRoutes
  }
}

Copy the code

Summary: In Matcher call createRouteMap method Mr PathList, pathMap, nameMap set. SRC \ create-rout-map. js is a loop router that generates an object that describes the route specified by the record. If there are children in the router, the * wildcard path will be placed at the end of the other routes. The matcher provides a match, addRoute getRoutes, addRoutes, alias, so a few redirect method to create routing addRoute, AddRoutes add new mappings to the pathList, pathMap, and nameMap collections. GetRoutes iterates through the pathList to get the path and then returns the Path object in the pathMap. The alias and rediret methods are called when the alias and redirection are triggered. Notice the difference between changing the route and changing the view and changing the view without changing the route. The similarity between the two methods is that they call match to take a set of matched routes and return the matched route object.

Routing patterns

    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if(process.env.NODE_ENV ! = ='production') {
          assert(false.`invalid mode: ${mode}`)}}Copy the code

When you generate the three pattern routes you create an instance of each of the three patterns and we’ll look at each of them. There are four JS files in the SRC history folder that correspond to three routing mode classes and a base class. All three classes inherit from this base class. So let’s first look at what the base class does

History

src\history\base.js

export class History {
  router: Router
  base: string
  current: Route
  pending: ?Route
  cb: (r: Route) = > void
  ready: boolean
  readyCbs: Array<Function>
  readyErrorCbs: Array<Function>
  errorCbs: Array<Function>
  listeners: Array<Function>
  cleanupListeners: Function
  +go: (n: number) = > void
  +push: (loc: RawLocation, onComplete? :Function, onAbort? :Function) = > void
  +replace: (loc: RawLocation, onComplete? :Function, onAbort? :Function
  ) = > void
  +ensureURL: (push? : boolean) = > void
  +getCurrentLocation: () = > string
  +setupListeners: Function
// If you want to add +function, you can use the flow document
// Let's go to the main code
    // Take two parameters, one is the router instance and the other is the baseurl
    // Initialize each item
  constructor (router: Router, base: ? string) {
    this.router = router
    // Initialize baseurl
    this.base = normalizeBase(base)
    this.current = START // The default current route is an object generated by the/initial route
    this.pending = null
    this.ready = false // Mark that the route loading is complete
    this.readyCbs = [] // The callback queue has been loaded
    this.readyErrorCbs = [] // Failed to load the callback queue
    this.errorCbs = [] // Error callback queue
    this.listeners = [] // Listen to queue
  }
    // The listening function saves the passed callback function
  listen (cb: Function) {
    this.cb = cb
  }
// The onReady event executes the current callback if the route loading is complete, puts the callback in the queue if it is not complete, and puts the failed callback in the failed callback queue if it is passed
  onReady (cb: Function.errorCb:?Function) {
    if (this.ready) {
      cb()
    } else {
      this.readyCbs.push(cb)
      if (errorCb) {
        this.readyErrorCbs.push(errorCb)
      }
    }
  }
// Register a callback that will be called in case of an error during route navigation.
  onError (errorCb: Function) {
    this.errorCbs.push(errorCb)
  }

// This method is an important one. It is called at the bottom of history.push and history.replace.
// You can accept three parameters: location, onComplete, onAbort,
// Are the target path, the callback when the path switchover succeeds, and the callback when the path switchover fails.transitionTo ( location: RawLocation, onComplete? :Function, onAbort? :Function
  ) {
    let route
    try {
     // Call the match method to get the matched route object
      route = this.router.match(location, this.current)
    } catch (e) {
    // If an error occurs, execute the callback in the errorCbs queue
      this.errorCbs.forEach(cb= > {
        cb(e)
      })
      throw e
    }
    const prev = this.current
     // Transition processing
    this.confirmTransition(
      route,
      () = > {
       // Update current, if there is a callback, perform the callback cb
        this.updateRoute(route)
        onComplete && onComplete(route)
        // Update url hash mode Update hash value The History mode updates with pushState/replaceState
        this.ensureURL()
        // Perform afterEach hook callback
        this.router.afterHooks.forEach(hook= > {
            AfterEach hook function parameter
          hook && hook(route, prev)
        })

        // Resets the ready state, traverses the readyCbs queue, and executes cb
        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb= > {
            cb(route)
          })
        }
      },
      err= > {
        if (onAbort) {
          onAbort(err)
        }
        if (err && !this.ready) {
          The initial redirection should not mark the history as ready
          // Because it is triggered by redirection
          if(! isNavigationFailure(err, NavigationFailureType.redirected) || prev ! == START) {this.ready = true
            this.readyErrorCbs.forEach(cb= > {
              cb(err)
            })
          }
        }
      }
    )
  }

  confirmTransition (route: Route, onComplete: Function, onAbort? :Function) {
    const current = this.current
    this.pending = route
      // Interrupt the hop routing function
    const abort = err= > {
      if(! isNavigationFailure(err) && isError(err)) {if (this.errorCbs.length) {
          this.errorCbs.forEach(cb= > {
            cb(err)
          })
        } else {
          warn(false.'uncaught error during route navigation:')
          console.error(err)
        }
      }
      onAbort && onAbort(err)
    }
    
    const lastRouteIndex = route.matched.length - 1
    const lastCurrentIndex = current.matched.length - 1
    // If the current route is the same as the previous route, return the url
    if (
      isSameRoute(route, current) &&
      lastRouteIndex === lastCurrentIndex &&
      route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
    ) {
      this.ensureURL()
      return abort(createNavigationDuplicatedError(current, route))
    }
 // Use the asynchronous queue to cross-compare the current route record with the current route record
    // In order to know exactly which components need to be updated and which do not need to be updated
    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )
      // Navigate the guard array
    // Use the array queue to store the corresponding route hook function
    const queue: Array<? NavigationGuard> = [].concat(// The beforeRouteLeave hook in the component
      extractLeaveGuards(deactivated),
       // The global beforeEach hook
      this.router.beforeHooks,
         // called when the current route has changed but the component is being reused
      extractUpdateHooks(updated),
     // beforeEnter check for the route to be updated
      activated.map(m= > m.beforeEnter),
      // Asynchronous components
      resolveAsyncComponents(activated)
    )
    // The iterator function for queue execution
    const iterator = (hook: NavigationGuard, next) = > {
      // If the routes are not equal, the route is not forwarded
      if (this.pending ! == route) {return abort(createNavigationCancelledError(current, route))
      }
      try {
      // Execute the hook
      // The next hook function is executed only when next is executed
      // Otherwise the jump will be suspended
      // The following logic is passed to determine next()
        hook(route, current, (to: any) = > {
        
          if (to === false) { / / if the next (false)
            this.ensureURL(true)
            abort(createNavigationAbortedError(current, route))
          } else if (isError(to)) { / / if the next (error)
            this.ensureURL(true)
            abort(to)
          } else if ( // If next('/') or next({path: '/'})
            typeof to === 'string'| | -typeof to === 'object' &&
              (typeof to.path === 'string' || typeof to.name === 'string'))
          ) {
            abort(createNavigationRedirectedError(current, route))
            if (typeof to === 'object' && to.replace) { next({ path: '/'.repalce:true  })
              this.replace(to)
            } else {
              this.push(to)
            }
          } else {
           // Execute next here
           RunQueue step(index + 1)
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }
     A recursive callback is used to start the execution of an asynchronous function queue
    runQueue(queue, iterator, () = > {
    // Hooks within the component
      const enterGuards = extractEnterGuards(activated)
      const queue = enterGuards.concat(this.router.resolveHooks)
    // When all asynchronous components have been loaded, the callback here, which is cb() in runQueue, is executed
    // Next, execute the navigation guard hook that requires the rendering component
      runQueue(queue, iterator, () = > {
       // The jump is complete
        if (this.pending ! == route) {return abort(createNavigationCancelledError(current, route))
        }
        this.pending = null 
        onComplete(route) // Call the callback in transitionTo to update current, perform the afterHook callback, change the ready state, and loop through readyCbs
        if (this.router.app) {
          this.router.app.$nextTick(() = > {
            handleRouteEntered(route)
          })
        }
      })
    })
  }

  updateRoute (route: Route) {
    this.current = route
    this.cb && this.cb(route)
  }

// Destroy events: clean listeners initialize current, pending
  teardown () {
    this.listeners.forEach(cleanupListener= > {
      cleanupListener()
    })
    this.listeners = []
    this.current = START
    this.pending = null}}// Receive two parameters, the current route matched and the target route matched. Iterate over the two array comparison records. If the records are different, terminate the loop and record the current index I
// Next from 0 to I is the same as current from I, next from I to DEactivated, current from I to deactivated, same part is updated, After being processed by the resolveQueue, the part of the route that needs to be changed is obtained. A series of hook functions can then be executed based on the route change.
function resolveQueue (
  current: Array<RouteRecord>,
  next: Array<RouteRecord>
) :{
  updated: Array<RouteRecord>,
  activated: Array<RouteRecord>,
  deactivated: Array<RouteRecord>
} {
  let i
  const max = Math.max(current.length, next.length)
  for (i = 0; i < max; i++) {
   // The current route path and the jump route path do not jump traversal at the same time
    if(current[i] ! == next[i]) {break}}return {
   // Reusable components correspond to routes
    updated: next.slice(0, i),
     // The component to be rendered corresponds to the route
    activated: next.slice(i),
        // Route of the component that is deactivated
    deactivated: current.slice(i)
  }
}



Copy the code

The transitionTo function is used to switch routes. It does two things: first, it matches the destination route object using the this.router-match method based on the destination location and the current route object. Route looks like this

{fullPath: "/detail/394" hash: "" matched: [{...}] meta: {title: "detail"} name: "detail" params: {id: "394"} path: "/detail/394" query: {} }Copy the code

An object that contains basic information about the target route. Then execute the confirmTransition method to do the actual route switchover. Because there are some asynchronous components, there are some asynchronous operations. And then I execute some hook functions, The execution order of hook functions is beforeRouteLeave => beforeEach => beforeRouteUpdate => beforeEnter => beforeRouteEnter => beforeResolve => afterEach

HashHistory

Here’s the first example of HashHistory. HTML5History is very similar to what it does. HashHistory does compatibility in certain methods.

src\history\hash.js

export class HashHistory extends History {
  constructor (router: Router, base: ? string, fallback: boolean) {
    super(router, base)
    // Override url that can be degraded
    if (fallback && checkFallback(this.base)) {
      return
    }
    // Check whether the HASH of the URL has a #, and if it does not, rewrite the URL to append # to it
    ensureSlash()
  }

  // Set the listener
  setupListeners () {
  // Return if there is already a listener
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
// If the history mode is supported, put the installation scroll method in the queue
    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }
    const handleRoutingEvent = () = > {
      const current = this.current
      if(! ensureSlash()) {return
      }
      this.transitionTo(getHash(), route= > {
      // If scrolling is supported, the scrolling event is passed in. When it is finished, the window. ScrollTo is changed in nextTick to change the content of the page
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)}// Override the URL if scrolling is not supported
        if(! supportsPushState) { replaceHash(route.fullPath) } }) }// Listen for hashChange events and execute this.transitionTo to switch pages
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    windowAddEventListener (eventType, handleRoutingEvent) queues the function that unbinds the HashChange eventthis.listeners.push(() = > {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }
// The VueRouter will call this.transitionTo to start performing some hook functions and then start the resolve component. After that, it will call pushHash(route.fullPath) to change the URL. PushHash ultimately calls pushState, but replace is passed false.push (location: RawLocation, onComplete? :Function, onAbort? :Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route= > {
        pushHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }
// Replace the route, which is not added to the recordreplace (location: RawLocation, onComplete? :Function, onAbort? :Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route= > {
        replaceHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }
/ / call historyapi
  go (n: number) {
    window.history.go(n) } ensureURL (push? : boolean) {const current = this.current.fullPath
    if(getHash() ! == current) { push ? pushHash(current) : replaceHash(current) } }// Get the current URL
  getCurrentLocation () {
    return getHash()
  }
}


Copy the code

Summary: Both the push and replace methods invoke the transitionTo to switch routes, triggering the route lifecycle hook. The difference is that push triggers pushState. This is a fault-tolerant way to determine if there is an HTML5 History API. If history.pushState() is supported for browser history, Otherwise, replace the document with window.location.hash = path. Note: Calling history.pushState() does not trigger the popState event. Popstate only triggers certain browser actions, such as clicking the back and forward buttons.

The router components

The VueRouter provides two components: router-view and router-link

router-view

Router-view is a functional component that has no instance of its own. It is responsible for calling keepAlive $route.match and other properties/methods stored on the parent component to control the rendering of the component to which the route corresponds. Its own page location is ultimately where the routing component on which it matches is mounted.

src\components\view.js

export default {
  name: 'RouterView'.functional: true.props: {
    name: {
      type: String.default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    // The tag is the RouterView component
    data.routerView = true

// The benefit of using the parent component's $createElement is that we know the naming slot
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

   // Depth Indicates the current component depth, which is used as a tag for routing nesting
   // Inactive indicates whether the inactive state is active
    let depth = 0
    let inactive = false
    // Loop recursively through the component depth and read if the current component is active
    while(parent && parent._routerRoot ! == parent) {const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      if (vnodeData.routerView) {
        depth++
      }
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    // Record the current depth
    data.routerViewDepth = depth

    // If it is a keepAlive component, fetch it from the cache
    if (inactive) {
      const cachedData = cache[name]
      const cachedComponent = cachedData && cachedData.component
      if (cachedComponent) {
        // pass props
        if (cachedData.configProps) {
          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
        }
        return h(cachedComponent, data, children)
      } else {
       // Render an empty view
        return h()
      }
    }
    // Find the matching depth component
    const matched = route.matched[depth]
    const component = matched && matched.components[name]

    // If no render null node is found
    if(! matched || ! component) { cache[name] =null
      return h()
    }

    // Cache component
    cache[name] = { component }
    // When installing VueRouter
    // Both life cycles are executed by registering and destroying
    data.registerRouteInstance = (vm, val) = > {
      const current = matched.instances[name]
      if( (val && current ! == vm) || (! val && current === vm) ) { matched.instances[name] = val } } ; (data.hook || (data.hook = {})).prepatch =(_, vnode) = > {
      matched.instances[name] = vnode.componentInstance
    }

    data.hook.init = (vnode) = > {
      if(vnode.data.keepAlive && vnode.componentInstance && vnode.componentInstance ! == matched.instances[name] ) { matched.instances[name] = vnode.componentInstance } handleRouteEntered(route) }const configProps = matched.props && matched.props[name]
    if (configProps) {
      extend(cache[name], {
        route,
        configProps
      })
      fillPropsinData(component, data, route, configProps)
    }
// Render component
    return h(component, data, children)
  }
}

Copy the code
router-link

src\components\link.js

export default {
  name: 'RouterLink'.props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String.default: 'a'
    },
    custom: Boolean.exact: Boolean.exactPath: Boolean.append: Boolean.replace: Boolean.activeClass: String.exactActiveClass: String.ariaCurrentValue: {
      type: String.default: 'page'
    },
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
  // Obtain the route instance and object
    const router = this.$router
    const current = this.$route
    // Resolve the new path
    const { location, route, href } = router.resolve(
      this.to,
      current,
      this.append
    )

    const classes = {}
    // Merge the global class configuration
    const globalActiveClass = router.options.linkActiveClass
    const globalExactActiveClass = router.options.linkExactActiveClass
    const activeClassFallback =
      globalActiveClass == null ? 'router-link-active' : globalActiveClass
    const exactActiveClassFallback =
      globalExactActiveClass == null
        ? 'router-link-exact-active'
        : globalExactActiveClass
    const activeClass =
      this.activeClass == null ? activeClassFallback : this.activeClass
    const exactActiveClass =
      this.exactActiveClass == null
        ? exactActiveClassFallback
        : this.exactActiveClass

    // Create a redirect route object
    const compareTarget = route.redirectedFrom
      ? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
      : route
  // Record the router-link-active router-link-exact-active status
    classes[exactActiveClass] = isSameRoute(current, compareTarget, this.exactPath)
    classes[activeClass] = this.exact || this.exactPath
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget)

    const ariaCurrentValue = classes[exactActiveClass] ? this.ariaCurrentValue : null
    / / the event processing
    const handler = e= > {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location, noop)
        } else {
          router.push(location, noop)
        }
      }
    }
    
    const on = { click: guardEvent }
    // Hang the handled event for each event
    if (Array.isArray(this.event)) {
      this.event.forEach(e= > {
        on[e] = handler
      })
    } else {
      on[this.event] = handler
    }

    const data: any = { class: classes }

    const scopedSlot =
      !this.$scopedSlots.$hasNormal &&
      this.$scopedSlots.default &&
      this.$scopedSlots.default({
        href,
        route,
        navigate: handler,
        isActive: classes[activeClass],
        isExactActive: classes[exactActiveClass]
      })
// Handle the scope slot
    if (scopedSlot) {
      if(process.env.NODE_ENV ! = ='production'&&!this.custom) { ! warnedCustomSlot && warn(false.'In Vue Router 4, the v-slot API will by default wrap its content with an <a> element. Use the custom prop to remove this warning:\n<router-link v-slot="{ navigate, href }" custom></router-link>\n')
        warnedCustomSlot = true
      }
      if (scopedSlot.length === 1) {
        return scopedSlot[0]}else if (scopedSlot.length > 1| |! scopedSlot.length) {if(process.env.NODE_ENV ! = ='production') {
          warn(
            false.`<router-link> with to="The ${this.to
            }" is trying to use a scoped slot but it didn't provide exactly one child. Wrapping the content with a span element.`)}return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
      }
    }

    if(process.env.NODE_ENV ! = ='production') {
      if ('tag' in this.$options.propsData && ! warnedTagProp) { warn(false.`
      
       's tag prop is deprecated and has been removed in Vue Router 4. Use the v-slot API to remove this warning:  https://next.router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link.`
      
        )
        warnedTagProp = true
      }
      if ('event' in this.$options.propsData && ! warnedEventProp) { warn(false.`<router-link>'s event prop is deprecated and has been removed in Vue Router 4. Use the v-slot API to remove this warning: https://next.router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link.`
        )
        warnedEventProp = true}}// Handle the case where label A is used
    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href, 'aria-current': ariaCurrentValue }
    } else {
     // Loop recursively to find if there is a tag in the default slot
      const a = findAnchor(this.$slots.default)
      if (a) {
       // Mark static
        a.isStatic = false
        // Copy the attributes in tag A
        const aData = (a.data = extend({}, a.data))
        // Copy and initialize the event
        aData.on = aData.on || {}
      // Reassign the handled event
        for (const event in aData.on) {
          const handler = aData.on[event]
          if (event in on) {
            aData.on[event] = Array.isArray(handler) ? handler : [handler]
          }
        }
      // Assign the event to the a tag
        for (const event in on) {
          if (event in aData.on) {
            aData.on[event].push(on[event])
          } else {
            aData.on[event] = handler
          }
        }
        // Reprocess the href attribute
        const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
        aAttrs.href = href
        aAttrs['aria-current'] = ariaCurrentValue
      } else {
        // If there is no a tag, listen for the event itself
        data.on = on
      }
    }

    return h(this.tag, data, this.$slots.default)
  }
}

Copy the code

conclusion

Route guard global guard:

  • BeforeEach (to, From, next) Global front guard
  • BeforeResolve (to, from, next) Resolves the guard globally
  • AfterEach (to, From) Global afterhook

Single route guard:

  • beforeEnter (to, from, next)

Component routing guard:

  • BeforeRouteLeave (to, from, next) calls the Leave guard in the deactivated component
  • BeforeRouteUpdate (to, from, next) is called in the reused component
  • BeforeRouteEnter (to, from, next) is called before the component is created to enter the corresponding route

Route navigation resolution 1. Hop back to the dynamic path. For example, hop 2 between /foo/:id /foo/1 and /foo/2. Triggering beforeRouteLeave (deactivating component) 3. Triggering beforeEach (global route Guard) 4. Trigger beforeRouteUpdate (reuse component) 5. Trigger beforeEnter (separate route guard) 6. Parsing the asynchronous component 7. Trigger beforeRouterEnter 8. Trigger beforeResolve (Global Route Guard) 9. Navigation is acknowledged 10. Trigger afterEach (Global Route Guard) 11. Trigger DOM updates 12. Call the next callback in beforeRouteEnter

Routing usage

{// Matches all paths: '*'} // When using wildcard routes, ensure that the routes are in the correct order, that is, the routes with wildcard characters should be placed last. The route {path: '*'} is usually used for client 404 errors. {// match any path starting with '/user- ': '/user-*'} // When the * wildcard is used in $route.params, a pathMatch parameter is automatically added. '/user-*'} this.$route.push ('/user-foo') this.$route.params.pathmatch // 'fo'o' // give a route {path: '*' } this.$router.push('/non-existing') this.$route.params.pathMatch // '/non-existing' // ? The optional parameter {path: '/optional-params/:foo? } // The route jump can set or not set the foo parameter, Optional <router-link to="/optional-params">/optional-params</router-link> <router-link To ="/optional-params/foo">/optional-params/foo</router-link> '/optional-params/*'} <router-link to="/number"> No parameter </router-link> <router-link to="/number/foo000"> One parameter </router-link> <router-link to="/number/foo111/fff222"> Multiple parameters </router-link> // One or more parameters {path: '/optional-params/:foo+'} <router-link to="/number/foo"> A parameter </router-link> <router-link To = "/ number/foo/foo111 / fff222" > multiple parameter < / router - the link > / / / / custom matching parameters can provide all the parameters with a custom regexp, it will override the default values (+) / [^ \] {path: '/optional-params/:id(\\d+)' } { path: '/optional-params/(foo/)? bar' }Copy the code

conclusion

VueRouter we said this first, what is wrong, please correct the big guy.

There are students who want to see vuE2 source code VUE2 source analysis

There are students who want to see the principle of vuE1 write a simplified version of vuE1

At last, please move your hands and give us a thumbs up!!