Vue – Router principle analysis

The overall content is long and requires a certain amount of patience. You can first understand the general idea and then make breakthroughs one content at a time. Let’s start with the basic use of vue-Router.

1.1 Defining a routing file
// router.js
import Router from 'vue-router'
Vue.use(Router)
const routes = [
  {
    path: '/'.name: 'Home'.component: Home
  },
  {
    path: '/about'.name: 'About'.component: () = > import(/* webpackChunkName: "about" */ '.. /views/About.vue'),
    children:[
      {
        path: 'a'.name: 'a'.component: () = >import(/* webpackChunkName: "aboutA" */ '.. /views/AboutA.vue')}, {path: 'b'.name: 'b'.component: () = >import(/* webpackChunkName: "aboutA" */ '.. /views/AboutB.vue'}]}]const router = new VueRouter({
  routes
})

router.beforeEach((to, from, next) = > {
    console.log('beforeEach')
    next()
})

export default router
Copy the code
1.2 Introduced in main.js
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  name: 'main'.render: h= > h(App)
}).$mount('#app')
Copy the code
1.3 Adding router-View and router-link components to app.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
    </div>
    <router-view/>
  </div>
</template>
<script>
export default {
  name: 'app'
}
</script>

<style>.</style>

Copy the code

The above completes the basic use of vue-router. After starting the service, enter the home page and click router-link to jump to the route. The router-view component will be replaced with the component of routes defined. The official version is to map components to routes and then tell the Vue Router where to render them. So let’s think about how it works.

Second, think about the principle from usage

1. Import and mount

From the point of view of the call sequence, the first is to introduce vue-router module, and then use vue. use(router) for plug-in installation, read the official document of vue.js: cn.vuejs.org/v2/guide/pl…

Vue.js plug-ins should expose an install method. The first argument to this method is the Vue constructor, and the second argument is an optional option object.

Create a vue-router folder, create index.js, and declare a class VueRouter. See developer.mozilla.org/zh-CN/docs/…

// index.js
import install from './install'
export default class VueRouter{}
// Instance attributes must be defined in class methods
// Install can be defined in two ways
// 1. Class attributes can be defined as static in the class
export default class VueRouter{
    static install = install
}
// 2
VueRouter.install = install;
Copy the code

Now that the infrastructure is complete, what did you do with install? Create install.js in the same directory

What does a plug-in do when it is installed? There are several points.

  1. Add a global method or property
  2. Adding a Global Resource
  3. Injection component options
  4. Add Instance method

So after vue.use (Router) is done,

  1. Add a global method or property as added$routewith$router;
  2. Add a global resource as a componentrouter-viewandrouter-link;
  3. Inject component options with vue. mixin mixed with the beforeCreate handler to provide routing operations during component initialization.
// install.js
import View from './components/view'
import Link from './components/link'
export let _Vue;
export function install (Vue) {
// Vue is passed in when use is used. The Vue constructor is stored and referenced where needed
  _Vue = Vue;
  // Inject component options
  Vue.mixin({ // Add the beforeCreate method to the life cycle of all components, which is mounted on the VUE prototype and executed for each vUE component loaded
    beforeCreate() {
      if (this.$options.router) { // If there is a router attribute, it is the root instance
        this._routerRoot = this; // Mount the root instance on the _routerRoot property
        this._router = this.$options.router; // Mount the current router instance on _router
        this._router.init(this); // Route initialization is performed on the root instance, where this refers to the router instance
        Vue.util.defineReactive(this.'_route'.this._router.history.current);
      } else { // The parent component renders the child component
        this._routerRoot = this.$parent && this.$parent._routerRoot; }}});// Add two attributes
  The value for this._routerRoot will be returned after the beforeCreate command is executed, and the value for the new Vue will not be returned until the new Vue command is executed
  $route = this.$route = this.$route = this.$route
  Object.defineProperty(Vue.prototype,'$route', {get(){
      return this._routerRoot._route; }});// Use this.$router to get an instance of the router, which is the same as new Router()
  Object.defineProperty(Vue.prototype,'$router', {get(){
      return this._routerRoot._router; }})// Added two global resource components
  Vue.component('RouterView',View);
  Vue.component('RouterLink',Link);
} 
Copy the code

2. New vueRouter parses the user configuration file

Now that we’re done with install, what does vueRouter do when it instantiates

  • (1) Establish a matcher for relation matching and provide match and Addroutes methods
  • (2) create a history type based on mode to perform path listening and matching.
  • (3) Register hook function (there are several kinds of hook function, take one to demonstrate)
// index.js
import {install} from './install'
import createMatcher from './create-matcher'
import HashHistory from './history/hash'
export default class VueRouter{
  constructor (options) {
    // To create a match based on the routes passed by the user, this.matcher requires two methods
    // match: The match method is used to match rules
    // addRoutes: used to addRoutes dynamically
    this.matcher = createMatcher(options.routes || []);
    // Create a history type based on mode, default hash
    this.history = new HashHistory(this);
    // Register the before check subroutines
    this.beforeHooks = [];
  }
  // this function is called when the component is initialized
  init(app){... }beforeEach(fn){
    this.beforeHooks.push(fn);
  }
}
VueRouter.install = install;

Copy the code

(1) Because of the complex functions, separate the createMatcher, create a create-matcher folder at the same level, and create index.js under the folder

