Analysis of the whole process of vue-router source code

Analysis of the whole process of vue-router source code

Description

  • The following are relying vue-router 2.0.0version to analyze.
  • This article, was their vue-router, if there is any error log analysis to achieve articles, please correct me, communicate with each other.

Basic use

import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const Home = { template: '<div>home</div>' };
const Foo = { template: '<div>foo</div>' };
const Bar = { template: '<div>bar</div>' };
const Child = { template: '<div>Child</div>' };

const router = new VueRouter({
  mode: 'history',
  //base: __dirname,
  base: '/', //  /
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
    {
      path: '/bar',
      component: Bar,
      children: [{ path: 'child', component: Child }]
    }
  ]
});

const template = `
<div id="app">
  <h1>Basic</h1>
  <ul>
    <li><router-link to="/">/</router-link></li>
    <li><router-link to="/foo">/foo</router-link></li>
    <li><router-link to="/bar">/bar</router-link></li>
    <li><router-link to="/bar/child">/bar</router-link></li>
  </ul>
  <router-view class="view"></router-view>
</div>
`;

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

Based on the above basic usage, we can probably sort out a basic process:

  • Register the plugin:
    • Will $routerand $routeinjected into sub-components all routing enabled.
    • Installation <router-view>and <router-link>.
  • Define routing components.
  • new VueRouter(options) Create a router and pass in the relevant configuration.
  • Create and mount the root instance, make sure to inject the router. Routing component in the <router-view>presentation.

Below we will analyze the vue-routercode step by step according to the above process steps .

Register the plugin (vue-router)

First Vue.use(VueRouter);code execution indirect VueRouterexposure installmethod, let's look at installthe specific implementation :( Because integrity of the Notes, it omitted text description)

install

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

/**
 *   Vue.js   install   Vue  
 *
 * @export
 * @param {*} Vue
 * @returns
 *
 */
export function install(Vue) {
  //  -   install  
  if (install.installed) return;
  install.installed = true;

  //  Vue   $router  ( VueRouter )  this.$root._router
  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this.$root._router;
    }
  });

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

  //  Vue  
  Vue.mixin({
    /**
     *   Vue  
     * 1.  Vue   init  
     * 2.  Vue   _router   VueRouter  
     * 3.  init   Vue  
     * 4.  ($route <=> _route)  
     */
    beforeCreate() {
      if (this.$options.router) {
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      }
    }
  });

  // 
  Vue.component('router-view', View);
  Vue.component('router-link', Link);
}
 

Above it is vue-routerexposed to the Vueregistration process. Here is a special note: defineReactive Vuethe core method of constructing responsive. In two studies registered global components: <router-view>and <router-link>before, we first discuss the VueRouterconstructor, because of them involve VueRoutera lot of methods.

Code is then performed, the next step is to vue-routerconfigure and instantiate loaded. Then pass in the result of the instantiation Vue.

const router = new VueRouter({
  // 
  mode: 'history',
  // : "/"  /app/  base   "/app/".
  base: '/', //  /
  // 
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
    {
      path: '/bar',
      component: Bar,
      children: [{ path: 'child', component: Child }]
    }
  ]
});

new Vue({
  router
}).$mount('#app');
 

Next, let's take a look at the VueRouter constructor that defines the route.

Define routing: VueRouter constructor

/* @flow */

import { install } from './install';
import { createMatcher } from './create-matcher';
import { HashHistory } from './history/hash';
import { HTML5History } from './history/html5';
import { AbstractHistory } from './history/abstract';
import { inBrowser, supportsHistory } from './util/dom';
import { assert } from './util/warn';

export default class VueRouter {
  static install: () => void;

  app: any; //Vue  
  options: RouterOptions; // 
  mode: string; //  hash
  history: HashHistory | HTML5History | AbstractHistory;
  match: Matcher; // 
  fallback: boolean; //  history.pushState   hash   true 
  beforeHooks: Array<?NavigationGuard>; // 
  afterHooks: Array<?(to: Route, from: Route) => any>; // 

  constructor(options: RouterOptions = {}) {
    this.app = null;
    this.options = options;
    this.beforeHooks = [];
    this.afterHooks = [];
    this.match = createMatcher(options.routes || []); // 

    /*******   -   hash *******/
    let mode = options.mode || 'hash';
    //   history    history   hash  
    this.fallback = mode === 'history' && !supportsHistory;
    if (this.fallback) {
      mode = 'hash';
    }
    //  abstract  
    if (!inBrowser) {
      mode = 'abstract';
    }
    this.mode = mode;
  }

  /**
   *  
   *
   * @readonly
   * @type {?Route}
   * @memberof VueRouter
   */
  get currentRoute(): ?Route {
    return this.history && this.history.current;
  }

