Recently, a flag was set to read through vue-Router source code. It’s a little stressful. I hope I can finish…

For details, see [email protected]

Github.com/vuejs/vue-r…

In./ SRC, use the tree command to view the directory structure

. | - components / / components (view/link) | | -- link. Js | ` - the js | - create - the matcher. Js / / Route matching | -- create -- the Route map. Js / / the Route map | -history// Router processing (hash, HTML 5) | | -- the abstract. Js | | -- base. Js | | -- hash. Js | ` -- HTML 5. Js | -- index. | js / / Router entrance - the js / / The Router installation ` -- util/library/tools | - async. Js | -- dom. Js | -- the location. Js | -- params. Js | -- path. Js | - push - state. Js | - query.js |-- resolve-components.js |-- route.js |-- scroll.js `-- warn.jsCopy the code

For example, see router.vuejs.org/guide/#html on the official website

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <! -- Use the router-link component to navigate.
    <! -- Specifies the link by passing in the 'to' attribute.
    <! -- <router-link> will be rendered as a '<a>' tag by default.
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <! -- Route exit -->
  <! -- Routing matching components will be rendered here -->
  <router-view></router-view>
</div>

<script>
  Vue. Use (VueRouter) to import Vue and VueRouter

  // 1. Define (routing) components.
  // You can import from other files
  const Foo = { template: '<div>foo</div>' }
  const Bar = { template: '<div>bar</div>' }

  // 2. Define a route
  // Each route should map one component. Where "Component" can be
  // Component constructor created by vue.extend (),
  // Or, just a component configuration object.
  // We'll talk about nesting later.
  const routes = [
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar }
  ]

  // 3. Create a router instance and pass the 'routes' configuration
  You can also pass other configuration parameters, but keep it simple for now.
  const router = new VueRouter({
    routes // routes: routes
  })

  // 4. Create and mount root instances.
  // Remember to inject the route with the router configuration parameter,
  // Make the entire application routable
  const app = new Vue({
    router
  }).$mount('#app')

  // Now the application is started!

</script>
Copy the code

Here’s a quick print:

App.options. Router equals router

The following steps are required to implement vue-Router:

1. Inject the VueRouter plug-in into Vue

2. Define router-link and router-view global components for page hopping

3. Enter the user-defined routes mapping

The plug-invue-routerThe installation

Vue-router is just a plug-in in vUE. There are several ways to develop plug-ins in VUE:

/ / https://cn.vuejs.org/v2/guide/plugins.html# plugin development

MyPlugin.install = function (Vue, options) {
  // 1. Add a global method or attribute
  Vue.myGlobalMethod = function () {
    / / logic...
  }

  // 2. Add global resources
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      / / logic...}... })// 3. Inject component options
  Vue.mixin({
    created: function () {
      / / logic...}... })// 4. Add instance methods
  Vue.prototype.$myMethod = function (methodOptions) {
    / / logic...}}Copy the code

index.js

Take a look at the vue-router entry file./ SRC /index.js

VueRouter.install = install
VueRouter.version = '__VERSION__'

if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}
Copy the code

VueRouter is inserted under./ SRC /install.js.

Make sure you only install it once

// Plug-in installation method
export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true
  // ...
}
Copy the code

The mixin then registers the instance in vue’s lifecycle beforeCreate and destroys the instance in Destroyed.

export function install (Vue) {
  // ...
  Vue.mixin({
    beforeCreate () {
      // this.$options.router comes from an instantiation of VueRouter, see app = new Vue() printed above: app.png
      // Determine whether the instance is already mounted
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        // call VueRouter's init method
        this._router.init(this)
        Vue.util.defineReactive(this.'_route'.this._router.history.current)
      } else {
        // Point each component's _routerRoot to the root Vue instance
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      / / register VueComponent
      registerInstance(this.this)
    },
    destroyed () {
      / / destroy VueComponent
      registerInstance(this)}})// ...
}
Copy the code

Using mixin will do beforeCreate and destroyed on each.vue file.

In a Vue project created using vue-CLI, if a mixin is called from helloWorld.vue, two mixins will be called, one for the parent app.vue and the other for itself.

So with this._routerRoot = this you can bind _routerRoot to the current component, that is, _routerRoot points to the root node.