// create-matcher/index.js
import createRouteMap from './create-route-map'
export default function createMatcher(routes){
  // Collect all the routing paths, collect the corresponding rendering relationship of the paths
  // pathList = ['/','/about','/about/a','/about/b']
  // pathMap = ['/':'/ record ','/about':'/about record '...]
  let {pathList, pathMap} = createRouteMap(routes);
  console.log(pathList, pathMap)

  // This method is used to load routes dynamically
  function addRoutes(routes){
    createRouteMap(routes,pathList,pathMap)
  }
  function match(location){... }return {
    addRoutes,
    match
  }
}

Copy the code

Create create-route-map.js under create-matcher

// create-matcher/create-route-map.js
export default function createRouteMap(routes,oldPathList,oldPathMap){
  // There is no pathList and pathMap when it is first loaded
  let pathList = oldPathList || [];
  let pathMap = oldPathMap || Object.create(null);
  routes.forEach(route= > {
    addRouteRecord(route,pathList,pathMap);
  });
  return {
    pathList,
    pathMap
  }
}

// pathList = ['/','/about','/about/a','/about/b']
// pathMap = ['/':'/ record ','/about':'/about record '...]
function addRouteRecord(route,pathList,pathMap,parent){
  let path = parent?`${parent.path}/${route.path}`:route.path;
  let record = {
    path,
    component:route.component,
    parent
  }
  if(! pathMap[path]){ pathList.push(path); pathMap[path] = record; }// Process child routes
  if(route.children){
    route.children.forEach(r= >{ addRouteRecord(r,pathList,pathMap,route); }}})Copy the code

(2) Implement path listening and matching, create a new folder named “history” in the same directory, and create “base.js” in the folder. Because there are many kinds of patterns, such as “history”, “hash” and “abstract”, we put the basic methods in the basic “history” class, so we think about which are the basic functions and which are unique to the pattern. Basic functions jump, update, monitor; The hash mode also changes the/path to /#/ after the project is started

// history/base.js
import {runQueue} from '.. /util/async'
export default class History{
  constructor(router){
    this.router = router;
    this.current = createRoute(null, {path:'/'});
    this.cb = null;
  }
  // Jump function
  transitionTo(location,onComplete){
    let route = this.router.match(location);
    if(location === route.path && route.matched.length === this.current.matched.length){
      return
    }
    // this. UpdateRoute (route); onComplete && onComplete();
    let queue = [].concat(this.router.beforeHooks);
    const iterator = (hook,next) = >{
      hook(route,this.current,() = >{
        next();
      })
    }
    runQueue(queue,iterator,() = >{
      this.updateRoute(route); onComplete && onComplete(); })}updateRoute(route){
    this.current = route;
    this.cb && this.cb(route);
  }
  listen(cb){
    this.cb = cb; }}export function createRoute(record,location){
  let res = [];
  if(record){
    while(record){
      res.unshift(record);
      record = record.parent
    }
  }
  return {
    ...location,
    matched: res
  }
}
Copy the code

Create hash.js and inherit Histoty

// history/hash.js
import History from "./base";
export default class HashHistory extends History{
  constructor(router){
    super(router);
    ensureSlash();
  }
  getCurrentLocation(){
    return getHash();
  }
  setupListener(){
    window.addEventListener('hashchange'.() = >{
      this.transitionTo(getHash()); })}push(location){
    this.transitionTo(location)
  }
}
function ensureSlash(){
  if(window.location.hash){
    return
  }
  window.location.hash = '/'
}
function getHash(){
  return window.location.hash.slice(1);
}

Copy the code

(3) Register hook functions

// index.js
export default class VueRouter{
  constructor (options) {...this.beforeHooks = []; }...init(app){... }beforeEach(fn){
    this.beforeHooks.push(fn); }... } VueRouter.install = install;Copy the code

Create a new util method that executes the ticker function

// util/async.js
export function runQueue (queue, iterator, cb) {
  function step (index) {
    if(index >= queue.length){
      cb();
    } else {
      let hook = queue[index];
      iterator(hook,()=>{
        step(index+1)
      })
    }
  }
  step(0)
}
Copy the code

This is made clearer by combining the runQueue in the transitionTo method in History.

3. Provide init method to listen for changes and redirect routes

// index.js
init(app){
    const history = this.history;
    // Set the path change listener
    const setupHashListener = () = >{
      history.setupListener();
    }
    // Register the route change function
    history.listen((route) = >{
      app._route = route
    })
    // Redirect route
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener
    )
  }
Copy the code

4. Then provide other push methods for routing operations

// index.js
push(location){
    this.history.push(location)
  }
Copy the code

5. Implement router-view components

Create a Components folder

// components/view.js
export default {
  functional: true.render(h,{parent,data}){  // Dynamic rendering
    let route = parent.$route;
    let depth = 0;
    data.routerView = true;
    while(parent){
      if(parent.$vnode && parent.$vnode.data.routerView){
        depth++;
      }
      parent = parent.$parent;
    }
    let record = route.matched[depth];
    console.log('record:',record);
    if(! record){return h();
    }
    returnh(record.component,data); }}Copy the code

Implement the router-link component

// components/link.js
export default {
  props: {to: {type:String.required:true
      },
      tag: {type:String}},render(){
    let tag = this.tag || 'a';
    let handler = () = >{
        this.$router.push(this.to);
    }
    return <tag onClick={handler}>{this.$slots.default}</tag>}}Copy the code