  /**
   *  
   * @param {Any} app Vue component instance
   */
  init(app: any) {
    // 
    assert(
      install.installed,
      `  Vue.use(VueRouter) `
    );

    this.app = app;
    const { mode, options, fallback } = this;
    // 
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base);
        break;
      case 'hash':
        this.history = new HashHistory(this, options.base, fallback);
        break;
      case 'abstract':
        this.history = new AbstractHistory(this);
        break;
      default:
        assert(false, `invalid mode: ${mode}`);
    }

    //  history   listen  
    this.history.listen(route => {
      this.app._route = route;
    });
  }

  /**
   * Router   beforeEach  
   *  
   *   resolve    
   *
   * @param {Function} fn (to, from, next) => {}
   * @memberof VueRouter
   *
   */
  beforeEach(fn: Function) {
    this.beforeHooks.push(fn);
  }

  /**
   * Router   afterEach  
   *
   * @param {Function} fn (to, from) => {}
   * @memberof VueRouter
   */
  afterEach(fn: Function) {
    this.afterHooks.push(fn);
  }

  /**
   *   push   location
   *   history  
   *   location 
   *
   * @param {RawLocation} location
   * @memberof VueRouter
   */
  push(location: RawLocation) {
    this.history.push(location);
  }

  /**
   *   replace   location
   *   history       history  
   *
   * @param {RawLocation} location
   * @memberof VueRouter
   */
  replace(location: RawLocation) {
    this.history.replace(location);
  }

  /**
   *   history   window.history.go(n) 
   *
   * @param {number} n
   * @memberof VueRouter
   */
  go(n: number) {
    this.history.go(n);
  }

  /**
   *  
   *
   * @memberof VueRouter
   */
  back() {
    this.go(-1);
  }

  /**
   *  
   *
   * @memberof VueRouter
   */
  forward() {
    this.go(1);
  }

  /**
   *  
   *
   * @returns {Array<any>}
   * @memberof VueRouter
   */
  getMatchedComponents(): Array<any> {
    if (!this.currentRoute) {
      return [];
    }
    return [].concat.apply(
      [],
      this.currentRoute.matched.map(m => {
        return Object.keys(m.components).map(key => {
          return m.components[key];
        });
      })
    );
  }
}

//  install  
VueRouter.install = install;

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

Analysis realized by the code, the first statement of some of the properties, methods, and common API; followed by the addition installmethod and performing Vue.usea method of registering plug.

There are detailed comments on the relevant code, and there is no need to repeat the discussion, but some of the methods will be analyzed in depth when involved later. Before continuing the analysis, here is the need to explain this code:

this.match = createMatcher(options.routes || []); // 
 
/**
 *  
 *
 * @export
 * @param {Array<RouteConfig>} routes
 * @returns {Matcher}
 */
export function createMatcher(routes: Array<RouteConfig>): Matcher {
  const { pathMap, nameMap } = createRouteMap(routes);

  function match(
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    ...
    return _createRoute(null, location);
  }

  function redirect(record: RouteRecord, location: Location): Route {
    ...
  }

  function alias(
    record: RouteRecord,
    location: Location,
    matchAs: string
  ): Route {
    ...
  }

  function _createRoute(
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
    ...
  }

  return match;
}

 

The above code first creates a new mapping table based on the incoming routing configuration table and deconstructs the path mapping table and name mapping table from it, and then returns to the built-in function match. I will not introduce its internal implementation in detail for the time being, and will be discussed in detail later by the caller.

initialization

We know that in VueRouter - installthe time of registration plug this method, mixed with a global life-cycle function beforeCreate, as follows:

export function install(Vue) {
  ...

  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      }
    }
  });

  ...

}
 

We found that the implementation of the vue-routeroffered initmethod and pass Vuecomponent instance. So then we take a look at initwhat had been done thing.

init A brief analysis of the method


export default class VueRouter {
  static install: () => void;

  app: any; //vue  
  options: RouterOptions; // 
  mode: string; //  hash
  history: HashHistory | HTML5History | AbstractHistory;
  match: Matcher; //   
  fallback: boolean; //    history.pushState   hash   true 
  beforeHooks: Array<?NavigationGuard>; // 
  afterHooks: Array<?(to: Route, from: Route) => any>; // 

  constructor(options: RouterOptions = {}) {
    ...
  }

  ...

  /**
   *  
   * @param {Any} app Vue component instance
   */
  init(app: any) {
    // 
    assert(
      install.installed,
      `  Vue.use(VueRouter) `
    );

    this.app = app;
    const { mode, options, fallback } = this;
    // 
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base);
        break;
      case 'hash':
        this.history = new HashHistory(this, options.base, fallback);
        break;
      case 'abstract':
        this.history = new AbstractHistory(this);
        break;
      default:
        assert(false, `invalid mode: ${mode}`);
    }

    // 
    this.history.listen(route => {
      this.app._route = route;
    });
  }

  ...

}

 

The above code implementation:

  • 1. assert whether the plug-in is installed, and if it does not throw an error message.
  • In VueRouterthe Add Vueinstance.
  • Instantiate different base classes according to different modes, throw an error in invalid mode
  • Execute the parent class monitor function and register the callback:
    • Alternatively when the route change vueon the instance of _routethe currently matched routing object properties
    • The responsive value is changed, which triggers the re-rendering of the view
    • In <router-view>the matching route to get the object, rendering the matching routing component. Complete the jump.

According to the above-described basic example, selected historypattern. Let's take a look at the implementation of this mode first.

HTML5History

/**
 * h5 - history  
 *
 * @export
 * @class HTML5History
 * @extends {History}
 */
export class HTML5History extends History {
  constructor(router: VueRouter, base: ?string) {
    // VueRouter 
    super(router, base);

    //   = 
    this.transitionTo(getLocation(this.base));

    const expectScroll = router.options.scrollBehavior;
    //  popstate  
    window.addEventListener('popstate', e => {
      _key = e.state && e.state.key;
      const current = this.current;
      this.transitionTo(getLocation(this.base), next => {
        if (expectScroll) {
          this.handleScroll(next, current, true);
        }
      });
    });

    //  scroll  
    if (expectScroll) {
      window.addEventListener('scroll', () => {
        saveScrollPosition(_key);
      });
    }
  }

