Vue source code Vue Router path switching

Keywords: Javascript Vue Vue.js

Vue source code Vue Router (IV) path switching

The learning content and article content are from Mr. Huang Yi
Uncover the source code of Vue.js2.0,
Vue.js 3.0 core source code analysis
The source code analyzed here is Vue.js of Runtime + Compiler
Debug code: node_modules\vue\dist\vue.esm.js
Vue version: Vue.js 2.5.17-beta

The more you live, the better your life will be. -- Frank Lloyd Wright
Classic quotations from the fruit of life

Click back to the Vue source code to learn the complete directory
Vue router – GitHub warehouse address

Path switching

history.transitionTo is a very important method in Vue router. When we switch the route, we will execute this method,

In the previous section, we analyzed the relevant implementation of matcher to know how it finds a new matching line, and what to do after matching a new line,

Next, let's fully analyze the implementation of transitionTo, which is defined in src/history/base.js:

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  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) })
    }
  })
}

transitionTo first executes this.router.match method according to the target location and current path this.current to match the path to the target.

Here this.current is the current path maintained by history, and its initial value is initialized in the constructor of history:

this.current = START

START is defined in src/util/route.js:

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

In this way, an initial Route is created, and transitionTo is actually switching this.current, as we will see later.

After the new path is obtained, the confirmTransition method will be executed to make real switching. Since this process may have some asynchronous operations (such as asynchronous components), the entire confirmTransition API is designed with successful callback functions and failed callback functions. Let's take a look at its definition first:

confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
  const current = this.current
  const abort = err => {
    if (isError(err)) {
      if (this.errorCbs.length) {
        this.errorCbs.forEach(cb => { cb(err) })
      } else {
        warn(false, 'uncaught error during route navigation:')
        console.error(err)
      }
    }
    onAbort && onAbort(err)
  }
  if (
    isSameRoute(route, current) &&
    route.matched.length === current.matched.length
  ) {
    this.ensureURL()
    return abort()
  }

  const {
    updated,
    deactivated,
    activated
  } = resolveQueue(this.current.matched, route.matched)

  const queue: Array<?NavigationGuard> = [].concat(
    extractLeaveGuards(deactivated),
    this.router.beforeHooks,
    extractUpdateHooks(updated),
    activated.map(m => m.beforeEnter),
    resolveAsyncComponents(activated)
  )

  this.pending = route
  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)
    }
  }

  runQueue(queue, iterator, () => {
    const postEnterCbs = []
    const isValid = () => 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(() => {
          postEnterCbs.forEach(cb => { cb() })
        })
      }
    })
  })
}

First, the abort function is defined, and then it is judged that if the calculated route and current are the same path, this.ensueurl and abort are called directly,

The ensueurl function will be introduced later.

Then, the resolveQueue method is executed according to current.matched and route.matched, and three queues are parsed:

function resolveQueue (
  current: Array<RouteRecord>,
  next: Array<RouteRecord>
): {
  updated: Array<RouteRecord>,
  activated: Array<RouteRecord>,
  deactivated: Array<RouteRecord>
} {
  let i
  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)
  }
}

Because route.matched is an array of routerecords, since the path changes from current to route, traverse and compare the routerecords on two sides to find a different position i, then the routerecords from 0 to i in the next are the same on both sides, which is the updated part;

The RouteRecord from i to the last is unique to next and is the activated part; The RouteRecord from i to the last in the current does not exist. It is the deactivated part.

After getting the updated, activated and deactivated ReouteRecord arrays, the next step is an important part after path transformation, and a series of hook functions are executed.

Navigation guard

The official name is navigation guard, which is actually a series of hook functions executed when routing path switching occurs.

Let's take a look at the logic executed by these hook functions as a whole. First, construct a queue, which is actually an array;

Then define an iterator function iterator;

Finally, the runQueue method is executed to execute the queue.

Let's take a look at the definition of runQueue. In src/util/async.js:

export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => { 
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}

This is a very classic queue execution mode of asynchronous functions. Queue is an array of NavigationGuard type. We define the step function, take a guard from the queue according to the index each time, execute the fn function, and pass the guard as a parameter,

