Thank you

Funfish, playing with the ghost of the heart, vue. js technology reveals the secrets of the article, for my help

preface

Vue-router doesn’t have a lot of source code, but it doesn’t have a lot of content either. This article is not a line by line analysis, but it will explain the main process and principles in as much detail as possible. Forgive me for skipping some of the utility functions and edge conditions because I didn’t go through them line by line either.

Pre-basic knowledge

Before we learn the VueRouter source code, let’s review the hash and histroy related knowledge. Refer to the MDN documentation for more details. This section is excerpted from the MDN documentation.

hash

onhashchange

When the fragment identifier of the URL changes, the HashChange event (the part of the URL that follows the # symbol, including the # symbol) is triggered. Note that histroy.pushState() never fires a HashChange event, even if the new URL differs from the old URL only in the hash.

histroy

pushState

PushState () takes three arguments: a state object, a title (currently ignored), and a URL.

  • State is a JavaScript object that is passed to the callback function when the popState event is fired
  • Title, currently ignored by all browsers
  • Url: Indicates a new URL record

replaceState

The use of history.replacestate () is very similar to history.pushState(), except that replaceState() changes the current history entry instead of creating a new one.

onpopstate

Calling history.pushState() or history.replacestate () does not trigger the popState event. For example, click the back and forward buttons (or call the history.back(), history.forward(), or history.go() methods in JavaScript).

If the currently active history entry was created by the history.pushState() method or modified by the history.replacestate () method, The State property of the PopState event object contains a copy of the history entry’s state object.

Application initialization

Usually when building a Vue application, we install VueRouter as a plug-in using vue.use. At the same time, the router instance is mounted to the Vue instance.

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

let a = new Vue({
  router,
  render: h= > h(App)
}).$mount('#app')
Copy the code

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router({
  mode: 'history'.base: process.env.BASE_URL,
  routes: [{path: '/'.name: 'home'.component: Home
    },
    {
      path: '/about'.name: 'about'.component: (a)= > import(/* webpackChunkName: "about" */ './views/About.vue')}]})Copy the code

Plug-in installation

It is stated in Vue’s documentation that the plug-in for vue.js should have a public method install. The first argument to this method is the Vue constructor, and the second argument is an optional option object. We first look at the install.js file in the source code.

In the install file, we initialize some private properties on the instance of Vue

  • _routerRoot refers to the instance of Vue
  • _router refers to an instance of VueRouter

Some getters are initialized on Vue’s Prototype

  • $router indicates the instance of the current router
  • $route indicates the information about the current Router

The mixins are mixed in globally, and the RouterView and RouterLink components are registered globally.


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

export let _Vue

export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v= >v ! = =undefined

  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 () {
      // Check whether the instance is mounted with router
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        // _router: hijack the current route
        Vue.util.defineReactive(this.'_route'.this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this.this)
    },
    destroyed () {
      registerInstance(this)}})Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

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

  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  conststrats = Vue.config.optionMergeStrategies strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate =  strats.created }Copy the code

Vue.util. DefineReactive, this is how the observer in Vue hijacks the data, hijacks the _route, and notifies the dependent components when the _route fires the setter method. The RouterView, on the other hand, needs to access the parent.$route so it forms a dependency (as we’ll see later).

👀 Let’s go to Vue and look at the defineReactive source code. In defineReactive, it hijks the setter method for _route with object.defineProperty. Set notifies observers.



