By analyzing vue-Router core flow logic, a minimum framework that can run is separated.

The front-end routing

The front end can update the page without refreshing by listening for URL changes, which is a better experience than directly location.reload(). Spa and even the micro front end we are familiar with are based on front-end routing. There are two ways to implement brushless page updates

  • hash, with the url#, such as#/a
  • history, without#, such as/a

Hash routing

By listening for the Hashchange event, the hash goes into a callback whenever it changes, at which point we can do some Ajax operations to update the page content without refreshing it.

<a href="#/a">A</a>
<a href="#/b">B</a>
<script>
    window.addEventListener('hashchange'.() = > {
        console.log(location.hash);
        // dosomething Ajax rerender
    })
</script>
Copy the code

History is part of the HTML5 specification and can listen for popState events, but the popState event callback is only triggered below

  • Click browser forward and backward
  • Manual callhistory.back.forward.gomethods
  • Change the current anchor point each timehistoryWill add an activity item
<a href="/a">home</a>
<a href="/b">about</a>
<p>Trigger popstate</p>
<script>
    window.addEventListener('click'.e= > {
        const target = e.target;
        if (target.tagName === 'A') {
            e.preventDefault();
            history.pushState(null.' ', target.getAttribute('href'))}})window.addEventListener('popstate'.() = > {
        console.log(location.pathname, location.hash);
        // dosomething Ajax rerender
    })
    // Anchor point changes
    const p = document.querySelector('p');
    p.addEventListener('click'.() = > {
        // PopState is triggered
        location.hash = '# /' + Math.random().toString(10).slice(2.6)})</script>
Copy the code

To isolate a framework that can run as little as possible, we need to know what features vue-Router has on it. At least we know that VueRouter is a class with push, go, and other methods on it. Vue-router is a plug-in to VUE, so it must comply with the plug-in mechanism and expose an install method.

Minimum frame construction

The minimum framework can be broken down into the following parts, and we will implement each part.

> < p style = "max-width: 100%; clear: both; min-height: 1em; ├─ ├─ base.js // ├─ base.js // ├─ base.js // ├─ base.js // Use └.js // Router-link Component To implement ├─ route.js // Matched route, ├─ exercises, ├─ exercises, ├─ exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercisesCopy the code

Vue.install & install

node_modules/vue/src/core/global-api/use.js

/* @flow */

import { toArray } from '.. /util/index'
export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // Avoid repeatedly installing the same plug-in
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    // Convert arguments to true array
    const args = toArray(arguments.1)
    Install (Vue) {} install(Vue) {}
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      // Call the install method of the plug-in
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this}}Copy the code

install.js

import View from './view';
import Link from './link';
export default function install(Vue) {
    For each instance, the beforeCreate will append the following periodic function
    Vue.mixin({
        beforeCreate() {
            The current component is the root instance of VUE
            if (this.$options.router) {
                // Add an attribute to point to yourself
                this._routerRoot = this;
                this._router = this.$options.router;
                // Call the init method on the instance
                this._router.init(this);
                // _route will be a responsive property that, if modified, will trigger the render Watcher update to update the view
                Vue.util.defineReactive(this.'_route'.this._router.history.current);
            } else {
                / / tree
                // If it is a child component, make the child's _routerRoot point to the parent component instance
                // All child components eventually point to the root component instance
                this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; }}});// The prototype defines the response_router attribute, which when used in child components refers to _router of _routerRoot on the root component
    Object.defineProperty(Vue.prototype, '$router', {
        get() {
            return this._routerRoot._router; }});// The prototype defines the response_route attribute, which when used in child components refers to _route of _routerRoot on the root component
    Object.defineProperty(Vue.prototype, '$route', {
        get() {
            return this._routerRoot._route; }});// Register the component
    Vue.component('router-view', View);
    Vue.component('router-link', Link);
}
Copy the code

As you can see, two global components, router-view and router-link, are registered during install

router-link & router-view

Router-link props include to and tag. Where the tag defaults to the A tag, the implementation of router-link could look like this.

<router-link to='/a'>GOTO A</router-link>
Copy the code

router-link.js

export default {
    name: 'RouterLink'.props: {
        to: {
            type: String.require: true,},tag: {
            type: String.default: 'a',}},render(h) {
        // Route redirects when clicked
        const onClick = (e) = > {
            e.preventDefault();
            this.$router.push(this.to);
        };
        // Use the render function directly to generate vNodes, additional compilation is required if JSX or SFC is used
        return h(
            this.tag,
            {
                on: {
                    click: onClick,
                },
            },
            // The default slot is children
            this.$slots.default ); }};Copy the code