Next, mount $router and $route to vue

Object.defineProperty(Vue.prototype, '$router', {
  get () { return this._routerRoot._router }
})

Object.defineProperty(Vue.prototype, '$route', {
  get () { return this._routerRoot._route }
})
Copy the code

When we use vue, this.$router, this.$route comes from here.

prompt

Define get with Object.defineProperty instead of vue.prototype. $router = this._routerroot. _router to make it read-only and unmodifiable

Finally, register the global components router-view and router-link

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

export function install (Vue) {
  // ...
  Vue.component('router-view', View)
  Vue.component('router-link', Link)
  // ...
}
Copy the code

You can use it

<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>

<router-view></router-view>
Copy the code

Return to VueRouter under./ SRC /index.js

import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'

export default class VueRouter {
  // ...
  constructor (options: RouterOptions = {}) {
    // The default is hash mode
    let mode = options.mode || 'hash'
    // Mode compatibility processing
    this.fallback = mode === 'history'&&! supportsPushState && options.fallback ! = =false
    if (this.fallback) {
      mode = 'hash'
    }
    if(! inBrowser) { mode ='abstract'
    }
    this.mode = mode

    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if(process.env.NODE_ENV ! = ='production') {
          assert(false.`invalid mode: ${mode}`)}}}// ...
}
Copy the code

You can see that there are three modes: HTML5History, HashHistory, and AbstractHistory, all from the history directory.

create-matcher.js

Routes in the example

const routes = [
  { path: '/foo'.component: Foo },
  { path: '/bar'.component: Bar }
]

const router = new VueRouter({
  routes // routes: routes
})
Copy the code

from

import { createMatcher } from './create-matcher'

export default class VueRouter {
  constructor (options: RouterOptions = {}) {
    this.matcher = createMatcher(options.routes || [], this)}}Copy the code

The matcher comes from the return of createMatcher. At first guess, createMatcher did a route mapping.

./src/create-matcher.js

export function createMatcher (routes: Array
       
        , router: VueRouter
       ) :Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  // ...

  return {
    match,
    addRoutes
  }
}
Copy the code

CreateMatcher takes two routes from the user-defined configuration. Router is an instance returned by new VueRouter. It also returns two methods match and addRoutes.

function match (raw: RawLocation, currentRoute? : Route, redirectedFrom? : Location) :Route {
  const location = normalizeLocation(raw, currentRoute, false, router)
  const { name } = location

  if (name) {
    const record = nameMap[name]
    if(process.env.NODE_ENV ! = ='production') {
      warn(record, `Route with name '${name}' does not exist`)}if(! record)return _createRoute(null, location)
    const paramNames = record.regex.keys
      .filter(key= >! key.optional) .map(key= > key.name)

    if (typeoflocation.params ! = ='object') {
      location.params = {}
    }

    if (currentRoute && typeof currentRoute.params === 'object') {
      for (const key in currentRoute.params) {
        if(! (keyin location.params) && paramNames.indexOf(key) > - 1) {
          location.params[key] = currentRoute.params[key]
        }
      }
    }

    if (record) {
      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      return _createRoute(record, location, redirectedFrom)
    }
  } else if (location.path) {
    location.params = {}
    for (let i = 0; i < pathList.length; i++) {
      const path = pathList[i]
      const record = pathMap[path]
      if (matchRoute(record.regex, location.path, location.params)) {
        return _createRoute(record, location, redirectedFrom)
      }
    }
  }
  // no match
  return _createRoute(null, location)
}
Copy the code

The match function has the following logic:

1. Call normalizeLocation to obtain location information.

2, check whether there is a name, if there is a nameMap create route

3, Check whether there is a path, if there is a path map to create a route

4. If neither name nor path exists, null is used to create a route

What is a map?

Here’s a pathMap example:

const routes = [
  // There is path but no name
  { path: '/foo'.component: Foo },
  { path: '/bar'.component: Bar }
]
Copy the code

Match is then converted to Object

function addRoutes (routes) {
  createRouteMap(routes, pathList, pathMap, nameMap)
}
Copy the code

AddRoutes Method for dynamically adding routes, such as router.addroutes ()

So just to summarize,createMatcherIs to expose two ways toVueRouterRoutes need to be mapped and dynamically added.