Object.defineProperty(obj, key, {
  enumerable: true.configurable: true.get: function reactiveGetter () {
    // ...
  },
  set: function reactiveSetter (newVal) {
    // ...childOb = ! shallow && observe(newVal) dep.notify() } })Copy the code

VueRouter instance


export default class VueRouter {
  constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)

    let mode = options.mode || 'hash'
    // fallback will fallback to hash mode if the history environment is not supported
    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

matcher

The Matcher object contains two properties, addRoutes and match.

pathList, pathMap, nameMap

PathList, pathMap, and nameMap are the list of paths, the mapping of paths and route objects, and the mapping of route names and route objects. Vue-router target supports dynamic routing. PathList, pathMap, and nameMap can be dynamically modified after initialization. They are created by the createRouteMap method. Let’s take a look at the source code for createRouteMap.


export function createRouteMap (routes, oldPathList, oldPathMap, oldNameMap) {
  // pathList, pathMap, nameMap can be dynamically added later
  const pathList: Array<string> = oldPathList || []
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  // Traverse the routing list
  routes.forEach(route= > {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })

  // Push the wildcard path to the end of pathList
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === The '*') {
      pathList.push(pathList.splice(i, 1) [0])
      l--
      i--
    }
  }

  return {
    pathList,
    pathMap,
    nameMap
  }
}
Copy the code

Routes are a set of routes, so we loop routes, but routes may have children so we create routes recursively. Return a route tree 🌲


function addRouteRecord (pathList, pathMap, nameMap, route, parent, matchAs) {
  const { path, name } = route
 
  const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}

  // normalizePath, which formats the path
  // If route is a child, the parent and child paths are connected to form a complete path
  const normalizedPath = normalizePath(
    path,
    parent,
    pathToRegexpOptions.strict
  )

  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }

  // Create a complete route object
  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    instances: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props: route.props == null
      ? {}
      : route.components
        ? route.props
        : { default: route.props }
  }

  // If the route has children, we will recursively create the route object
  // Create a route object recursively
  if (route.children) {
    route.children.forEach(child= > {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  // Here is the processing of the route alias
  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)})}// Fill pathMap, nameMap, pathList
  if(! pathMap[record.path]) { pathList.push(record.path) pathMap[record.path] = record }if (name) {
    if(! nameMap[name]) { nameMap[name] = record } } }Copy the code

addRoutes

Dynamically add more routing rules and dynamically modify pathList, pathMap, and nameMap

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

match

The match method finds the corresponding Route in nameMap and returns it based on the raw arguments (which can be either a string or a Location object) and the currentRoute (the currentRoute object returns a Route object).

If the location contains name, I find the corresponding Route through nameMap, but at this time, the path may contain params, so we will fillParams to patch through fillParams function, and return a real path path.


function match (raw, currentRoute, redirectedFrom) {
  // Raw and currentRoute are processed, and formatted path, hash, and params are returned
  const location = normalizeLocation(raw, currentRoute, false, router)

  const { name } = location

  if (name) {
    const record = nameMap[name]
    if(! record)return _createRoute(null, location)
    
    // Get all required params. If optional is true, params is not required
    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) {
      // Fill the path with params to return a real path
      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      // Create a Route object
      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]
      // Use each regex in the pathList to match the path
      if (matchRoute(record.regex, location.path, location.params)) {
        return _createRoute(record, location, redirectedFrom)
      }
    }
  }
  return _createRoute(null, location)
}
Copy the code

Let’s move on and see what’s going on in _createRoute.


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)
  }
  return createRoute(record, location, redirectedFrom, router)
}
Copy the code

The redirect and alias will eventually call the createRoute method. Let’s turn to the createRoute function. The createRoute function returns a frozen Router object.

The matched attribute is an array containing the routing records of all nested path fragments of the current route. Arrays are ordered from the outside to the inside of the tree.


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)
}
Copy the code

init

Init. The cb callback is mounted, which is related to the RouteView rendering. According to the current URL, we complete the route initialization in the beforeCreate lifecycle hook of the Vue root instance, and complete the first route navigation.


init (app) {

  // app is an instance of Vue
  this.apps.push(app)

  if (this.app) {
    return
  }

  // Mount the app properties on the VueRouter
  this.app = app

  const history = this.history

  // Initializes the current route and completes the first navigation, calling setupListeners in hash mode in the callback to transitionTo
  // Hashchange events will be monitored in setupListeners
  // The transitionTo function performs route navigation, as described below
  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation())
  } else if (history instanceof HashHistory) {
    const setupHashListener = (a)= > {
      history.setupListeners()
    }
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener,
      setupHashListener
    )
  }

  // The cb callback is mounted. Update _route each time
  history.listen(route= > {
    this.apps.forEach((app) = > {
      app._route = route
    })
  })
}
Copy the code

history