The second parameter is a function. When the function is executed, the step function will be executed recursively to advance to the next one,

Note that fn here is the iterator function just now, so let's go back to the definition of the iterator function:

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)
  }
}

The iterator function logic is very simple. It is to execute each navigation guard hook and pass in route, current and anonymous functions. These parameters correspond to to to, from and next in the document. When the anonymous function is executed, abort or next will be executed according to some conditions. Only when next is executed will it advance to the next navigation guard hook function, This is why the official document says that only the next method is executed to resolve the hook function.

Finally, let's look at how the queue is constructed:

const queue: Array<?NavigationGuard> = [].concat(
  extractLeaveGuards(deactivated),
  this.router.beforeHooks,
  extractUpdateHooks(updated),
  activated.map(m => m.beforeEnter),
  resolveAsyncComponents(activated)
)

The sequence is as follows:

  1. Call leave guard in the deactivated component.

  2. Call the global beforeEach guard.

  3. Call beforeRouteUpdate in the reused component

  4. Call beforeEnter in the active routing configuration.

  5. Resolve asynchronous routing components.

Next, let's introduce the implementation of these five steps.

The first step is to execute extractLeaveGuards(deactivated). Let's take a look at the definition of extractLeaveGuards:

function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

It internally calls the general method of extractGuards, which can extract guards of each stage from the RouteRecord array:

function extractGuards (
  records: Array<RouteRecord>,
  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)
}

The flatMapComponents method is used here to obtain all navigation from records. It is defined in src/util/resolve-components.js:

export function flatMapComponents (
  matched: Array<RouteRecord>,
  fn: Function
): Array<?Function> {
  return flatten(matched.map(m => {
    return Object.keys(m.components).map(key => fn(
      m.components[key],
      m.instances[key],
      m, key
    ))
  }))
}

export function flatten (arr: Array<any>): Array<any> {
  return Array.prototype.concat.apply([], arr)
}

The function of flatMapComponents is to return an array. The elements of the array get the key s of all components from matched, and then return the execution result of fn function. The function of flatten is to flatten a two-dimensional array into a one-dimensional array.

For the call of flatMapComponents in extractGuards, when executing each fn, get the navigation guard of the corresponding name in the component through extractGuard(def, name):

function extractGuard (
  def: Object | Function,
  key: string
): NavigationGuard | Array<NavigationGuard> {
  if (typeof def !== 'function') {
    def = _Vue.extend(def)
  }
  return def.options[key]
}

After obtaining the guard, the bind method will be called to bind the instance instance of the component to the guard as the context of function execution. The bind method corresponds to bindGuard:

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

Then, for extractLeaveGuards(deactivated), what is obtained is the beforeroutleave hook function defined in all deactivated components.

The second step is this.router.beforeHooks. The beforeEach method is defined in our vueroter class. In src/index.js:

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

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

When the user registers a global guard with router.beforeEach, a hook function will be added to router.beforeHooks, so this.router.beforeHooks obtains the global beforeEach guard registered by the user.

Step 3 implements extractUpdateHooks(updated). Let's take a look at the definition of extractUpdateHooks:

function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}

Similar to extractLeaveGuards(deactivated), extractUpdateHooks(updated) obtains the beforeRouteUpdate hook function defined in all reused components.

The fourth step is to execute activated.map (M = > m.beforeEnter), and obtain the beforeEnter function defined in the activated routing configuration.

The fifth step is to execute resolveAsyncComponents(activated) to parse asynchronous components. First, let's take a look at the definition of resolveAsyncComponents in src/util/resolve-components.js:

