Decrypt vue-router: from source

Keywords: Javascript Vue html5 Attribute

The other day I saw a question: Do you really know vue-router?Do you know how vue-router works?With this in mind, the author started the source exploration journey of vue-router.This article does not go into the source line by line, but follows the flow chart drawn by the author to analyze the operation process of each step.

Analyze Running Flow

The author draws a flowchart beforehand according to the structure of the source code and his own understanding. At first glance, this flowchart may be a little obscured. The author will now analyze the running process according to this diagram, and then analyze the core part of the source code step by step.

To help us understand this flowchart, we'll print out the Vue instances that have been mounted on the vue-router to see what's added:

  • The router object under $options is well understood, which is the vue-router instance that we mounted when instantiating Vue;
  • _route is a responsive routing route object that stores our routing information. It is responsive through Vue.util.defineReactive provided by Vue. The following get and set are the data hijacks performed on it.
  • _router stores the vue-router objects we get from $options;
  • _routerRoot points to our Vue root node;
  • _routerViewCache is our cache of Views;
  • $route and $router are the two getter s defined on the Vue.prototype.The former points to _route under _routerRoot, the latter to _router under _routerRoot

Next let's follow this "dazzling picture" to better understand the source analysis behind us.

First we installed vue-router according to Vue's plug-in mechanism. What we did here is very simple. To sum up, we encapsulated a mixin, defined two'prototypes', and registered two components.In this mixin, the beforeCreate hook is invoked to determine if the vue-router instantiates a conversation and initializes routing-related logic, as defined in the _routerRoot, _router, _route mentioned earlier.Defining two "prototypes" means setting up two getter s on the Vue.prototype, that is, $route and $router.Registering two components means registering two components here, RouterView and RouterLink, which we will use later.

We then created an instance of VueRouter and mounted it on the Vue instance, where the constructor in the VueRouter instance initialized various hook queues; initialized the matcher to do our routing matching logic and create routing objects; initialized the history to perform transitional logic and execute the hook queueColumn.

The next thing beforeCreate does in mixin is initialize the init() method of our VueRouter instance, a process similar to the one we click on RouteLink or function-controlled routing, which I've said here.The transitionTo method of the history object is invoked in the init method, then matches to get the data of the current route match and creates a new route object. Next, the route object is taken to perform the confirmTransition method to execute events in the hook queue, and finally updateRoute is used to update the storageThe current object of the former routing data points to the route object we just created.

At the beginning, we said that after _route is defined as a responsive route update, the _route object receives a response and notifies RouteView to update the view.

At this point, the process is over, and we'll go into the source code of vue-router to learn more about its principles.

Parse Source

Say before

The source code of vue-router uses flow as type check. Without flow configuration, it may be full screen error. This article does not introduce flow much.For your understanding, I'll remove the flow-related syntax from the source code section below.Incidentally include some flow-related:

Official flow Documentation (requires scientific Internet access): https://flow.org/
Getting started with flow: https://zhuanlan.zhihu.com/p/...
flow configuration: https://zhuanlan.zhihu.com/p/...

Project structure

When we get the source code for a project, we first need to look at its directory structure:

Where src is our project source part, it contains the following structure:

  • Compoonets are components of RouterLink and RouterView;
  • create-matcher.js is the entry file where we create match;
  • create-route-map.js is used to create path lists, path map s, name map s, etc.
  • history is the logic for creating a hitory class;
  • index.js is our entry file, where the VueRouter class is created;
  • install.js is our logic for mounting vue-router plugins;
  • util defines a number of tool functions;

Application Entry

Usually when we go to build a Vue application, the entry file will say this:

// app.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Main from '../components/main';

Vue.use(VueRouter);

const router = new VueRouter({
  routes: [{
    path: '/',
    component: Main,
  }],
});

// app.js
new Vue({
  router,
  template,
}).$mount('#app')

We can see that vue-router is installed as a plug-in, and instances of vue-router are also mounted on the instances of Vue.

Plug-in Installation

At this point, we look into the source's entry file and find that the install module is introduced in index.js and a static install method is mounted on the VueRouter class.It also determines that the plug-in will automatically be used if the Vue is already mounted in the environment.

Source location: /src/index.js