History has three modes: hash, histroy, and Abstract. All three classes inherit from base

base

Let’s first look at the constructor for base, where router is an instance of VueRouter and base is the base path of the route. Current is the default “/” of the current route, ready is the status of the route, readyCbs is the collection of ready callbacks, and readyErrorCbs is the callback of raday failure. ErrorCbs Collection of callbacks for navigation errors.


export class History {
  constructor(router: Router, base: ? string) {this.router = router
    NormalizeBase: normalizeBase: normalizeBase: normalizeBase: normalizeBase: normalizeBase: normalizeBase: normalizeBase: normalizeBase: normalizeBase: normalizeBase: normalizeBase
    this.base = normalizeBase(base)
    // The current route object initialized
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
  }
}
Copy the code

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

function normalizeBase (base: ? string) :string {
  if(! base) {// inBrowser Determines whether the environment is a browser
    if (inBrowser) {
      const baseEl = document.querySelector('base')
      base = (baseEl && baseEl.getAttribute('href')) || '/'
      base = base.replace(/^https? : \ \ [^ \] / / / + /.' ')}else {
      base = '/'}}if (base.charAt(0)! = ='/') {
    base = '/' + base
  }
  return base.replace(/ / / $/.' ')}Copy the code

The base listen method is used in the VueRouter init method, and it adds a callback to each route update


listen (cb: Function) {
  this.cb = cb
}   
Copy the code

There are other methods in the base class such as transitionTo, confirmTransition, updateRoute that are used in the base subclass. We’ll look at their implementation in a moment in hashRouter.

HashRouter

The constructor

In the constructor of HashHistory. We will determine if the current fallback is true. If true, use checkFallback, add ‘#’, and replace the document with window.location.replace.

If fallback is false, we will call ensureSlash, which will add “#” to the url without “#” and replace the document using histroy’s API or replace.

So when we access 127.0.0.1, we automatically replace it with 127.0.0.1/#/


export class HashHistory extends History {
  constructor(router: Router, base: ? string, fallback: boolean) {super(router, base)
    // If it is a backoff hash case, and check whether the current path has /#/. If not, '/#/' will be added.
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }
}
Copy the code

checkFallback


// Check whether the url contains' /#/ '
function checkFallback (base) {
  // Get the hash value
  const location = getLocation(base)
  // If location does not begin with /#. Add /# to replace the document with window.location.replace
  if (!/ # ^ / / /.test(location)) {
    window.location.replace(
      cleanPath(base + '/ #' + location)
    )
    return true}}Copy the code
/ / return the hash
export function getLocation (base) {
  let path = decodeURI(window.location.pathname)
  if (base && path.indexOf(base) === 0) {
    path = path.slice(base.length)
  }
  return (path || '/') + window.location.search + window.location.hash
}
Copy the code