router-view.js

Router-view is a component that is responsible for rendering the current route match.

export default {
    name: 'RouterView'.functional: true.render(h, props) {
        return h('div'.'I am router-view'); }}Copy the code

create-matcher

That’s how we tend to use the VueRouter

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

Vue.use(VueRouter);
export default new VueRouter({
    mode: 'hash'.routes: [{path: '/'.component: {
                render(h) {
                    return <div>HOME</div>}}, {},path: '/me'.component: {
                render(h) {
                    return <div>ME</div>},}}]});Copy the code

So VueRouter needs to receive one of these arguments

type Route = {
    path: string.component: ComponentInstance
}

type RouterOptions = {
    mode: string.routes: Route[]
}
Copy the code

Therefore, the router-View component can render the content by iterating through the Routes array to find the matching component and rendering the matching component

export default {
    name: 'RouterView'.functional: true.// Functional component, no instance
    render(h, props) {
        // todo 
        // Const matched some matching operations, such as this.$router.match(currentHash)
        returnh(matched.component, props.data); }}Copy the code

We need to define a key-map structure to map the route relationship, so that the path can be directly matched each time, and the key-map only needs to be traversed once, instead of finding the target route configuration through the number group each time.

// path-map
const pathMap = {
    '/a': {
        path: '/a'.component: { render: f },
        meta: {},
        parent: null
    },
    '/b': {
        path: '/b'.component: { render: f },
        meta: {},
        parent: null}},Copy the code

create-matcher.js

  • Create a routing mapping tablepathMap
  • Provides a route that matches the current routematch, returns the value we normally operate on$route
  • addRouteRecordRecursively generates key-value pairs, concatenating the parent’s path
import { normalizePath } from './utils';
import { createRoute } from './route';

export function createMatcher(routes) {
    // Create a routing mapping table
    / / {
    // '/a': {
    // path: '/a',
    // component: { render: f },
    // meta: {},
    // parent: null
    / /},
    // '/b': {
    // path: '/b',
    // component: { render: f },
    // meta: {},
    // parent: null
    / /},
    // }
    const pathMap = createRouteMap(routes);

    const getRoutes = () = > {
        return pathMap;
    };
    const match = (raw) = > {
        $router.push({path: '/a'})
        const pathConfig = typeof raw === 'string' ? { path: raw } : raw;

        // matchRoute is the route exposed to the user. We modify the current route attributes by operating on this object
        // Use vue-devtool
        / / {
        // fullPath: "/b"
        // hash: ""
        / / matched: [{...}]
        // meta: {}
        // name: undefined
        // params: {}
        // path: "/b"
        // query: undefined
        // }
        const matchRoute =  createRoute(pathMap[pathConfig.path], pathConfig);
        return matchRoute;
    };
    // Expose the route matching method
    return {
        getRoutes,
        match,
    };
}

export function createRouteMap(routes) {
    const pathMap = Object.create(null);
    routes.forEach((route) = > {
        addRouteRecord(pathMap, route, null);
    });
    return pathMap;
}

// DFS generates routing key-value pairs
export function addRouteRecord(pathMap, route, parent) {
    const path = route.path;
    // If the current is children, concatenate the parent's path
    / / {
    // '/b/child': {
    // path: 'b',
    // component: { render: f },
    // meta: {},
    // parent: {path: '/b', component: { render: f }, }
    / /},
    // }

    // Concatenate parent's path method '/b/child'
    const normalizedPath = normalizePath(path, parent);

    const record = {
        path: normalizedPath,
        component: route.component,
        meta: route.meta,
        parent,
    };
    if (route.children) {
        route.children.forEach((child) = > {
            addRouteRecord(pathMap, child, record);
        });
    }
    // Set key-value pairs
    if (!pathMap[record.path]) {
        pathMap[record.path] = record;
    }
}
Copy the code

route.js

Duties and responsibilities

  • Defines the currently matched route$routeData format, for examplequery,path.metaParameters, etc.
  • BFS calculationmatched, this parameter is used for route-view, the currently matched route