import { install } from './install'
import { inBrowser } from './util/dom'
// ...
export default class VueRouter {}
// ...
// Mount install;
VueRouter.install = install
// Judge that if the Vue is mounted on the window, the plug-in will be used automatically;
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

Next, look at the file install.js, which exports the export method for Vue.use to install:

Source location: /src/install.js

import View from './components/view'
import Link from './components/link'

// The reason export has a Vue is that you can use some of the Vue methods instead of packaging the Vue into the plug-in.
// Instances of this Vue can only exist after install;
export let _Vue

export function install (Vue) {
  // return if plug-in is already installed
  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
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    beforeCreate () {
      // this.$options.router is a VueRouter instance;
      // This determines if the instance has been mounted;
      if (isDef(this.$options.router)) {
        // Point the root component of router to the Vue instance
        this._routerRoot = this
        this._router = this.$options.router
        // router initialization, calling the init method of VueRouter;
        this._router.init(this)
        // Increase_route responsive objects using Vue's defineReactive
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // Point each component's _routerRoot to the root Vue instance;
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      // Register VueComponent for Observer processing;
      registerInstance(this, this)
    },
    destroyed () {
      // Log off VueComponent
      registerInstance(this)
    }
  })
  // Define << getter >> for $router and 4 routes to _router and _route for _routerRoot, respectively
  // _router is an instance of VueRouter;
  // _route is an object that stores routing data;
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  // Register Components
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  // Vue hook merge policy
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

Here are some points to note:

  • Export a Vue reference: This is to use some API s provided by Vue without packaging the entire Vue, which of course must be installed and mounted;
  • Define two getter s on the Vue.prototype: the components of the Vue are an extension of the Vue instance, and they both have access to methods and properties on the prototype;
  • Define a responsive_route object: With this responsive routing object, you can notify RouterView to update components in time when routing updates occur.

Instantiate VueRouter

Next, let's look at the instantiation of the VueRouter class. There are two main things to do in the constructor, creating a matcher and creating a history:

Source location: /src/index.js