export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
  return (to, from, next) => {
    let hasAsync = false
    let pending = 0
    let error = null

    flatMapComponents(matched, (def, _, match, key) => {
      if (typeof def === 'function' && def.cid === undefined) {
        hasAsync = true
        pending++

        const resolve = once(resolvedDef => {
          if (isESModule(resolvedDef)) {
            resolvedDef = resolvedDef.default
          }
          def.resolved = typeof resolvedDef === 'function'
            ? resolvedDef
            : _Vue.extend(resolvedDef)
          match.components[key] = resolvedDef
          pending--
          if (pending <= 0) {
            next()
          }
        })

        const reject = once(reason => {
          const msg = `Failed to resolve async component ${key}: ${reason}`
          process.env.NODE_ENV !== 'production' && warn(false, msg)
          if (!error) {
            error = isError(reason)
              ? reason
              : new Error(msg)
            next(error)
          }
        })

        let res
        try {
          res = def(resolve, reject)
        } catch (e) {
          reject(e)
        }
        if (res) {
          if (typeof res.then === 'function') {
            res.then(resolve, reject)
          } else {
            const comp = res.component
            if (comp && typeof comp.then === 'function') {
              comp.then(resolve, reject)
            }
          }
        }
      }
    })

    if (!hasAsync) next()
  }
}

resolveAsyncComponents returns a navigation guard function with standard to, from, and next parameters. Its internal implementation is very simple. It uses the flatMapComponents method to obtain the definition of each component from matched. It is judged that if it is an asynchronous component, the asynchronous component loading logic will be executed. This is very similar to the analysis of Vue loading asynchronous components before. After loading successfully, match. Components [key] will be executed =Resolveddef puts the parsed asynchronous components on the corresponding components and executes the next function.

In this way, after the resolveAsyncComponents(activated) parses all the activated asynchronous components, we can get all the activated components this time. In this way, we have done some things after completing these five steps:

runQueue(queue, iterator, () => {
  const postEnterCbs = []
  const isValid = () => 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(() => {
        postEnterCbs.forEach(cb => { cb() })
      })
    }
  })
})
  1. Call beforeRouteEnter in the activated component.

  2. Call the global beforeResolve guard.

  3. Call the global afterEach hook.

For step 6, there are these related logic:

const postEnterCbs = []
const isValid = () => this.current === route
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)

function extractEnterGuards (
  activated: Array<RouteRecord>,
  cbs: Array<Function>,
  isValid: () => boolean
): Array<?Function> {
  return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
    return bindEnterGuard(guard, match, key, cbs, isValid)
  })
}

function bindEnterGuard (
  guard: NavigationGuard,
  match: RouteRecord,
  key: string,
  cbs: Array<Function>,
  isValid: () => boolean
): NavigationGuard {
  return function routeEnterGuard (to, from, next) {
    return guard(to, from, cb => {
      next(cb)
      if (typeof cb === 'function') {
        cbs.push(() => {
          poll(cb, match.instances, key, isValid)
        })
      }
    })
  }
}

function poll (
  cb: any,
  instances: Object,
  key: string,
  isValid: () => boolean
) {
  if (instances[key]) {
    cb(instances[key])
  } else if (isValid()) {
    setTimeout(() => {
      poll(cb, instances, key, isValid)
    }, 16)
  }
}

The implementation of the extractEnterGuards function also uses the extractGuards method to extract the beforeRouteEnter navigation hook function in the component, which is different from the previous bind method. The document specifically emphasizes that the component instance cannot be obtained in the beforeRouteEnter hook function, because the component instance has not been created before the guard is executed, but we can access the component instance by passing a callback to next. Execute the callback when the navigation is confirmed, and take the component instance as the parameter of the callback method:

beforeRouteEnter (to, from, next) {
  next(vm => {
    // Accessing component instances through 'vm'
  })
}

Let's see how this is achieved.

In the bindEnterGuard function, the routeEnterGuard function is returned, so when executing the hook function in the iterator, it is equivalent to executing the routeEnterGuard function, then the navigation guard function defined by us will be executed, and when this callback function is executed, first execute the next function to reverse the current navigation hook, Then collect the parameters of the callback function, which is also a callback function, in cbs, which is actually collected into the postEnterCbs defined outside, and then execute at the end:

if (this.router.app) {
  this.router.app.$nextTick(() => {
    postEnterCbs.forEach(cb => { cb() })
  })
}

After the root routing component is re rendered, the postEnterCbs is traversed to execute callbacks. When each callback is executed, the poll(cb, match.instances, key, isValid) method is actually executed. Considering that some routing components are covered, and the transition components may not get instances in some slow modes, a polling method is used to constantly judge, Until the component instance can be obtained, call cb and pass the component instance as a parameter, which is why we can get the component instance in the callback function.