// Delete // and replace with /
export function cleanPath (path) {
  return path.replace(/\/\//g.'/')}Copy the code

ensureSlash


function ensureSlash () :boolean {
  // Check whether # is included and get the hash value. If the URL has no #, return ' '
  const path = getHash()
  // Check whether path starts with /
  if (path.charAt(0) = = ='/') {
    return true
  }
  // Add/if it doesn't start with '/'
  replaceHash('/' + path)
  return false
}
Copy the code
// Get the hash after "#"
export function getHash () :string {
  const href = window.location.href
  const index = href.indexOf(The '#')
  return index === - 1 ? ' ' : decodeURI(href.slice(index + 1))}Copy the code
function replaceHash (path) {
  // supportsPushState determines whether an API for history exists
  // Replace the document with replaceState or window.location.replace
  // getUrl Gets the full URL
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
Copy the code
// getUrl returns the full path and will add # to make sure /#/ exists
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

In the replaceHash, we call the replaceState method, and in the replaceState method, we call the pushState method. In pushState we call the saveScrollPosition method, which records the current scroll position. The document is then updated using either the histroyAPI or window.location.replace.


export function replaceState (url? : string) {
  pushState(url, true)}export function pushState (url? : string, replace? : boolean) {
  // Record the current x and y axes, using the time of navigation as the key, and record the location information in the positionStore
  saveScrollPosition()
  const history = window.history
  try {
    if (replace) {
      history.replaceState({ key: _key }, ' ', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, ' ', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}
Copy the code

push, replace,

We put push and replace together because they implement similar source code. In push and replace, call the transitionTo method. The transitionTo method is in the base class. Let’s now turn around and look at the transitionTo source code (👇). But callback nested callback, like honey passed to honey, is still a bit disgusting.)


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

replace (location, onComplete, onAbort) {
  const { current: fromRoute } = this
  this.transitionTo(
    location,
    route => {
      replaceHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    },
    onAbort
  )
}
Copy the code

transitionTo, confirmTransition, updateRoute

The location argument to transitionTo is our destination path, which can be a string or a RawLocation object. We use the router-match method (which we discussed in Matcher), which returns our target route object. Then we call the confirmTransition function.


transitionTo (location, onComplete, onAbort) {
  const route = this.router.match(location, this.current)
  this.confirmTransition(
    route,
    () => {
      // ...
    },
    err => {
      // ...})}Copy the code

IsSameRoute detects if the navigation is the same route, and if it is, stops 🤚 navigation and performs a callback that terminates the navigation.


if (
  isSameRoute(route, current) &&
  route.matched.length === current.matched.length
) {
  this.ensureURL()
  return abort()
}
Copy the code

Then we call the resolveQueue method, which takes the current route and the target route matched as parameters. The resolveQueue works as shown below. We will compare the routes of the two arrays one by one, find the routes that need to be destroyed, updated, or activated, and return them (because we need to perform different route guards for them).

function resolveQueue (current next) {
  let i
  // Compare the current route with the matched attribute of the target route
  const max = Math.max(current.length, next.length)
  for (i = 0; i < max; i++) {
    if(current[i] ! == next[i]) {break}}return {
    updated: next.slice(0, i),
    activated: next.slice(i),
    deactivated: current.slice(i)
  }
}
Copy the code

Next, we’re going to extract, one by one, all of the route guards that we want to execute, and concat them into a queue. The queue contains all route guards that need to be executed during this route update.

In the first step, we use the extractLeaveGuards function to extract the “beforeRouteLeave” guards from all components in DeActivated that need to be destroyed. ExtractLeaveGuards is going to call extractGuards, extractGuards is going to call flatMapComponents, The flatMapComponents function iterates over records(resolveQueue returns Deactivated), and during the iterating process we take the component, its instance, the route object, Fn (the callback passed to flatMapComponents in extractGuards) is passed in, and in FN we get the beforeRouteLeave guard in the component.


// Return the collection of navigation in each component
function extractLeaveGuards (deactivated) {
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)}function extractGuards (records, name, bind, reverse?) {
  const guards = flatMapComponents(
    records,
    // def is a component
    // Instance indicates the instance of the component
    (def, instance, match, key) => {
      // Return the route guard defined in each component
      const guard = extractGuard(def, name)
      if (guard) {
        // The bindGuard function ensures that this points to an instance of the Component
        return Array.isArray(guard)
          ? guard.map(guard= > bind(guard, instance, match, key))
          : bind(guard, instance, match, key)
      }
    }
  )
  // Return to the collection of navigation
  return flatten(reverse ? guards.reverse() : guards)
}

export function flatMapComponents (matched, fn) {
  // Pass through matched and return each Component in each route in matched
  return flatten(matched.map(m= > {
    // If components are not set, the default is Components {default: YouComponent}, as shown in the addRouteRecord function
    // Pass all components in matched to FN
    M.ponents [key] is the component corresponding to the key key in Components
    M.i npO [key] is an instance of a 参 考 component, and this property is assigned in the beforecreated in the RouterView component
    return Object.keys(m.components).map(key= > fn(
      m.components[key],
      m.instances[key],
      m,
      key
    ))
  }))
}

// Return a new array
export function flatten (arr) {
  return Array.prototype.concat.apply([], arr)
}

// Get the properties in the component
function extractGuard (def, key) {
  if (typeofdef ! = ='function') {
    def = _Vue.extend(def)
  }
  return def.options[key]
}

// Fix the reference to this in the function
function bindGuard (guard, instance) {
  if (instance) {
    return function boundRouteGuard () {
      return guard.apply(instance, arguments)}}}Copy the code

Second, get the guard of the global VueRouter object beforeEach

The third step is to extract all beforeRouteUpdate guards from the Update component using the extractUpdateHooks function. The process is similar to the first step.

Step 4 Obtain the beforeEach guard in the Options configuration of Activated

Part five, get all asynchronous components


We define an iterator after we get all the route guards. Next we use runQueue to traverse the queue. Pass each element in the queue into the FN (iterator), where the route guard executes, and the route guard must explicitly call the next method to enter the next pipe and the next iteration. After the iteration is complete, the Callback for runQueue is executed.

In the runQueue callback, we get the guard of the beforeRouteEnter in the active component, and store the next callback from the beforeRouteEnter guard into postEnterCbs. A callback to next is performed through postEnterCbs after the navigation is confirmed.

After the queue completes, confirmTransition executes the onComplete callback passed by transitionTo. Look down 👇

// Queue indicates the queue of route guards
// fn is the iterator defined
export function runQueue (queue, fn, cb) {
  const step = index= > {
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        // Handle each hook with an iterator
        // fn is an iterator
        fn(queue[index], () => {
          step(index + 1)})}else {
        step(index + 1)
      }
    }
  }
  step(0)}/ / the iterator
const iterator = (hook, next) = > {
  if (this.pending ! == route) {return abort()
  }
  try {
    // The incoming route guards three parameters, corresponding to to, from, next
    hook(route, current, (to: any) => {
      if (to === false || isError(to)) {
        // If the next argument is false
        this.ensureURL(true)
        abort(to)
      } else if (
        // If next needs to redirect to another route
        typeof to === 'string'| | -typeof to === 'object' && (
          typeof to.path === 'string' ||
          typeof to.name === 'string'
        ))
      ) {
        abort()
        if (typeof to === 'object' && to.replace) {
          this.replace(to)
        } else {
          this.push(to)
        }
      } else {
        // Go to the next pipe
        next(to)
      }
    })
  } catch (e) {
    abort(e)
  }
}

runQueue(
  queue,
  iterator,
  () => {
    const postEnterCbs = []
    const isValid = (a)= > this.current === route
    // Get beforeRouteEnter for all active components. BeforeRouteEnter guards within components cannot get this instance
    // Since the active component has not yet been created, we can access the component instance by passing a callback to next.
    // beforeRouteEnter (to, from, next) {
    // next(vm => {
    // // Access the component instance through 'vm'
    / /})
    // }
    const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
    // Get the routing guard for global beforeResolve
    const queue = enterGuards.concat(this.router.resolveHooks)
    // Iterate through the queue again
    runQueue(queue, iterator, () => {
      // Complete the transition
      if (this.pending ! == route) {return abort()
      }
      // Set the transitioning route to NULL
      this.pending = null
      // 
      onComplete(route)
      // After the navigation is confirmed, we execute the beforeRouteEnter guard in the next callback
      if (this.router.app) {
        this.router.app.$nextTick((a)= > {
          postEnterCbs.forEach(cb= > { cb() })
        })
      }
    }
  )
})

// Get the beforeRouteEnter guard in the component
function extractEnterGuards (activated, cbs, isValid) {
  return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
    // There is no change to the pointing of this in guard
    return bindEnterGuard(guard, match, key, cbs, isValid)
  })
}

