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.