The seventh step is to get this.router.resolveHooks, and
Similar to the acquisition of this.router.beforeHooks, the beforeResolve method is defined in our vueroter class:

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

When the user registers a global guard with router.beforeResolve, a hook function will be added to router.resolveHooks, so this.router.resolveHooks obtains the global beforeResolve guard registered by the user.

The eighth step is to execute this.updateRoute(route) method after onComplete(route) is finally executed:

updateRoute (route: Route) {
  const prev = this.current
  this.current = route
  this.cb && this.cb(route)
  this.router.afterHooks.forEach(hook => {
    hook && hook(route, prev)
  })
}

The afterEach method is also defined in our vueroter class:

afterEach (fn: Function): Function {
  return registerHook(this.afterHooks, fn)
}

When the user registers a global guard with router.afterEach, a hook function will be added to router.afterHooks, so this.router.afterHooks obtains the global afterHooks guard registered by the user.

So far, we have completed the execution analysis of all navigation guards. We know that in addition to executing these hook functions, route switching will change in two places, one is the url and the other is the component. Next, we introduce the implementation principles of these two blocks.

url

When we click router link, we will actually execute router.push, as follows:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.history.push(location, onComplete, onAbort)
}

this.history.push function is implemented by subclasses. The implementation of this function is slightly different in different modes. Let's take a look at the implementation of this function in hash mode, which is often used. In src/history/hash.js:

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)
}

The push function will first execute this.transitionTo for path switching. In the callback function after switching, execute the pushHash function:

function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

supportsPushState is defined in src/util/push-state.js:

export const supportsPushState = inBrowser && (function () {
  const ua = window.navigator.userAgent

  if (
    (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
    ua.indexOf('Mobile Safari') !== -1 &&
    ua.indexOf('Chrome') === -1 &&
    ua.indexOf('Windows Phone') === -1
  ) {
    return false
  }

  return window.history && 'pushState' in window.history
})()

If supported, obtain the current complete url and execute the pushState method:

export function pushState (url?: string, replace?: boolean) {
  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)
  }
}

pushState will call the pushState interface or replaceState interface of the browser's native history to update the browser's url address and push the current url into the history stack.

Then, in the initialization of history, a listener will be set to listen to the changes of 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)
      }
    })
  })
}

When you click the browser return button, if a url has been pushed into the history stack, the pop state event will be triggered, and then get the current hash to jump, and execute the transitionto method for path conversion.

When students use Vue router to develop projects, open the debugging page http://localhost:8080 The url will be automatically changed to http://localhost:8080/#/ , how is this done? Originally, when instantiating HashHistory, the constructor will execute the ensueslash() method:

function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

export function getHash (): string {
  // We can't use window.location.hash here because it's not
  // consistent across browsers - Firefox will pre-decode it!
  const href = window.location.href
  const index = href.indexOf('#')
  return index === -1 ? '' : href.slice(index + 1)
}

function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}

At this time, the path is empty, so replace hash ('/' + path) is executed, and then getUrl is executed internally. The calculated new url is http://localhost:8080/#/ Finally, the pushState(url, true) will be executed, which is why the url will change.

assembly

The final rendering of routing is inseparable from components. Vue router has a built-in < router View > component, which is defined in src/components/view.js.

export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    data.routerView = true
   
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

    let depth = 0
    let inactive = false
    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()
    }

    const component = cache[name] = matched.components[name]
   
    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 || !(key in component.props)) {
          attrs[key] = propsToPass[key]
          delete propsToPass[key]
        }
      }
    }

    return h(component, data, children)
  }
}

< router View > is a functional component, and its rendering also depends on the render function. So what component should < router View > render? First get the current path:

const route = parent.$route

As we analyzed before, in src/install.js, we defined $route on the prototype of Vue:

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

Then, when executing the router.init method on the vueroter instance, the following logic will be executed and defined in src/index.js:

history.listen(route => {
  this.apps.forEach((app) => {
    app._route = route
  })
})

The history.listen method is defined in src/history/base.js:

listen (cb: Function) {
  this.cb = cb
}