./src/create-route-map.js

The most important function in create-route-map.js is the addRouteRecord function

function addRouteRecord (pathList: Array
       
        , pathMap: Dictionary
        
         , nameMap: Dictionary
         
          , route: RouteConfig, parent? : RouteRecord, matchAs? : string
         
        
       ) {

  // ...

  if (route.children) {
    // ...
  }

  if(! pathMap[record.path]) { pathList.push(record.path) pathMap[record.path] = record }if (name) {
    if(! nameMap[name]) { nameMap[name] = record } }// ...

}
Copy the code

If there is a path, store pathMap, and if there is a name, store nameMap

Record: What is RouteRecord?

const record: RouteRecord = {
  path: normalizedPath, / / path
  regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), / / regular matching path, such as {path: '/ params - with regex / : id (\ \ d +)'}
  components: route.components || { default: route.component }, // Corresponding component
  instances: {},
  name, / / alias
  parent, // Parent route
  matchAs, // alias is used
  redirect: route.redirect, / / redirection
  beforeEnter: route.beforeEnter, / / hooks
  meta: route.meta || {},
  props: route.props == null
    ? {}
    : route.components
      ? route.props
      : { default: route.props }
}
Copy the code

That is, the Record holds the information needed in a route, for example we can find the corresponding component to render through the path.

If there is a nested path, that is, the current route has the children attribute, then they need to be added to the map recursively.

The fourth parameter of addRouteRecord, parent, is used to save the parent route of the current route.

if (route.children) {
  route.children.forEach(child= > {
    const childMatchAs = matchAs
      ? cleanPath(`${matchAs}/${child.path}`)
      : undefined
    addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
  })
}
Copy the code

And if you have an alias, like

const routes = [
  {path: '/a'.component: A, alias: '/ali'}]Copy the code

In the case of aliases, alias does not make a copy of a record like path, but uses a fifth parameter, matchAs, to hold path