  /**
   *  
   *
   * @param {number} n
   * @memberof HTML5History
   */
  go(n: number) {
    // (   ) 
    window.history.go(n);
  }

  /**
   *   location   history  
   *
   * @param {RawLocation} location
   * @memberof HTML5History
   */
  push(location: RawLocation) {
    // 
    const current = this.current;
    // 
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath));
      this.handleScroll(route, current, false);
    });
  }

  /**
   *   location   history  
   *
   * @param {RawLocation} location
   * @memberof HTML5History
   */
  replace(location: RawLocation) {
    const current = this.current;
    this.transitionTo(location, route => {
      replaceState(cleanPath(this.base + route.fullPath));
      this.handleScroll(route, current, false);
    });
  }

  /**
   *   URL
   *
   * @memberof HTML5History
   */
  ensureURL() {
    if (getLocation(this.base) !== this.current.fullPath) {
      replaceState(cleanPath(this.base + this.current.fullPath));
    }
  }

  /**
   *  
   *
   * @param {Route} to  
   * @param {Route} from  
   * @param {boolean} isPop   popstate   (  /  )  
   * @memberof HTML5History
   */
  handleScroll(to: Route, from: Route, isPop: boolean) {
    const router = this.router;
    //  Vue  return
    if (!router.app) {
      return;
    }

    //        return
    // 
    // html5 ;  ;  false .
    //{ x: number, y: number }
    //{ selector: string, offset? : { x: number, y: number }}
    const behavior = router.options.scrollBehavior;
    if (!behavior) {
      return;
    }
    //   
    assert(typeof behavior === 'function', `scrollBehavior must be a function`);

    // 
    router.app.$nextTick(() => {
      // 
      let position = getScrollPosition(_key);
      // 
      const shouldScroll = behavior(to, from, isPop ? position : null);
      //  return
      if (!shouldScroll) {
        return;
      }
      const isObject = typeof shouldScroll === 'object';
      // 
      if (isObject && typeof shouldScroll.selector === 'string') {
        const el = document.querySelector(shouldScroll.selector);
        if (el) {
          position = getElementPosition(el);
        } else if (isValidPosition(shouldScroll)) {
          position = normalizePosition(shouldScroll);
        }
      } else if (isObject && isValidPosition(shouldScroll)) {
        position = normalizePosition(shouldScroll);
      }

      if (position) {
        // 
        window.scrollTo(position.x, position.y);
      }
    });
  }
}
 

To HTML5Historyachieve Analysis:

  • The initmethod model: historyinstantiates new HTML5History(this, options.base)constructor is called and the incomingVueRouter and the base path applications.
  • Constructor
    • Call the parent class super(router, base)and pass in the VueRouter routing instance and base path;
    • Call the core transition jump method to jump to the base path of the application;
    • Defined popstatelistener function, and accordingly the jump processing in the callback years;
    • If the instantiation of VueRouterincoming scrolling behavior configuration scrollBehavioradd scroll listen for events. The callback is: scroll to the last marked position.
  • Define the corresponding Routerinstance method; and a number of sub-class parent class method implementation calls.

There are very detailed annotation information for the methods it provides, so let's take a look at the implementation of the parent class directly. The other two modes also inherit this base class History.

History


/**
 * History  
 *
 * @export
 * @class History
 */
export class History {
  router: VueRouter;
  base: string;
  current: Route;
  pending: ?Route;
  cb: (r: Route) => void;

  // 
  go: (n: number) => void;
  push: (loc: RawLocation) => void;
  replace: (loc: RawLocation) => void;
  ensureURL: () => void; // URL

  constructor(router: VueRouter, base: ?string) {
    //VueRouter  
    this.router = router
    // 
    this.base = normalizeBase(base)
    //   nowhere    route  
    this.current = START
    // 
    this.pending = null
  }

  /**
   *  
   *
   * @param {Function} cb
   * @memberof History
   */
  listen(cb: Function) {
    this.cb = cb;
  }

  /**
   *  
   *
   * @param {RawLocation} location
   * @param {Function} [cb]
   * @memberof History
   */
  transitionTo(location: RawLocation, cb?: Function) { ... }

  // 
  confirmTransition(route: Route, cb: Function) { ... }

  // 
  updateRoute(route: Route) { ... }
}

/**
 *  
 *
 * @param {?string} base
 * @returns {string}
 */
function normalizeBase(base: ?string): string {
  if (!base) {
    if (inBrowser) {
      //respect <base> tag
      //HTML <base>     URL   URL  <base>  
      const baseEl = document.querySelector('base')
      base = baseEl ? baseEl.getAttribute('href') : '/'
    } else {
      base = '/'
    }
  }
  // 
  if (base.charAt(0) !== '/') {
    base = '/' + base
  }
  // 
  return base.replace(/\/$/, '')
}

 
  • The above is Historyall the code implemented in the base class. The same is to add a few attributes, and some core methods required for jumps. Here you only need to get a rough idea of its internal implementation, which will be discussed in detail later.
  • Back to the HTML5Historyexecution of the code constructor this.transitionTo(getLocation(this.base)), call the central transition Jump method Jump to the application base path.

Core jump method transitionTo

Call ( HTML5History - this.transitionTo(getLocation(this.base));) during initialization according to the above example , the input parameters here are:location:/, cb: undefined