Then execute this.cb when updating route:

updateRoute (route: Route) {
  //. ..
  this.current = route
  this.cb && this.cb(route)
  // ...
}

That is, when we execute the transitionTo method and finally updateRoute, we will execute the callback, and then update the component instance saved by this.apps_ route value,

The characteristic of the instances saved in this.apps array is that the router configuration item is passed in during initialization. Generally, the scene array will only save the root Vue instance, because we passed in the router instance in new Vue.

$route is defined on Vue.prototype. Each component instance accesses the $route attribute, that is, it accesses the root instance_ Route, that is, the current route.

< router View > supports nesting. Return to the render function, which defines the concept of depth, which represents the nesting depth of < router View >. Each < router View > executes the following logic during rendering:

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

const matched = route.matched[depth]
// ...
const component = cache[name] = matched.components[name]

parent._routerRoot represents the root Vue instance, so this cycle is to find the root Vue instance from the parent node of the current < router View >, and in this process, if the parent node is also < router View >, it indicates that < router View > is nested, depth + +.

After traversing, find the corresponding RouteRecord according to the path and depth matched by the current line, and then find the rendered component.

In addition to finding the components that should be rendered, a method for registering routing instances is also defined:

data.registerRouteInstance = (vm, val) => {     
  const current = matched.instances[name]
  if (
    (val && current !== vm) ||
    (!val && current === vm)
  ) {
    matched.instances[name] = val
  }
}

The registerRouteInstance method is defined for vnode data. In src/install.js, we will call this method to register routing instances:

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)
  }
})

In the mixed beforeCreate hook function, the registerInstance method will be executed, and then the registerRouteInstance method defined in the render function will be executed, so as to assign the vm instance of the current component to matched.instances[name].

At the end of the render function, the corresponding component vonde is rendered according to the component:

return h(component, data, children)

So how does the component re render when we execute transitionTo to change the route? There is such a logic in the beforeCreate hook function we mixed in:

Vue.mixin({
  beforeCreate () {
    if (isDef(this.$options.router)) {
      Vue.util.defineReactive(this, '_route', this._router.history.current)
    }
    // ...
  }
})

Because we put the root Vue instance_ The route attribute is defined as responsive. We will access parent.$route every time < route View > executes the render function. For example, we will access this_ routerRoot._ Route triggers its getter, which is equivalent to that < route View > depends on it. Then, after executing transitionTo, modify the app_ During route, the setter is triggered again, so the < route View > render watcher will be notified to update and re render the component.

Vue router also has another built-in component < router link >,
It supports users to navigate (click) in applications with routing function. Specify the target address through the to attribute, and render it as a < a > tag with the correct link by default. You can generate other tags by configuring the tag attribute.

In addition, when the target route is successfully activated, the link element automatically sets a CSS class name representing the activation.

< router link > is better than the dead < a href = "..." > for the following reasons:

Whether HTML5 history mode or hash mode, its performance behavior is consistent. Therefore, when you want to switch the routing mode or use hash mode in IE9 degradation, there is no need to make any change.

In HTML5 history mode, router link will guard the click event so that the browser will not reload the page.

When you use the base option in HTML5 history mode, all to attributes do not need to be written (base path).

Next, let's analyze its implementation. Its definition is in src/components/link.js:

export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
    const router = this.$router
    const current = this.$route
    const { location, route, href } = router.resolve(this.to, current, this.append)

    const classes = {}
    const globalActiveClass = router.options.linkActiveClass
    const globalExactActiveClass = router.options.linkExactActiveClass
    const activeClassFallback = globalActiveClass == null
            ? 'router-link-active'
            : globalActiveClass
    const exactActiveClassFallback = globalExactActiveClass == null
            ? 'router-link-exact-active'
            : globalExactActiveClass
    const activeClass = this.activeClass == null
            ? activeClassFallback
            : this.activeClass
    const exactActiveClass = this.exactActiveClass == null
            ? exactActiveClassFallback
            : this.exactActiveClass
    const compareTarget = location.path
      ? createRoute(null, location, null, router)
      : route

    classes[exactActiveClass] = isSameRoute(current, compareTarget)
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget)

    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location)
        } else {
          router.push(location)
        }
      }
    }

    const on = { click: guardEvent }
    if (Array.isArray(this.event)) {
      this.event.forEach(e => { on[e] = handler })
    } else {
      on[this.event] = handler
    }

    const data: any = {
      class: classes
    }

    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      const a = findAnchor(this.$slots.default)
      if (a) {
        a.isStatic = false
        const extend = _Vue.util.extend
        const aData = a.data = extend({}, a.data)
        aData.on = on
        const aAttrs = a.data.attrs = extend({}, a.data.attrs)
        aAttrs.href = href
      } else {
        data.on = on
      }
    }

    return h(this.tag, data, this.$slots.default)
  }
}