// ...
import { createMatcher } from './create-matcher'
import { supportsPushState } from './util/push-state'
import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'
// ...
export default class VueRouter {
  constructor (options) {
    this.app = null
    this.apps = []
    // VueRouter configuration item;
    this.options = options
    // Three hooks
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    // Create routing matching instances; pass on routes that we define: objects containing path s and component s;
    this.matcher = createMatcher(options.routes || [], this)
    // Judgement mode
    let mode = options.mode || 'hash'
    // Determine whether the browser supports history or falls back to hash mode if it does not;
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    // node runtime environment mode ='abstract';
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
    // Create a corresponding history instance from a pattern
    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}`)
        }
    }
  }
  // ...
}

Create a matcher

Following this line, let's first look at the createMatcher function:

Source location: /src/create-matcher.js

import VueRouter from './index'
import { resolvePath } from './util/path'
import { assert, warn } from './util/warn'
import { createRoute } from './util/route'
import { fillParams } from './util/params'
import { createRouteMap } from './create-route-map'
import { normalizeLocation } from './util/location'

// routes initializes the routing configuration for VueRouter;
// router is our VueRouter instance;
export function createMatcher (routes, router) {
  // A pathList is an array of paths generated from routes;
  // A pathMap is a map generated from the name of the path;
  // If we define a name on the routing configuration, then there will be a Map with such a name.
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  // Generate routes based on new routes;
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  // Route matching function;
  function match (raw, currentRoute, redirectedFrom) {
    // Simply put, take out our path params query and so on.
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location

    if (name) {
      // If there is a name, go to the name map to find the route record.
      const record = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {
        warn(record, `Route with name '${name}' does not exist`)
      }
      // Create a routing object without this routing record.
      if (!record) return _createRoute(null, location)
      const paramNames = record.regex.keys
        .filter(key => !key.optional)
        .map(key => key.name)

      if (typeof location.params !== 'object') {
        location.params = {}
      }

      if (currentRoute && typeof currentRoute.params === 'object') {
        for (const key in currentRoute.params) {
          if (!(key in location.params) && paramNames.indexOf(key) > -1) {
            location.params[key] = currentRoute.params[key]
          }
        }
      }

      if (record) {
        location.path = fillParams(record.path, location.params, `named route "${name}"`)
        return _createRoute(record, location, redirectedFrom)
      }
    } else if (location.path) {
      location.params = {}
      for (let i = 0; i < pathList.length; i++) {
        const path = pathList[i]
        const record = pathMap[path]
        // Route matching based on current path
        // Create a routing object if it matches;
        if (matchRoute(record.regex, location.path, location.params)) {
          return _createRoute(record, location, redirectedFrom)
        }
      }
    }
    // no match
    return _createRoute(null, location)
  }
  
  // ...

  function _createRoute (record, location, redirectedFrom) {
    // Create routing objects according to different conditions;
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    return createRoute(record, location, redirectedFrom, router)
  }

  return {
    match,
    addRoutes
  }
}

function matchRoute (regex, path, params) {
  const m = path.match(regex)

  if (!m) {
    return false
  } else if (!params) {
    return true
  }

  for (let i = 1, len = m.length; i < len; ++i) {
    const key = regex.keys[i - 1]
    const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i]
    if (key) {
      params[key.name] = val
    }
  }

  return true
}

function resolveRecordPath (path, record) {
  return resolvePath(path, record.parent ? record.parent.path : '/', true)
}

First createMatcher generates a map with a corresponding relationship based on the routes configuration defined when we initialize the VueRouter instance. The logic is described below.Then it returns an object match that contains two methods: match and addRoutes, which is the detailed logic we implement for routing matching. It returns the matching routing object; addRoutes is the method for adding routes.

Next, let's go to create-route-map.js

Source location: /src/create-route-map.js

/* @flow */

import Regexp from 'path-to-regexp'
import { cleanPath } from './util/path'
import { assert, warn } from './util/warn'

export function createRouteMap (routes, oldPathList, oldPathMap, oldNameMap) {
  // the path list is used to control path matching priority
  const pathList = oldPathList || []
  // $flow-disable-line
  const pathMap = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap = oldNameMap || Object.create(null)
  // path list
  // map mapping of path
  // map mapping of name
  // Increase routing records for configured routing items
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })

  // ensure wildcard routes are always at the end
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }
  // Returns an object containing a path array, a path map, and a name map;
  return {
    pathList,
    pathMap,
    nameMap
  }
}

function addRouteRecord (pathList, pathMap, nameMap, route, parent, matchAs) {
  const { path, name } = route
  if (process.env.NODE_ENV !== 'production') {
    assert(path != null, `"path" is required in a route configuration.`)
    assert(
      typeof route.component !== 'string',
      `route config "component" for path: ${String(path || name)} cannot be a ` +
      `string id. Use an actual component instead.`
    )
  }

  // Define options for path s to Reg s;
  const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
  // Serialize path,'/'will be replaced with'';
  const normalizedPath = normalizePath(
    path,
    parent,
    pathToRegexpOptions.strict
  )

  // Is regular matching case sensitive?
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }

  const record = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    instances: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props: route.props == null
      ? {}
      : route.components
        ? route.props
        : { default: route.props }
  }
  // If there are nested subroutes, route records are added recursively;
  if (route.children) {
    // Warn if route is named, does not redirect and has a default child route.
    // If users navigate to this route by name, the default child will
    // not be rendered (GH Issue #629)
    if (process.env.NODE_ENV !== 'production') {
      if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
        warn(
          false,
          `Named Route '${route.name}' has a default child route. ` +
          `When navigating to this named route (:to="{name: '${route.name}'"), ` +
          `the default child route will not be rendered. Remove the name from ` +
          `this route and use the name of the default child route for named ` +
          `links instead.`
        )
      }
    }
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  // Add an alias routing record to a route if it contains an alias
  // About alias
  // https://router.vuejs.org/zh-cn/essentials/redirect-and-alias.html
  if (route.alias !== undefined) {
    const aliases = Array.isArray(route.alias)
      ? route.alias
      : [route.alias]

    aliases.forEach(alias => {
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      )
    })
  }

  // Update path map
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }
  // Update name map for routes with name defined
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        `Duplicate named routes definition: ` +
        `{ name: "${name}", path: "${record.path}" }`
      )
    }
  }
}

function compileRouteRegex (path, pathToRegexpOptions) {
  const regex = Regexp(path, [], pathToRegexpOptions)
  if (process.env.NODE_ENV !== 'production') {
    const keys: any = Object.create(null)
    regex.keys.forEach(key => {
      warn(!keys[key.name], `Duplicate param keys in route with path: "${path}"`)
      keys[key.name] = true
    })
  }
  return regex
}

function normalizePath (path, parent, strict): string {
  if (!strict) path = path.replace(/\/$/, '')
  if (path[0] === '/') return path
  if (parent == null) return path
  return cleanPath(`${parent.path}/${path}`)
}

From the code above, you can see that create-route-map.js generates route records based on the path, alias, and name configured by the user's routes.

Create history

This part of matcher is finished, let's talk about the instantiation of History. From the source code, there are four files under the history folder. Base is the base class, and the other three inherit this base class to handle the various mode s of vue-router respectively. Let's just look at the logic of base.

// install Vues everywhere to avoid adding volume when Vue is packaged into a project;
import { START, isSameRoute } from '../util/route'

export class History {
  constructor (router, base) {
    this.router = router
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    // Generate a basic route object;
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
  }
  // ...
}
// ...
function normalizeBase (base: ?string): string {
  if (!base) {
    if (inBrowser) {
      // respect <base> tag
      const baseEl = document.querySelector('base')
      base = (baseEl && baseEl.getAttribute('href')) || '/'
      // strip full URL origin
      base = base.replace(/^https?:\/\/[^\/]+/, '')
    } else {
      base = '/'
    }
  }
  // make sure there's the starting slash
  if (base.charAt(0) !== '/') {
    base = '/' + base
  }
  // remove trailing slash
  return base.replace(/\/$/, '')
}

Once you've finished the basic mounting and various instantiations, we can start with init to see the following process.

Previously, when I talked about install, I learned that init was executed in the beforeCreate hook in mixin. Now let's move to the init method of VueRouter.

Source location: /src/index.js

// ...
init (app) {
    process.env.NODE_ENV !== 'production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )
    // From the call in install, we know that this app is the vVue instance we instantiated;
    this.apps.push(app)

    // main app already initialized.
    if (this.app) {
      return
    }
    // Point the app in VueRouter to our instance of Vue;
    this.app = app

    const history = this.history
    // For special handling of HTML5History and HashHistory,
    // Because in both modes it is possible that the entry time is not the default page.
    // You need to activate the corresponding route based on the path or hash in the current browser address bar
    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }
    //...
  }
// ...

You can see that initialization is mainly about assigning app s, and special handling is done for HTML5History and HashHistory, since it is possible that there will be incoming pages that are not default pages and that the corresponding routes need to be activated based on the path or hash in the current browser's address bar, in which case t is calledRansitionTo achieve the goal;

Next, let's look at this specific transitionTo:

Source location: /src/history/base.js

transitionTo (location, onComplete, onAbort) {
    // Localization is the route to our current page;
    // Call the match method of VueRouter to get the matching routing object and create the next state routing object.
    // this.current is the routing object we save in its current state;
    const route = this.router.match(location, this.current)
    this.confirmTransition(route, () => {
      // Update the current route object;
      this.updateRoute(route)
      onComplete && onComplete(route)
      // Calling a method of a subclass to update the url
      this.ensureURL()
      // fire ready cbs once
      // Callback function of read after success;
      if (!this.ready) {
        this.ready = true
        this.readyCbs.forEach(cb => { cb(route) })
      }
    }, err => {
      if (onAbort) {
        onAbort(err)
      }
      // Failed err callback function called;
      if (err && !this.ready) {
        this.ready = true
        this.readyErrorCbs.forEach(cb => { cb(err) })
      }
    })
  }
  confirmTransition (route, onComplete, onAbort) {
    const current = this.current
    const abort = err => {
      if (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)
    }
    // Do not jump if it is the same route;
    if (
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      route.matched.length === current.matched.length
    ) {
      // Calling a method of a subclass to update the url
      this.ensureURL()
      return abort()
    }
    // Cross-matching the route record of the current route with that of the current route
    // In order to know exactly when parent-child routing updates are available
    // Which components need to be updated and which do not need to be updated
    const {
      updated,
      deactivated,
      activated
    } = resolveQueue(this.current.matched, route.matched)
    // Note that matched stores an array of routing records;

    // //Queue for entire switching cycle, various hook update queues to be performed
    const queue: Array<?NavigationGuard> = [].concat(
      // in-component leave guards
      // beforeRouteLeave hook for extracting components
      extractLeaveGuards(deactivated),
      // global before hooks
      this.router.beforeHooks,
      // in-component update hooks
      // beforeRouteUpdate hook for extracting components
      extractUpdateHooks(updated),
      // in-config enter guards
      activated.map(m => m.beforeEnter),
      // async components
      // Asynchronous Processing Components
      resolveAsyncComponents(activated)
    )
    // Save route in next state
    this.pending = route
    // iterator function executed by each queue
    const iterator = (hook: NavigationGuard, next) => {
      if (this.pending !== route) {
        return abort()
      }
      try {
        hook(route, current, (to: any) => {
          if (to === false || isError(to)) {
            // next(false) -> abort navigation, ensure current URL
            this.ensureURL(true)
            abort(to)
          } else if (
            typeof to === 'string' ||
            (typeof to === 'object' && (
              typeof to.path === 'string' ||
              typeof to.name === 'string'
            ))
          ) {
            // next('/') or next({ path: '/' }) -> redirect
            abort()
            if (typeof to === 'object' && to.replace) {
              this.replace(to)
            } else {
              this.push(to)
            }
          } else {
            // confirm transition and pass on the value
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }
    // Execute various hook queues
    runQueue(queue, iterator, () => {
      const postEnterCbs = []
      const isValid = () => this.current === route
      // wait until async components are resolved before
      // extracting in-component enter guards
      // Execute hooks within components while waiting for asynchronous component OK
      const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
      const queue = enterGuards.concat(this.router.resolveHooks)
      // Execute hooks within components after last queue execution is complete
      // Because you need to wait for an asynchronous component and be OK to execute
      runQueue(queue, iterator, () => {
        if (this.pending !== route) {
          return abort()
        }
        // Routing transition complete
        this.pending = null
        onComplete(route)
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            postEnterCbs.forEach(cb => { cb() })
          })
        }
      })
    })
  }
  updateRoute (route) {
    const prev = this.current
    // Point the current to our updated route object;
    this.current = route
    this.cb && this.cb(route)
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }

The logic may seem complex, but it is actually a round-trip of various hook functions, but here it is important to note that each route route object has a matchd property, which contains a route record, the generation of which has been mentioned in create-matcher.js.

Wait a moment. We seem to have missed something. There's nothing left behind init:

Source location: /src/index.js

// Set up monitoring when the route changes;
history.listen(route => {
    this.apps.forEach((app) => {
        app._route = route
    })
})

Setting the callback function after the route change here calls in the onComplete callback in the confirmTransition and updates the current value of _route, as we mentioned earlier, _route is responsive, so when it updates it notifies the component to render again.

Two components

Now that you have finished the general process, let's look at the two components. Let's first look at the RouterView component:
Source location: /src/components/view.js

import { warn } from '../util/warn'

export default {
  name: 'RouterView',
  functional: true,
  props: {
    // Attempt name, default
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    data.routerView = true
    // directly use parent context's createElement() function
    // so that components rendered by router-view can resolve named slots
    // renderer
    const h = parent.$createElement
    const name = props.name
    // Get the _route object and the cache object;
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    // determine current view depth, also check to see if the tree
    // has been toggled inactive but kept-alive.
    // Component Level
    // Terminate loop when _routerRoot points to Vue instance
    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      // Processing keep-alive components
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth

    // render previous view if the tree is inactive and kept-alive
    // Render Cached keep-alive Component
    if (inactive) {
      return h(cache[name], data, children)
    }
    const matched = route.matched[depth]
    // render empty node if no matched route
    if (!matched) {
      cache[name] = null
      return h()
    }
    const component = cache[name] = matched.components[name]
    // attach instance registration hook
    // this will be called in the instance's injected lifecycle hooks
    // Add a registered hook, which is injected into the component's life cycle hook
    // In src/install.js, called in beforeCreate hook
    data.registerRouteInstance = (vm, val) => {
      // val could be undefined for unregistration
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }
    // also register instance in prepatch hook
    // in case the same component instance is reused across different routes
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }
    // resolve props
    let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
    if (propsToPass) {
      // clone to prevent mutation
      propsToPass = data.props = extend({}, propsToPass)
      // pass non-declared props as attrs
      const attrs = data.attrs = data.attrs || {}
      for (const key in propsToPass) {
        if (!component.props || !(key in component.props)) {
          attrs[key] = propsToPass[key]
          delete propsToPass[key]
        }
      }
    }

    return h(component, data, children)
  }
}

function resolveProps (route, config) {
  switch (typeof config) {
    case 'undefined':
      return
    case 'object':
      return config
    case 'function':
      return config(route)
    case 'boolean':
      return config ? route.params : undefined
    default:
      if (process.env.NODE_ENV !== 'production') {
        warn(
          false,
          `props in "${route.path}" is a ${typeof config}, ` +
          `expecting an object, function or boolean.`
        )
      }
  }
}

function extend (to, from) {
  for (const key in from) {
    to[key] = from[key]
  }
  return to
}

Then the RouterLink component:

Source location: /src/components/link.js

/* @flow */

import { createRoute, isSameRoute, isIncludedRoute } from '../util/route'
import { _Vue } from '../install'

// work around weird flow bug
const toTypes: Array<Function> = [String, Object]
const eventTypes: Array<Function> = [String, Array]

export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
    // Get mounted VueRouter instances
    const router = this.$router
    // Get the current routing object
    const current = this.$route
    // Get current matching routing information
    const { location, route, href } = router.resolve(this.to, current, this.append)

    const classes = {}
    const globalActiveClass = router.options.linkActiveClass
    const globalExactActiveClass = router.options.linkExactActiveClass
    // Support global empty active class
    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
    const compareTarget = location.path
      ? createRoute(null, location, null, router)
      : route

    classes[exactActiveClass] = isSameRoute(current, compareTarget)
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget)

    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location)
        } else {
          router.push(location)
        }
      }
    }
    
    // Event Binding
    const on = { click: guardEvent }
    if (Array.isArray(this.event)) {
      this.event.forEach(e => { on[e] = handler })
    } else {
      on[this.event] = handler
    }

    const data: any = {
      class: classes
    }

    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // find the first <a> child and apply listener and href
      // Find the first <a>event binding and href attribute given to this element
      const a = findAnchor(this.$slots.default)
      if (a) {
        // in case the <a> is a static node
        a.isStatic = false
        const extend = _Vue.util.extend
        const aData = a.data = extend({}, a.data)
        aData.on = on
        const aAttrs = a.data.attrs = extend({}, a.data.attrs)
        aAttrs.href = href
      } else {
        // doesn't have <a> child, apply listener to self
        // Bind events to the current element itself without <a>
        data.on = on
      }
    }

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

function guardEvent (e) {
  // don't redirect with control keys
  if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
  // don't redirect when preventDefault called
  if (e.defaultPrevented) return
  // don't redirect on right click
  if (e.button !== undefined && e.button !== 0) return
  // don't redirect if `target="_blank"`
  if (e.currentTarget && e.currentTarget.getAttribute) {
    const target = e.currentTarget.getAttribute('target')
    if (/\b_blank\b/i.test(target)) return
  }
  // this may be a Weex event which doesn't have this method
  if (e.preventDefault) {
    e.preventDefault()
  }
  return true
}

function findAnchor (children) {
  if (children) {
    let child
    for (let i = 0; i < children.length; i++) {
      child = children[i]
      if (child.tag === 'a') {
        return child
      }
      if (child.children && (child = findAnchor(child.children))) {
        return child
      }
    }
  }
}

epilogue

At this point, the source analysis of vue-router has come to an end. Although we did not understand the author's ideas line by line, it is still an overall smoothing of the operation principle of the project, and understanding the principle is more convenient for us to develop our daily needs.Finally, thank you all for enjoying it.

Posted by ElkySS on Fri, 30 Aug 2019 20:04:32 -0700