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 call
history
.back
.forward
.go
methods - Change the current anchor point each time
history
Will 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 table
pathMap
- Provides a route that matches the current route
match
, returns the value we normally operate on$route
addRouteRecord
Recursively 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
$route
Data format, for examplequery
,path
.meta
Parameters, etc. - BFS calculation
matched
, 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 a
class
, receives incoming routing configuration items, and constructs a routing mapmatcher
Table ofmatch
Method can match the current route - Judge the current
mode
The default ishash
Routing, strength of ahistory
We’ll put that implementation later - Collect and use to
router
Vue 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
HtmlHistory
Don’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 methods
transitionTo
It will match our mapping table and update the reactive root component’s reactive properties$route
, View update teardown
Remove 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,
setupListeners
Plus 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
+enter
Key can also update the page - Expose a few commonly used ones
Push, replace
Method of the parent class of the underlying calltransition
methods
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