The rendering of < router link > tags is also based on the render function, which first performs route analysis:

const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(this.to, current, this.append)

router.resolve is an instance method of vueroter, which is defined in src/index.js:

resolve (
  to: RawLocation,
  current?: Route,
  append?: boolean
): {
  location: Location,
  route: Route,
  href: string,
  normalizedTo: Location,
  resolved: Route
} {
  const location = normalizeLocation(
    to,
    current || this.history.current,
    append,
    this
  )
  const route = this.match(location, current)
  const fullPath = route.redirectedFrom || route.fullPath
  const base = this.history.base
  const href = createHref(base, fullPath, this.mode)
  return {
    location,
    route,
    href,
    normalizedTo: location,
    resolved: route
  }
}

function createHref (base: string, fullPath: string, mode) {
  var path = mode === 'hash' ? '#' + fullPath : fullPath
  return base ? cleanPath(base + '/' + path) : path
}

It first generates the target location according to the specification, then calculates the generated target path route through this.match method according to the location and match, and then calculates the final jump href through createHref method according to base, fullPath and this.mode.

After the router is parsed and the target location, route and href are obtained, the exactactivclass and activeClass will be processed. When exact is configured to true, exactactivclass will be added only when the target path and the current path completely match; When the target path contains the current path, activeClass will be added.

Then a guard function is created:

const handler = e => {
  if (guardEvent(e)) {
    if (this.replace) {
      router.replace(location)
    } else {
      router.push(location)
    }
  }
}

function guardEvent (e) {
  if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
  if (e.defaultPrevented) return
  if (e.button !== undefined && e.button !== 0) return 
  if (e.currentTarget && e.currentTarget.getAttribute) {
    const target = e.currentTarget.getAttribute('target')
    if (/\b_blank\b/i.test(target)) return
  }
  if (e.preventDefault) {
    e.preventDefault()
  }
  return true
}

const on = { click: guardEvent }
  if (Array.isArray(this.event)) {
    this.event.forEach(e => { on[e] = handler })
  } else {
    on[this.event] = handler
  }

Finally, it will listen for click events or other event types that can be passed in through prop, execute the hanlder function, and finally execute the router.push or router.replace functions, which are defined in src/index.js:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.history.push(location, onComplete, onAbort)
}

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
 this.history.replace(location, onComplete, onAbort)
}

In fact, the push and replace methods of history are executed for routing jump.

Finally, judge whether the current tag is a < a > tag, < router link > will be rendered as a < a > tag by default. Of course, we can also modify the prop of the tag to render to other nodes. In this case, we will try to find the < a > tag of its child elements. If so, bind the event to the < a > tag and add a href attribute, otherwise bind to the outer element itself.

summary

So far, we have analyzed the main process of the transitionTo of routing. Students can analyze other branches, such as redirection, alias, rolling behavior, etc.

Path change is the most important function in routing. We should remember the following:

The route will always maintain the current line. During route switching, the current line will be switched to the target line. During the switching process, a series of navigation guard hook functions will be executed, the url will be changed, and the corresponding components will also be rendered. After switching, the target line will be updated to replace the current line, which will be used as the basis for the next path switching.

Vue source code learning directory

Click back to the Vue source code to learn the complete directory
Vue router – GitHub warehouse address

Thank you for reading to the end~
We look forward to your attention, collection, comments and likes~
Let's become stronger together

Posted by steveclondon on Fri, 01 Oct 2021 13:33:50 -0700