export function createRoute(record, location) {
    $route = $route = $route = $route
    const route = {
        name: location.name || (record && record.name),
        meta: (record && record.meta) || {},
        path: location.path || '/'.hash: location.hash || ' '.query: location.query,
        params: location.params || {},
        fullPath: location.path,
        // matched is used to handle nested router-views
        // If the child component has children, the router-view should render the parent first, and then the router-view in the parent should render children
        // <router-view>
        // 
      
        // </router-view>
        // Then the corresponding matched should look like this
        / / /
        // {path: "/parent", components, ... },
        // {path: "/parent/child", components, ... }
        // ]
        //
        matched: record ? formatMatch(record) : [],
    };
    return Object.create(route);
}

function formatMatch(record) {
    const res = [];
    while (record) {
        res.unshift(record);
        record = record.parent;
    }
    return res;
}
// Initializes the route at the beginning
export const START = createRoute(null, {
    path: '/'});Copy the code

The matched parameter above mainly solved the nesting scenario of

. If a route is configured with children, its component is also the entrance of < Route-view > to improve the Route-view component

export default {
    name: 'RouterView'.functional: true.render(_, { parent, data, children, props, _c }) {
        // The fuctional component does not have the usual "this", its second argument "context" refers to the current context
        // Add a tag to data
        data.routeView = true;

        let depth = 0;
        // When the install method is used, each component will have $route, pointing to the $route of the root component
        const route = parent.$route;
        // Why do I traverse up
        // Because vUE components are created in the order of parent before child, such a DFS process, for the following kind
        // <router-view>
        // 
      
        // </router-view>
        // Then the corresponding matched should look like this
        / / /
        // {path: "/parent", components, ... },
        // {path: "/parent/child", components, ... }
        // ]
        while (parent) {
            const vnodeData = parent.$vnode ? parent.$vnode.data : {};
            if (vnodeData.routeView) {
                depth++;
            }
            parent = parent.$parent;
        }
        / / the depth = 1 at this time
        const component = route.matched[depth];
        // _c is $createElement, which generates a vnode
        return_c(component.component, data); }};Copy the code

This can be viewed from a breakpoint when it is a secondary route

index.js

  • I derived aclass, receives incoming routing configuration items, and constructs a routing mapmatcherTable ofmatchMethod can match the current route
  • Judge the currentmodeThe default ishashRouting, strength of ahistoryWe’ll put that implementation later
  • Collect and use torouterVue instance, each increase listening once destruction cycle function, to avoid repeated listening caused by memory problems
  • Initialize the current history to add a callback for route changes
  • Define several common methods
import install from './install';
import { createMatcher } from './create-matcher';
import { HashHistory } from './history/hash';

export default class VueRouter {
    constructor(options) {
        this.apps = [];
        this.options = options;
        this.matcher = createMatcher(options.routes);
        this.mode = options.mode || 'hash';
        switch (options.mode) {
            case 'hash':
                // We can only handle one hash route, histroy
                this.history = new HashHistory(this);
                break;
            default:
                console.error('invalid mode: ', mode); }}match(raw) {
        return this.matcher.match(raw);
    }

    init(app) {
        // Why apps is an array, mainly because it is possible to have a global modal box, which is constructed by new Vue and then manually mounted to the end of the body node
        // The vue instance is isolated from the Root component and wants to use router capabilities
        // The status of both instances should be updated simultaneously, so apps is an array
        // For example
        // const globalDiaglog = props => {
        // const vm = new Vue({
        // router,
        // store,
        // render: h => h(Dialog, props)
        / /});

        // const component = vm.$mount();
        // // appends the dialog content to the end of the body
        // document.body.appendChild(component.$el);
        // return vm.$children[0];
        // };

        this.apps.push(app);

        // Only listen for the uninstallation time once
        app.$once('hook:destoryed'.() = > {
            // Reset the route
            // Remove the listening event
            this.history.teardown();
        });

        // Prevent multiple initializations of history
        if (this.app) {
            return
        }
        this.app = app;
        const history = this.history;

        // Render router-view for the first time
        history.transitionTo(history.getCurrentLocation(), () = > {
            history.setupListeners();
        });
        // Actively update the view when changing hash or history
      
        history.listen((route) = > {
            // Batch update
            this.apps.forEach((app) = > {
                // _route is a reactive attribute set via defineProperty,
                // When _route is modified, the root instance's render Watcher update is triggered to update the view
                app._route = route;
            });
        });
    }
    replace(location, onComplete) {
        this.history.replace(location, onComplete);
    }
    push(location, onComplete) {
        this.history.push(location, onComplete);
    }
    go(n) {
        this.history.go(n);
    }
}