// Push the next callback from the beforeRouteEnter guard to postEnterCbs
function bindEnterGuard (guard, match, key, cbs, isValid) {
  // The next argument is passed in the iterator
  return function routeEnterGuard (to, from, next) {
    return guard(to, from, cb => {
      // Execute next passed in the iterator to go to the next pipe
      next(cb)
      if (typeof cb === 'function') {
        // We will wrap the next callback and save it to CBS. The next callback will execute the callback when the navigation is confirmed
        cbs.push((a)= > {
          poll(cb, match.instances, key, isValid)
        })
      }
    })
  }
}
Copy the code

In the onComplete callback for confirmTransition, we call the updateRoute method, taking the route to navigate. In updateRoute we will update the current route (history.current) and run cb(update the _route property on the Vue instance, 🌟 which will trigger the RouterView to rerender)


updateRoute (route: Route) {
  const prev = this.current
  this.current = route
  this.cb && this.cb(route)
  // Execute after hooks
  this.router.afterHooks.forEach(hook= > {
    hook && hook(route, prev)
  })
}
Copy the code

Next we execute onComplete, the transitionTo callback function. The replaceHash or pushHash methods are called in the callback. They update the hash value of the location. If historyAPI compatible, either history.replacestate or history.pushState will be used. If historyAPI is not compatible it will use window.location.replace or window.location.hash. The handleScroll method updates the position of our scrollbar and we won’t go into that here.