/**
  *  
  *
  * @param {RawLocation} location  
  * @param {Function} [cb]  
  * @memberof History
  */
transitionTo(location: RawLocation, cb?: Function) {
  //  location   current 
  const route = this.router.match(location, this.current)
  // 
  // ,   URL
  this.confirmTransition(route, () => {
    this.updateRoute(route)
    cb && cb(route)
    this.ensureURL()
  })
}

 

According to the above code to achieve a brief analysis:

  • Incoming addresses and currentproperties, currentproperties in calling superin the parent class is initially assignedSTART

    //vue-router/src/history/base.js
    //   nowhere    route  
    this.current = START;
     
    START code implementation and result display

    START

    // 
    export const START = createRoute(null, {
      path: '/'
    });
    
    /**
     *  
     *
     * @export
     * @param {?RouteRecord} record
     * @param {Location} location
     * @param {Location} [redirectedFrom]
     * @returns {Route}
     */
    export function createRoute(
      record: ?RouteRecord,
      location: Location,
      redirectedFrom?: Location
    ): Route {
      const route: Route = {
        name: location.name || (record && record.name),
        meta: (record && record.meta) || {},
        path: location.path || '/',
        hash: location.hash || '',
        query: location.query || {},
        params: location.params || {},
        fullPath: getFullPath(location),
        matched: record ? formatMatch(record) : []
      };
      // 
      if (redirectedFrom) {
        route.redirectedFrom = getFullPath(redirectedFrom);
      }
      return Object.freeze(route);
    }
    
    /**
     *  
     *
     * @param {?RouteRecord} record
     * @returns {Array<RouteRecord>}
     */
    function formatMatch(record: ?RouteRecord): Array<RouteRecord> {
      const res = [];
      while (record) {
        res.unshift(record);
        record = record.parent;
      }
      return res;
    }
    
    /**
     *  
     *
     * @param {*} { path, query = {}, hash = '' }
     * @returns
     */
    function getFullPath({ path, query = {}, hash = '' }) {
      return (path || '/') + stringifyQuery(query) + hash;
    }
     

    START results are as follows:

      START = {
        fullPath: "/",
        hash: "",
        matched: [],
        meta: {},
        name: null,
        params: {},
        path: "/",
        query: {},
        __proto__: Object,
        ,
      }
     
  • First call the VueRouterclass matchattribute initialization VueRouteris assigned a method.

    //vue-router/src/index.js
    this.match = createMatcher(options.routes || []);
     
    Code implementation of createMatcher to generate match

    createMatcher achieve

    • First call createRouteMap -routing map, deconstruction of the path, name mapping table
    • Built-in methods defined match redirect _createRoutefinal returnmatch
    /**
      *  
      *
      * @export
      * @param {Array<RouteConfig>} routes
      * @returns {Matcher}
      */
      export function createMatcher(routes: Array<RouteConfig>): Matcher {
    
        const { pathMap, nameMap } = createRouteMap(routes);
    
        // 
        function match(
          raw: RawLocation,
          currentRoute?: Route,
          redirectedFrom?: Location
        ): Route {
          ...
          return _createRoute(null, location);
        }
    
        // 
        function redirect(record: RouteRecord, location: Location): Route {
          ...
        }
    
        // 
        function alias(
          record: RouteRecord,
          location: Location,
          matchAs: string
        ): Route {
          ...
        }
    
        // 
        function _createRoute(
          record: ?RouteRecord,
          location: Location,
          redirectedFrom?: Location
        ): Route {
          ...
        }
    
        return match;
      }
    
     

    createRouteMap

    • First create name and path mapping objects
    • Process each item in the routing table
    • Finally returns the mapping table containing the path/name
    /**
     *  
     *
     * @export
     * @param {Array<RouteConfig>} routes
     * @returns {{
     *   pathMap: Dictionary<RouteRecord>,
     *   nameMap: Dictionary<RouteRecord>
     * }}
     */
    export function createRouteMap(
      routes: Array<RouteConfig>
    ): {
      pathMap: Dictionary<RouteRecord>,
      nameMap: Dictionary<RouteRecord>
    } {
      const pathMap: Dictionary<RouteRecord> = Object.create(null);
      const nameMap: Dictionary<RouteRecord> = Object.create(null);
    
      // 
      routes.forEach(route => {
        addRouteRecord(pathMap, nameMap, route);
      });
    
      return {
        pathMap,
        nameMap
      };
    }
     

    addRouteRecord

    /**
     *  
     *
     * @param {Dictionary<RouteRecord>} pathMap  
     * @param {Dictionary<RouteRecord>} nameMap  
     * @param {RouteConfig} route  
     * @param {RouteRecord} [parent]  
     * @param {string} [matchAs]
     */
    function addRouteRecord(
      pathMap: Dictionary<RouteRecord>,
      nameMap: Dictionary<RouteRecord>,
      route: RouteConfig,
      parent?: RouteRecord,
      matchAs?: string
    ) {
      // 
      const { path, name } = route;
      assert(path != null, ` path `);
    
      // 
      const record: RouteRecord = {
        path: normalizePath(path, parent), // 
        components: route.components || { default: route.component }, // 
        instances: {},
        name, // 
        parent, // 
        matchAs,
        redirect: route.redirect, // 
        beforeEnter: route.beforeEnter, // (to: Route, from: Route, next: Function) => void;
        meta: route.meta || {} // 
      };
    
      // 
      if (route.children) {
        // 
        // (GH #629) 
        if (process.env.NODE_ENV !== 'production') {
          if (
            route.name &&
            route.children.some(child =>/^\/?$/.test(child.path)) // 
          ) {
            warn(
              false,
              ` '${route.name}' 
                 (:to="{name: '${
                  route.name
                }'") 
                 `
            );
          }
        }
        // 
        route.children.forEach(child => {
          addRouteRecord(pathMap, nameMap, child, record);
        });
      }
    
      //  string | Array<string>
      if (route.alias) {
        // 
        if (Array.isArray(route.alias)) {
          // 
          route.alias.forEach(alias => {
            addRouteRecord(
              pathMap,
              nameMap,
              { path: alias },
              parent,
              record.path
            );
          });
        } else {
          addRouteRecord(
            pathMap,
            nameMap,
            { path: route.alias },
            parent,
            record.path
          );
        }
      }
    
      // 
      pathMap[record.path] = record;
      if (name) nameMap[name] = record;
    }
    
    /**
     *  
     *
     * @param {string} path
     * @param {RouteRecord} [parent]
     * @returns {string}
     */
    function normalizePath(path: string, parent?: RouteRecord): string {
      path = path.replace(/\/$/, ''); // '/' => ''  '/foo/' => '/foo'
      if (path[0] === '/') return path;
      if (parent == null) return path;
      return cleanPath(`${parent.path}/${path}`); //  '//' => '/'  'router//foo//' => 'router/foo/'
    }
     
    • The above code mainly processes each item of the routing configuration, and finally writes the corresponding path and name mapping table.

    • The above basic sample code routing configuration items are processed as follows:

        pathMap {
          '': {
            beforeEnter: undefined,
            components: {
              default: { template: "<div>home</div>" },
              __proto__: Object
            },
            instances: {},
            matchAs: undefined,
            meta: {},
            name: undefined,
            parent: undefined,
            path: "",
            redirect: undefined,
            __proto__: Object,
          },
          '/bar': {
            beforeEnter: undefined,
            components: {
              default: {template: "<div>bar</div>"},
              __proto__: Object
            },
            instances: {},
            matchAs: undefined,
            meta: {},
            name: undefined,
            parent: undefined,
            path: "/bar",
            redirect: undefined,
            __proto__: Object
          },
          '/bar/child': {
            beforeEnter: undefined,
            components: {
              default: {template: "<div>Child</div>"},
              __proto__: Object
            },
            instances: {},
            matchAs: undefined,
            meta: {},
            name: undefined,
            parent: {path: "/bar", ... },
            path: "/bar/child",
            redirect: undefined,
            __proto__: Object
          },
          '/foo': {
            beforeEnter: undefined,
            components: {
              default: {template: "<div>foo</div>"},
              __proto__: Object
            },
            instances: {},
            matchAs: undefined,
            meta: {},
            name: undefined,
            parent: undefined,
            path: "/foo",
            redirect: undefined,
            __proto__: Object
          }
        }
      
        nameMap {}
      
       

    Know const { pathMap, nameMap } = createRouteMap(routes);and its results deconstruction, we continue to look at matchthe code implementation

    function match(
      raw: RawLocation,
      currentRoute?: Route,
      redirectedFrom?: Location
    ): Route {
      const location = normalizeLocation(raw, currentRoute);
      const { name } = location;
    
      if (name) {
        // 
        const record = nameMap[name];
        if (record) {
          // 
          location.path = fillParams(
            record.path,
            location.params,
            `named route "${name}"`
          );
          return _createRoute(record, location, redirectedFrom);
        }
      } else if (location.path) {
        location.params = {};
        for (const path in pathMap) {
          if (matchRoute(path, location.params, location.path)) {
            return _createRoute(pathMap[path], location, redirectedFrom);
          }
        }
      }
      //  null 
      return _createRoute(null, location);
    }
     
    • Normalize the link of the target route

      normalizeLocation code implementation

      normalizeLocation

      /**
       *  
       *
       * @export
       * @param {RawLocation} raw  
       * @param {Route} [current]  
       * @param {boolean} [append]   ( )  
       * @returns {Location}
       */
      export function normalizeLocation(
        raw: RawLocation,
        current?: Route,
        append?: boolean
      ): Location {
        // to 
        //'home'
        //{ path: 'home' }
        //{ path: `/user/${userId}` }
        //{ name: 'user', params: { userId: 123 }}
        //{ path: 'register', query: { plan: 'private' }}
        const next: Location = typeof raw === 'string' ? { path: raw } : raw;
        // name  next
        if (next.name || next._normalized) {
          return next;
        }
        //    { path, query, hash }
        const parsedPath = parsePath(next.path || '');
        //current.path -  
        const basePath = (current && current.path) || '/';
        // 
        const path = parsedPath.path
          ? resolvePath(parsedPath.path, basePath, append)
          : (current && current.path) || '/';
        // 
        const query = resolveQuery(parsedPath.query, next.query);
        //  hash   (  #)   hash  
        let hash = next.hash || parsedPath.hash;
        if (hash && hash.charAt(0) !== '#') {
          hash = `#${hash}`;
        }
      
        return {
          _normalized: true,
          path,
          query,
          hash
        };
      }
       
      • normalizeLocation Code implementation of the function call involved

        parsePath code implementation

        Resolve path

        /**
         *  
         *
         * @export
         * @param {string} path
         * @returns {{
         *   path: string;
         *   query: string;
         *   hash: string;
         * }}
         */
        export function parsePath(
          path: string
        ): {
          path: string,
          query: string,
          hash: string
        } {
          let hash = '';
          let query = '';
        
          //  #
          const hashIndex = path.indexOf('#');
          if (hashIndex >= 0) {
            hash = path.slice(hashIndex); //  hash  
            path = path.slice(0, hashIndex); // 
          }
        
          // 
          const queryIndex = path.indexOf('?');
          if (queryIndex >= 0) {
            query = path.slice(queryIndex + 1); // 
            path = path.slice(0, queryIndex); // 
          }
        
          return {
            path,
            query,
            hash
          };
        }
         
        resolvePath code implementation

        Path address after export processing

        /**
         *  
         *
         * @export
         * @param {string} relative  
         * @param {string} base  
         * @param {boolean} [append]   ( )  
         * @returns {string}
         */
        export function resolvePath(
          relative: string,
          base: string,
          append?: boolean
        ): string {
          if (relative.charAt(0) === '/') {
            return relative;
          }
        
          if (relative.charAt(0) === '?' || relative.charAt(0) === '#') {
            return base + relative;
          }
        
          //'/vue-router/releases' => ["", "vue-router", "releases"]
          const stack = base.split('/');
        
          // 
          //-  
          //-  ( )
          if (!append || !stack[stack.length - 1]) {
            stack.pop();
          }
        
          //resolve  
          //'/vue-router/releases'.replace(/^\//, '') => "vue-router/releases"
          //'vue-router/releases'.split('/') => ["vue-router", "releases"]
          const segments = relative.replace(/^\//, '').split('/');
          for (let i = 0; i < segments.length; i++) {
            const segment = segments[i];
            if (segment === '.') {
              continue;
            } else if (segment === '..') {
              stack.pop();
            } else {
              stack.push(segment);
            }
          }
        
          //  ensure leading slash
          if (stack[0] !== '') {
            stack.unshift('');
          }
        
          return stack.join('/');
        }
         
        resolveQuery code implementation

        Path address after export processing

        /**
         *  
         *
         * @export
         * @param {?string} query
         * @param {Dictionary<string>} [extraQuery={}]
         * @returns {Dictionary<string>}
         */
        export function resolveQuery(
          query: ?string,
          extraQuery: Dictionary<string> = {}
        ): Dictionary<string> {
          if (query) {
            let parsedQuery;
            try {
              parsedQuery = parseQuery(query);
            } catch (e) {
              warn(false, e.message);
              parsedQuery = {};
            }
            for (const key in extraQuery) {
              parsedQuery[key] = extraQuery[key];
            }
            return parsedQuery;
          } else {
            return extraQuery;
          }
        }
        
        /**
         *  
         *
         * @param {string} query
         * @returns {Dictionary<string>}
         */
        function parseQuery(query: string): Dictionary<string> {
          const res = Object.create(null);
        
          //   # &    '?id=1'.match(/^(\?|#|&)/) => ["?", "?", index: 0, input: "?id=1", groups: undefined]
          //'?id=1&name=cllemon'.replace(/^(\?|#|&)/, '') => id=1&name=cllemon
          query = query.trim().replace(/^(\?|#|&)/, '');
        
          if (!query) {
            return res;
          }
        
          //  => ["id=1", "name=cllemon"]
          query.split('&').forEach(param => {
            //   + 
            // "id=1" => ["id", "1"]
            const parts = param.replace(/\+/g, ' ').split('=');
            // ["id", "1"] => 'id'
            //  decode   decodeURIComponent()   encodeURIComponent  URI 
            const key = decode(parts.shift());
            // ["1"]
            const val = parts.length > 0 ? decode(parts.join('=')) : null;
        
            if (res[key] === undefined) {
              res[key] = val;
            } else if (Array.isArray(res[key])) {
              res[key].push(val);
            } else {
              res[key] = [res[key], val];
            }
          });
        
          return res;
        }
         
    • fillParams Fill parameter

      const regexpCompileCache: {
        [key: string]: Function
      } = Object.create(null);
      
      /**
       *  
       *
       * @param {string} path
       * @param {?Object} params
       * @param {string} routeMsg
       * @returns {string}
       */
      function fillParams(
        path: string,
        params: ?Object,
        routeMsg: string
      ): string {
        try {
          //  path-to-regexp:  `/user/:name` 
          //compile :  
          //  const toPath = Regexp.compile('/user/:id')
          //    toPath({ id: 123 })//=> "/user/123"
          const filler =
            regexpCompileCache[path] ||
            (regexpCompileCache[path] = Regexp.compile(path));
          return filler(params || {}, { pretty: true });
        } catch (e) {
          assert(false, `missing param for ${routeMsg}: ${e.message}`);
          return '';
        }
      }
       
    • _createRoute Call different route creation processing functions according to different configuration item information

      function _createRoute(
        record: ?RouteRecord,
        location: Location,
        redirectedFrom?: Location
      ): Route {
        // 
        if (record && record.redirect) {
          return redirect(record, redirectedFrom || location);
        }
        // 
        if (record && record.matchAs) {
          return alias(record, location, record.matchAs);
        }
        //createRoute    START    
        return createRoute(record, location, redirectedFrom);
      }
       

Final route(by matchthe value returned by the function) is:

route = {
  fullPath: '/',
  hash: '',
  matched: [
    {
      beforeEnter: undefined,
      components: {
        default: {
          template: '<div>home</div>'
        }
      },
      instances: {},
      matchAs: undefined,
      meta: {},
      name: undefined,
      parent: undefined,
      path: '',
      redirect: undefined
    }
  ],
  meta: {},
  name: undefined,
  params: {},
  path: '/',
  query: {},
  __proto__: Object
};
 

Final jump method confirmTransition


/**
 *  
 *
 * @param {Route} route  
 * @param {Function} cb
 * @memberof History
 */
confirmTransition(route: Route, cb: Function) {
  const current = this.current
  // 
  if (isSameRoute(route, current)) {
    this.ensureURL()
    return
  }

  const {
    deactivated,
    activated
  } = resolveQueue(this.current.matched, route.matched)

  // 
  const queue: Array<?NavigationGuard> = [].concat(
    //in-component leave guards
    extractLeaveGuards(deactivated),
    //global before hooks
    this.router.beforeHooks,
    //enter guards beforeEnter: (to, from, next) => {}
    activated.map(m => m.beforeEnter),
    // 
    resolveAsyncComponents(activated)
  )

  this.pending = route
  // 
  const iterator = (hook: NavigationGuard, next) => {
    if (this.pending !== route) return
    //route:     current:   next:   resolve  
    hook(route, current, (to: any) => {
      //to === false    URL   ( )  URL   from  
      if (to === false) {
        //next(false) -> abort navigation, ensure current URL
        this.ensureURL()
      } else if (typeof to === 'string' || typeof to === 'object') {
        //next('/')   next({ path: '/' }):  
        //next('/') or next({ path: '/' }) -> redirect
        this.push(to)
      } else {
        //  confirmed ( ) 
        // 
        next(to)
      }
    })
  }
  // 
  runQueue(queue, iterator, () => {
    const postEnterCbs = []
    //  enter  
    runQueue(extractEnterGuards(activated, postEnterCbs), iterator, () => {
      if (this.pending === route) {
        this.pending = null
        cb(route)
        this.router.app.$nextTick(() => {
          postEnterCbs.forEach(cb => cb())
        })
      }
    })
  })
}

 
  • At this point, the routing core base classes have all been sorted out

  • The above code logic is very clear, there are detailed comments, the text description is skipped here, the function calls involved in the above code :

    Code implementation of resolveQueue
    /**
     *  
     *
     * @param {Array<RouteRecord>} current
     * @param {Array<RouteRecord>} next
     * @returns {{
     *   activated: Array<RouteRecord>,
     *   deactivated: Array<RouteRecord>
     * }}
     */
    function resolveQueue(
      current: Array<RouteRecord>,
      next: Array<RouteRecord>
    ): {
      activated: Array<RouteRecord>,
      deactivated: Array<RouteRecord>
    } {
      let i;
      const max = Math.max(current.length, next.length);
      for (i = 0; i < max; i++) {
        if (current[i] !== next[i]) {
          break;
        }
      }
      return {
        activated: next.slice(i),
        deactivated: current.slice(i)
      };
    }
     
    Code implementation of extractLeaveGuards
    /**
     *  
     *
     * @param {Array<RouteRecord>} matched
     * @returns {Array<?Function>}
     */
    function extractLeaveGuards(matched: Array<RouteRecord>): Array<?Function> {
      // 
      return flatMapComponents(matched, (def, instance) => {
        // 
        const guard = def && def.beforeRouteLeave;
        if (guard) {
          return function routeLeaveGuard() {
            return guard.apply(instance, arguments);
          };
        }
      }).reverse();
    }
     
    Code implementation of resolveAsyncComponents
    /**
     *  
     *
     * @param {Array<RouteRecord>} matched
     * @returns {Array<?Function>}
     */
    function resolveAsyncComponents(
      matched: Array<RouteRecord>
    ): Array<?Function> {
      return flatMapComponents(matched, (def, _, match, key) => {
        //  Vue  
        // 
        //  Vue  
        // 
        if (typeof def === 'function' && !def.options) {
          return (to, from, next) => {
            const resolve = resolvedDef => {
              match.components[key] = resolvedDef;
              next();
            };
    
            const reject = reason => {
              warn(false, `Failed to resolve async component ${key}: ${reason}`);
              next(false);
            };
    
            const res = def(resolve, reject);
            if (res && typeof res.then === 'function') {
              res.then(resolve, reject);
            }
          };
        }
      });
    }
     
    Code implementation of runQueue
    /**
     *  
     *
     * @export
     * @param {Array<?NavigationGuard>} queue
     * @param {Function} fn
     * @param {Function} cb
     */
    export function runQueue(
      queue: Array<?NavigationGuard>,
      fn: Function,
      cb: Function
    ) {
      const step = index => {
        if (index >= queue.length) {
          cb();
        } else {
          if (queue[index]) {
            fn(queue[index], () => {
              step(index + 1);
            });
          } else {
            step(index + 1);
          }
        }
      };
      step(0);
    }
     
    Code implementation of extractEnterGuards
    /**
     *  
     *
     * @param {Array<RouteRecord>} matched
     * @param {Array<Function>} cbs
     * @returns {Array<?Function>}
     */
    function extractEnterGuards(
      matched: Array<RouteRecord>,
      cbs: Array<Function>
    ): Array<?Function> {
      return flatMapComponents(matched, (def, _, match, key) => {
        // 
        // beforeRouteEnter (to, from, next) {}
        //  confirm  ,    `this`,  .
        const guard = def && def.beforeRouteEnter;
        if (guard) {
          return function routeEnterGuard(to, from, next) {
            //  next 
            // next(vm => {//  `vm`   })
            return guard(to, from, cb => {
              next(cb);
              if (typeof cb === 'function') {
                cbs.push(() => {
                  cb(match.instances[key]);
                });
              }
            });
          };
        }
      });
    }
     
/**
  *   location   history  
  *
  * @param {RawLocation} location
  * @memberof HTML5History
  */
  push(location: RawLocation) {
    // 
    const current = this.current
    // 
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      this.handleScroll(route, current, false)
    })
  }
 
  • Finally, we use the jump method of the routing instance ( push) to sort out the process of routing jump:

    • First get the current routing information.

    • Call Historyprovides transitionToa method of transition jump, jump to the incoming path information location(from the above analysis, here is a path information after normalized), as well as a parameter to the callback function routing information (mainly used to update the browser The URL of the browser and the method to handle the scrolling behavior of the user ("analyzed above").

    • transitionToGet on the method for routing information matches the target path, calling confirmTransitionand passing the routing information and a callback function, the callback function is mainly to do: call the updateRouteupdate routing, execution transitionTopassed in the callbacks, and update URL.

    • confirmTransition1. the same route was filtered off; then call resolveQueuea method and pass this information matches the routing information of the target route matching deconstruction deactivated, activated; configuration and execution queue (global hook processing function, the asynchronous component analysis or the like); and then performing a method defined; last execution queue to execute the function (See the above code analysis for specific implementation). In the final execution queue transitionToincoming callback routing update, monitor functions performed: update URL; update vueroot instance _routeattribute, a value change which triggers the re-render the object in the view route to get a match, the matching routing rendering Components. Complete the jump.

Two global components registered: <router-view>and<router-link>

<router-view>

  • <router-view>The component is a functional component, the view component matched by the rendering path. <router-view>The rendered component can also be embedded in its own, <router-view>and the nested component is rendered according to the nesting path.
  • Props: Default: "default"; if <router-view>set name, corresponding routing arrangement will render componentsthe respective components under.
export default {
  name: 'router-view',

  functional: true, //functional  

  props: {
    //  components  
    name: {
      type: String,
      default: 'default'
    }
  },

  render(h, { props, children, parent, data }) {
    data.routerView = true;

    const route = parent.$route;
    const cache = parent._routerViewCache || (parent._routerViewCache = {});
    let depth = 0;
    let inactive = false; // 

    // 
    while (parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++;
      }
      //  keep-alive _inactive vue  
      if (parent._inactive) {
        inactive = true;
      }
      parent = parent.$parent;
    }

    //  router-view  
    data.routerViewDepth = depth;
    // 
    const matched = route.matched[depth];
    // 
    if (!matched) {
      return h();
    }

    // 
    const component = inactive
      ? cache[props.name]
      : (cache[props.name] = matched.components[props.name]);

    //keep-alive  
    if (!inactive) {
      // 
      (data.hook || (data.hook = {})).init = vnode => {
        debugger;
        matched.instances[props.name] = vnode.child;
      };
    }

    return h(component, data, children);
  }
};
 

<router-link>

  • <router-link>The component supports the user to (click) navigation in the application with routing function. By tothe specified property destination address, default rendered with the correct link <a>label, can be configured tagto generate other tag attributes.
  • <router-link>Compared to write dead <a href="...">will be better.
  • See more vue-routerdocumentation.
/* @flow */

import { cleanPath } from '../util/path';
import { createRoute, isSameRoute, isIncludedRoute } from '../util/route';
import { normalizeLocation } from '../util/location';

//  flow bug
const toTypes: Array<Function> = [String, Object];

export default {
  name: 'router-link',
  props: {
    // 
    to: {
      type: toTypes,
      required: true
    },
    // 
    tag: {
      type: String,
      default: 'a'
    },
    // 
    exact: Boolean,
    //  ( )   /a =>/b (true/a/b; false:/b)
    append: Boolean,
    //true:   router.replace()   router.push()   history  
    replace: Boolean,
    //  CSS  
    activeClass: String
  },

  render(h: Function) {
    const router = this.$router;
    const current = this.$route;
    //   
    const to = normalizeLocation(this.to, current, this.append);
    const resolved = router.match(to);
    const fullPath = resolved.redirectedFrom || resolved.fullPath;
    const base = router.history.base;
    const href = base ? cleanPath(base + fullPath) : fullPath;
    const classes = {};
    const activeClass =
      this.activeClass ||
      router.options.linkActiveClass ||
      'router-link-active';
    const compareTarget = to.path ? createRoute(null, to) : resolved;
    classes[activeClass] = this.exact
      ? isSameRoute(current, compareTarget)
      : isIncludedRoute(current, compareTarget);

    const on = {
      click: e => {
        //   a 
        e.preventDefault();
        if (this.replace) {
          router.replace(to);
        } else {
          router.push(to);
        }
      }
    };

    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
      const a = findAnchor(this.$slots.default);
      if (a) {
        const aData = a.data || (a.data = {});
        aData.on = on;
        const aAttrs = aData.attrs || (aData.attrs = {});
        aAttrs.href = href;
      }
    }

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

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;
      }
    }
  }
}
 

Concluding remarks

Finally, the last, Hashand the Abstracttwo models are relying onHistory base class to achieve here is not to do in-depth analysis. If you are interested, please refer tovue-router .

For more details, it is recommended to put vue-router project pulled down, local run, according to vue-routeranalysis provided by, some full harvest.

Refer to