VueRouter.install = install;
Copy the code

The HashHistory class is derived from the History class. Why should we have a HashHistory class

  • HtmlHistoryDon’t take#
  • AbstractHistory, the mode used for server rendering

They all have some common attributes and methods. To avoid duplicate code, a unified model of the parent class is needed, and subclasses can extend and decorate other methods themselves.

history/bash.js

  • Defining core methodstransitionToIt will match our mapping table and update the reactive root component’s reactive properties$route, View update
  • teardownRemove listening events that address memory leaks when components are uninstalled
import { START } from '.. /route';

export class History {
    constructor(router) {
        this.router = router;
        // Current matched route
        this.current = START;
        this.listeners = [];
    }
    // The asynchronous update policy is triggered by a reactive data route change
    updateRoute(route) {
        this.current = route;
        this.cb && this.cb(route);
    }
    listen(cb) {
        this.cb = cb;
    }
    // Core method
    // Used to switch routes
    transitionTo(location, onComplete) {
        // This match method is defined by VueRouter ourselves
        const route = this.router.match(location);
        // Update route, update view
        this.updateRoute(route);
        // A callback after success
        onComplete && onComplete(route);
    }
    teardown() {
        // clean up event listeners
        // https://github.com/vuejs/vue-router/issues/2341
        // Reset the route and delete the listening event
        this.listeners.forEach((cleanupListener) = > {
            cleanupListener();
        });
        this.listeners = [];

        // reset current history route
        // https://github.com/vuejs/vue-router/issues/3294
        this.current = START;
        this.pending = null; }}Copy the code

history/hash.js

  • After the first route jump,setupListenersPlus route listening events, why would you add, when you’re not pushing a route using an API, you’re manually modifying it in the address barhash + enterKey can also update the page
  • Expose a few commonly used onesPush, replaceMethod of the parent class of the underlying calltransitionmethods
import { History } from './base';
import { supportsPushState } from '.. /utils';
export class HashHistory extends History {
    constructor(router) {
        super(router); // Inherits the parent class
    }
    setupListeners() {
        if (this.listeners.length > 0) {
            return;
        }
        const router = router;
        const handleRoutingEvent = (e) = > {
            console.log('popstate', e)
            // Jump method of the parent base class
            this.transitionTo(getHash(), (route) = > {
                // Update the URL
                this.ensureURL();
            });
        };
        // The popState event is triggered when the hash is changed while the anchor is being used
        const eventType = supportsPushState ? 'popstate' : 'hashchange';
        window.addEventListener(eventType, handleRoutingEvent);
        this.listeners.push(() = > {
            window.removeEventListener(eventType, handleRoutingEvent);
        });
    }
    // Make sure the hash is updated
    ensureURL() {
        const { fullPath } = this.current.fullPath;
        const hash = location.hash;
       
        if (hash.substr(1) === fullPath) {
            return false;
        }
        window.location.hash = hash;
    }
    push(location) {
        this.transitionTo(location, (route) = > {
            / / update the hash
            pushHash(route.fullPath);
        });
    }
    replace(location) {
        this.transitionTo(location, (route) = > {
            / / replace the url
            window.location.replace(getUrl(route.fullPath));
        });
    }
    getCurrentLocation() {
        return getHash();
    }
    go(n) {
        window.history.go(n); }}/ / update the hash
function pushHash(path) {
    window.location.hash = path;
}

export function getHash() {
    // Hack the browser differences
    // We can't use window.location.hash here because it's not
    // consistent across browsers - Firefox will pre-decode it!
    let href = window.location.href;
    const index = href.indexOf(The '#');
    // empty path
    if (index < 0) {
        location.hash = '# /';
        return '/';
    }

    href = href.slice(index + 1);
    return href;
}

// Get the current full URL
function getUrl(path) {
    const href = window.location.href;
    const i = href.indexOf(The '#');
    const base = i >= 0 ? href.slice(0, i) : href;
    return `${base}#${path}`;
}

Copy the code

The last

After the above analysis of each core file, we have extracted the minimum running framework, of course it is very simple, such as vue-Router provides a variety of routing hooks and how to handle asynchronous components, we have not mentioned, but this is a gradual process, through the breakpoint source debugging can be slowly learned. My writing is limited, so you still learn through breakpoint debugging