/ / replaceHash method
(route) => {
  replaceHash(route.fullPath)
  handleScroll(this.router, route, fromRoute, false)
  onComplete && onComplete(route)
}

/ / push method
route => {
  pushHash(route.fullPath)
  handleScroll(this.router, route, fromRoute, false)
  onComplete && onComplete(route)
}
Copy the code

Ok, so now we’re done with the replace or push method.

🎉🎉🎉🎉🎉🎉 the following is the complete code in transitionTo, confirmTransition. 🎉 🎉 🎉 🎉 🎉 🎉


// onComplete navigation successful callback
// onAbort Callback for navigation termination
transitionTo (location, onComplete, onAbort) {
  const route = this.router.match(location, this.current)
  this.confirmTransition(route,
    () => {
      this.updateRoute(route)
      onComplete && onComplete(route)
      this.ensureURL()
      if (!this.ready) {
        this.ready = true
        this.readyCbs.forEach(cb= > { cb(route) })
      }
    },
    err => {
      if (onAbort) {
        onAbort(err)
      }
      if (err && !this.ready) {
        this.ready = true
        this.readyErrorCbs.forEach(cb= > { cb(err) })
      }
    }
  )
}

// onComplete navigation successful callback
// onAbort Callback for navigation termination
confirmTransition (route: Route, onComplete: Function, onAbort? :Function) {

  // The current route
  const current = this.current

  const abort = err= > {
    if (isError(err)) {
      if (this.errorCbs.length) {
        this.errorCbs.forEach(cb= > { cb(err) })
      }
    }
    onAbort && onAbort(err)
  }
  
  // Determine whether the navigation is to the same route. If so, we terminate the navigation
  if (
    isSameRoute(route, current) &&
    route.matched.length === current.matched.length
  ) {
    this.ensureURL()
    return abort()
  }

  // Get all routes that need to be activated, updated, or destroyed
  const {
    updated,
    deactivated,
    activated
  } = resolveQueue(this.current.matched, route.matched)

  // Get all route guards that need to be executed
  const queue = [].concat(
    extractLeaveGuards(deactivated),
    this.router.beforeHooks,
    extractUpdateHooks(updated), 
    activated.map(m= > m.beforeEnter),
    resolveAsyncComponents(activated)
  )

  this.pending = route

  // Define iterators
  const iterator = (hook: NavigationGuard, next) = > {
    if (this.pending ! == route) {return abort()
    }
    try {
      hook(route, current, (to: any) => {
        if (to === false || isError(to)) {
          this.ensureURL(true)
          abort(to)
        } else if (
          typeof to === 'string'| | -typeof to === 'object' && (
            typeof to.path === 'string' ||
            typeof to.name === 'string'
          ))
        ) {
          abort()
          if (typeof to === 'object' && to.replace) {
            this.replace(to)
          } else {
            this.push(to)
          }
        } else {
          next(to)
        }
      })
    } catch (e) {
      abort(e)
    }
  }

  // Iterate over all routing guards
  runQueue(
    queue,
    iterator, 
    () => {
      const postEnterCbs = []
      const isValid = (a)= > this.current === route
      const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
      const queue = enterGuards.concat(this.router.resolveHooks)
      runQueue(queue, iterator, () => {
        if (this.pending ! == route) {return abort()
        }
        this.pending = null
        onComplete(route)
        if (this.router.app) {
          this.router.app.$nextTick((a)= > {
            postEnterCbs.forEach(cb= > { cb() })
          })
        }
      }
    )
  })
}
Copy the code