if(route.alias ! = =undefined) {
  const aliases = Array.isArray(route.alias)
    ? route.alias
    : [route.alias]

  aliases.forEach(alias= > {
    const aliasRoute = {
      path: alias,
      children: route.children
    }
    addRouteRecord(
      pathList,
      pathMap,
      nameMap,
      aliasRoute,
      parent,
      record.path || '/' // matchAs)})}Copy the code

In this way, the alias alias can find the path path, and then the path can find the corresponding record, so as to find the corresponding routing information.

/ SRC /create-route-map.js createRouteMap./ SRC /create-route-map.js createRouteMap./ SRC /create-route-map.js

const routes = [
  { path: '/foo'.component: Foo },
  { path: '/bar'.component: Bar }
]
Copy the code

Convert to a tree structure map

map = {
  '/foo': {
    component: Foo
  },
  '/bar': {
    component: Bar
  }
}
Copy the code

Plus a function addRoutes that can be added dynamically.

It’s not clear why the author changed it to this way after going all the way around. In general, the original array is also operable.

history

Two ways to implement routing

1.hashrouting

<a href="#/home">Home page</a>
<a href="#/about">about</a>
<div id="html"></div>

<script>
  window.addEventListener('load'.function () {
    document.getElementById('html').innerHTML = location.hash.slice(1);
  });
  window.addEventListener('hashchange'.function () {
    document.getElementById('html').innerHTML = location.hash.slice(1);
  });
</script>
Copy the code

2,historyrouting

<div onclick="go('/home')">Home page</div>
<div onclick="go('/about')">about</div>
<div id="html"></div>

<script>
  function go(pathname) {
    document.getElementById('html').innerHTML = pathname;
    history.pushState({}, null, pathname);
  }
  window.addEventListener('popstate'.function () {
    go(location.pathname);
  });
</script>
Copy the code

Pay attention to

PushState (history.pushState) :

“DOMException: Failed to execute ‘pushState’ on ‘History’: A history state object with URL”

The local environment localhost needs to be created

Js, hash route Hash.js, HTML5 route html5.js, and abstract mode abstract. Js.

export class History {
  / / url jumptransitionTo (location: RawLocation, onComplete? :Function, onAbort? :Function) {
    // ...
  }
  // Confirm the jump
  confirmTransition (route: Route, onComplete: Function, onAbort? :Function) {
    // ...}}// Get the base value
function normalizeBase (base: ? string) :string {
  // ...
}
// Cross-compare the current route record with the current route record to determine which route record hook function to call
function resolveQueue () {
  // ...
}
Copy the code

First, base class History has an important function transitionTo. TransitionTo matches the current path this.current based on the destination location. For example, to jump from the home page to the /bar page, run

const route = this.router.match(location, this.current)
Copy the code

This. The current from

import { START, isSameRoute } from '.. /util/route'

export class History {
  constructor(router: Router, base: ? string) {this.current = START
  }
}
Copy the code
// ./src/util/route.js
export function createRoute (record: ? RouteRecord, location: Location, redirectedFrom? :? Location, router? : VueRouter) :Route {
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}
  try {
    query = clone(query)
  } catch (e) {}

  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/'.hash: location.hash || ' ',
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  return Object.freeze(route)
}

export const START = createRoute(null, {
  path: '/'
})
Copy the code

The confirmTransition function is then called

confirmTransition (route: Route, onComplete: Function, onAbort? :Function) {
  // Return if the route is the same
  if (
    isSameRoute(route, current) &&
    route.matched.length === current.matched.length
  ) {
    this.ensureURL()
    return abort()
  }
}
Copy the code
const {
  updated,
  deactivated,
  activated
} = resolveQueue(this.current.matched, route.matched)
Copy the code

Matched from the. / SRC/util/route. Js

const route: Route = {
  // ...
  matched: record ? formatMatch(record) : []
}
Copy the code

A record is the basic unit of a Route, and the formatMatch function takes a record and all its parents into an array.

function resolveQueue (
  current: Array<RouteRecord>, //Next: Array<RouteRecord>//About to jump page) :{
  updated: Array<RouteRecord>, // Same parent component
  activated: Array<RouteRecord>, // Jump to the component that needs to be updated
  deactivated: Array<RouteRecord> // The component of the current page that needs to be updated
}
Copy the code

For example, if you jump from /foo to /bar in the example above, the parent component is the same and does not need to be updated, but /foo needs to be updated (removed) and /bar needs to be updated (added).

Further down, it’s what’s officially called “navigation Guard.”

// Navigation guard array
const queue: Array<? NavigationGuard> = [].concat(// Deactivate the component hook
  extractLeaveGuards(deactivated),
  // global beforeEach hook
  this.router.beforeHooks,
  // Called when the current route changes but the component is being reused
  extractUpdateHooks(updated),
  // Requires the render component Enter guard hook
  activated.map(m= > m.beforeEnter),
  // Parse the asynchronous routing component
  resolveAsyncComponents(activated)
)
Copy the code

1,deactivatedThe component plusbeforeRouteLeaveHook.

ExtractGuards extracts the guards for each stage from the RouteRecord array. The flatMapComponents method retrieves all navigation from Records.

function extractGuards (records: Array
       
        , name: string, bind: Function, reverse? : boolean
       ) :Array<?Function> {
  const guards = flatMapComponents(records, (def, instance, match, key) => {
    const guard = extractGuard(def, name)
    if (guard) {
      return Array.isArray(guard)
        ? guard.map(guard= > bind(guard, instance, match, key))
        : bind(guard, instance, match, key)
    }
  })
  return flatten(reverse ? guards.reverse() : guards)
}
Copy the code

Once guard is obtained, the bind method is also called to bind the component instance to Guard as the context in which the function executes.

function bindGuard (guard: NavigationGuard, instance: ? _Vue): ?NavigationGuard {
  if (instance) {
    return function boundRouteGuard () {
      return guard.apply(instance, arguments)}}}Copy the code

2. Add globalbeforeEachHook.

BeforeEach is defined in./ SRC /index.js. When the user uses router.beforeEach, a hook function is added to router.beforeHooks, so this.router.beforeHooks get the global beforeEach of the user registration.

beforeEach (fn: Function) :Function {
  return registerHook(this.beforeHooks, fn)
}

function registerHook (list: Array<any>, fn: Function) :Function {
  list.push(fn)
  return (a)= > {
    const i = list.indexOf(fn)
    if (i > - 1) list.splice(i, 1)}}Copy the code

3, toupdatedThe component plusbeforeRouteUpdateHook.

And extractLeaveGuards(deactivated) not much different.

4. Add globalbeforeEnterHook.

BeforeEnter function defined in the active route configuration.

5. Load the asynchronous component to be activated.

ResolveAsyncComponents returns a navigational guard function with standard to, FROM, and next parameters. Its internal implementation is very simple, using the method of flatMapComponents from matched to obtain the definition of each component, judge if it is asynchronous components, the implementation of asynchronous component loading logic.

After resolveAsyncComponents(activated) resolves all active asynchronous components, we can retrieve all active components.

6. Call from an active componentbeforeRouteEnter

Call globalbeforeResolve

beforeResolve (fn: Function) :Function {
  return registerHook(this.resolveHooks, fn)
}
Copy the code

8. Navigation is confirmed.

9. Call globalafterEach

After onComplete(route) is executed, the this.updateroute (route) method is executed.

Trigger DOM updates.

11. Call with the created instancebeforeRouteEnterThe guards tonextThe callback function of.

const iterator = (hook: NavigationGuard, next) = > {
  // ...
}
Copy the code

The iterator function logic is simple. It executes each navigational guard hook and passes in route, current, and anonymous functions that correspond to to, from, and next in the document. When the anonymous function is executed, Abort or next is called depending on some condition, and only when next is called is the next navigational guard hook function advanced. This is why the official documentation says that resolve must be called only when next is called.

So, how do we execute these queues?

A function that creates an iterator pattern

function runQueue (queue, fn, cb) {
  var step = function (index) {
    if (index >= queue.length) {
      cb();
    } else {
      if (queue[index]) {
        fn(queue[index], function () {
          step(index + 1);
        });
      } else {
        step(index + 1); }}}; step(0);
}
/ / sample
// Execution can be stopped at any time by calling 'next' or not.
var arr = [1.2.3];
runQueue(arr, function (a, next) {
  console.log(a, next);
  // next();
}, function () {
  console.log('callback');
});
Copy the code

component


components are rendered by render.

const route = parent.$route // Get the current path
Copy the code


supports nesting

while(parent && parent._routerRoot ! == parent) {if (parent.$vnode && parent.$vnode.data.routerView) {
    depth++
  }
  if (parent._inactive) {
    inactive = true
  }
  parent = parent.$parent
}
Copy the code

Remember from the beginning that _routerRoot represents the root Vue instance.

It also defines a method for registering route instances.

data.registerRouteInstance = (vm, val) = > {
  // val could be undefined for unregistration
  const current = matched.instances[name]
  if( (val && current ! == vm) || (! val && current === vm) ) { matched.instances[name] = val } }Copy the code

In./ SRC /install.js, we call this method to register instances of the route.

const registerInstance = (vm, callVal) = > {
  let i = vm.$options._parentVnode
  if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
    i(vm, callVal)
  }
}

Vue.mixin({
  beforeCreate () {
    registerInstance(this.this)
  },
  destroyed () {
    registerInstance(this)}})Copy the code

The

component is also rendered by render.

When we click on

, we actually end up executing router.push.

// ./src/index.jspush (location: RawLocation, onComplete? :Function, onAbort? :Function) {
  this.history.push(location, onComplete, onAbort)
}
Copy the code

Let’s look at the implementation of push in hash mode.

push (location: RawLocation, onComplete? :Function, onAbort? :Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    pushHash(route.fullPath)
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}
Copy the code

After the transitionTo jump, pushHash is called to update the browser URL and push the current URL onto the history stack.

During the initialization of history, a listener is set up to listen for changes in the history stack.

setupListeners () {
  const router = this.router
  const expectScroll = router.options.scrollBehavior
  const supportsScroll = supportsPushState && expectScroll

  if (supportsScroll) {
    setupScroll()
  }

  window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () = > {const current = this.current
    if(! ensureSlash()) {return
    }
    this.transitionTo(getHash(), route => {
      if (supportsScroll) {
        handleScroll(this.router, route, current, true)}if(! supportsPushState) { replaceHash(route.fullPath) } }) }) }Copy the code

Portal: Vuex source code full analysis


Finally finished!! Click the last like or follow the public account “Big front-end FE”.