go, forward, back

The go, forward, and back methods defined on the VueRouter call the GO method of the history property

// index.js

go (n) {
  this.history.go(n)
}

back () {
  this.go(- 1)
}

forward () {
  this.go(1)}Copy the code

The go method on the hash is called history.go. How does it update the RouteView? The answer is that the hash object is listening for either popState or HashChange events in the setupListeners method. An update to the RoterView is triggered in the callback to the event

// The go method calls history.go
go (n) {
  window.history.go(n)
}
Copy the code

setupListeners

When we click on the back, forward button or call back, forward, go methods. We did not actively update _app.route and current. How do we trigger an update to the RouterView? By listening for popState or hashChange events on the window. In the callback to the event, the transitionTo method is called to complete the update to _route and current.

Alternatively, when using the push, replace method, the hash is updated after the _route update. With go, back, the hash update precedes the _route update.


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

HistoryRouter

The implementation of HistoryRouter is basically the same as that of HashRouter. The difference is that The HistoryRouter doesn’t do any fault tolerance to determine whether the historyAPI is supported in the current environment. Popstate events are monitored by default, and histroyAPI is used by default. If you are interested, you can see the definition of HistoryRouter in /history/html5.js.

component

RouterView

Routerviews can be nested with each other, and RouterViews rely on parent.The route is enclosing _routerRoot _route. We set _router to be responsive using vue.util.definereactive. The _route is updated in the transitionTo callback, which triggers the rendering of the RouteView. (rendering mechanism is not very understand, so far has not seen Vue source code, male tears).

export default {
  name: 'RouterView'.functional: true.// The name of the RouterView is default
  props: {
    name: {
      type: String.default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    data.routerView = true

    // h is the render function
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

    let depth = 0
    let inactive = false
    // Use the while loop to find the root of Vue. _routerRoot is the root instance of Vue
    // Depth is the depth of the current RouteView. Since routeViews can be nested within each other, depth helps us find the components that each level of RouteView needs to render
    while(parent && parent._routerRoot ! == parent) {if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth

    if (inactive) {
      return h(cache[name], data, children)
    }

    const matched = route.matched[depth]
    if(! matched) { cache[name] =null
      return h()
    }

    // Get the rendered component
    const component = cache[name] = matched.components[name]

    // registerRouteInstance will be called in beforeCreated, and the global vue. mixin implementation
    // Register the component instances in matches.instances. This will help us fix the internal pointing to this in the confirmTransition route guard
    data.registerRouteInstance = (vm, val) = > {
      const current = matched.instances[name]
      if( (val && current ! == vm) || (! val && current === vm) ) { matched.instances[name] = val } } ;(data.hook || (data.hook = {})).prepatch = (_, vnode) = > {
      matched.instances[name] = vnode.componentInstance
    }

    let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
    if (propsToPass) {
      propsToPass = data.props = extend({}, propsToPass)
      const attrs = data.attrs = data.attrs || {}
      for (const key in propsToPass) {
        if(! component.props || ! (keyin component.props)) {
          attrs[key] = propsToPass[key]
          delete propsToPass[key]
        }
      }
    }
    // Render component
    return h(component, data, children)
  }
}
Copy the code

conclusion

We’re done looking at the VueRouter source code. Generally speaking, it’s not very complicated. The bottom line is to use vue.util.definereactive to set the _route attribute of the instance to responsive. The push, replace methods will actively update the _route property. The go, back, or back button updates the _route in the onHashChange or onPopState callback, and the _route update triggers a rerendering of the RoterView

But it also skips things like keep-live, scrolling behavior. I’m going to go ahead and implement a simple version of VueRouter based on the core principles of VueRouter, but I haven’t started yet.

other

From mid-late March around has been learning some library source code, itself learning source code is not very helpful to the work. Because like VueRouter, Preact is well documented. Look at the source code is purely personal interest, but learned the source code of these libraries, their own implementation of a simple version, or a sense of achievement.

Preact source code analysis

Simple React implementation