preface

The Vue routing component cache problem has been solved and the project has been properly optimized, but the source code of vue-Router has not been explored. This article will continue the topic of last time. In-depth analysis of vue-Router source code for navigation guard, dynamic parameter matching, transition effect and asynchronous components, etc.

The source code for this article is [email protected], [email protected]

Great oaks from little acorns grow

The basic idea of vue-router is to generate a VueRouter instance from the route record and pass it into the router attribute of the APP instance of Vue. At the same time, router-view is used to mount the routing component matching the route to a certain position on the page.

const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

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

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

const app = new Vue({
  router
}).$mount('#app')
Copy the code

Using the process

The design philosophy of Vue-Router is different from that of React-RouterV4. The former uses route configuration to configure routes in a unified manner, while the latter uses a route-as-component concept (no unified route configuration is required, but it is encapsulated as a route configuration).

Core features

These are the core functions provided by vue-Router. Please refer to the official documentation for complete usage instructions. The following will step by step analyze how vue-Router source code implements these core functions.

Preconditions for reading the source code

Source directory structure

Source code structure is adhering to the VUE series of clear features, mainly divided into components link and view, maintenance route history, VUE plug-in registration method install.js, module export file index.js

Basic concepts – Routing instance Router

Router is an instance object that is generated when vue-Router is used by passing in routing records and other configurations. The emphasis is on the implementation of VueRouter.

The implementation of the init method

The init method here is most associated with the global mixins registered in install.js, and is the initial routing method performed by the Vue component when creating, which needs to be analyzed.

init (app: any /* Vue component instance */) {
  this.apps.push(app)

  app.$once('hook:destroyed', () = > {const index = this.apps.indexOf(app)
    if (index > - 1) this.apps.splice(index, 1)
    if (this.app === app) this.app = this.apps[0] | |null
  })

  if (this.app) {
    return
  }

  this.app = app

  const history = this.history

  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation())
  } else if (history instanceof HashHistory) {
    const setupHashListener = (a)= > {
      history.setupListeners()
    }
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener,
      setupHashListener
    )
  }

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

Source: L83

$once(‘hook: Destroyed ‘, () => {} declarantly registers the component’s destroyed lifecycle hook to ensure that the component app instance is removed from router.apps when the component is destroyed.

Ensure that the route is initialized only once: since init is called by a globally registered mixin, the route is initialized only once on the root component < app /> by the existence of this.app logic.

Trigger route changes & Start route listening: Use history.transitionTo routing mode to trigger route changes and use history.listen to listen for route changes to update the root component instance app._route is the current route to jump to.

Basic concept – Routing matcher

The routing matcher macther is an object generated by creation-matcher, which internally converts the routing records passed into VueRouter class, and externally provides routing methods based on location — match, and registered routing methods — addRoutes.

  • Match method: Matches the route object corresponding to location based on the internal route mapping
  • AddRoutes method: Adds the route record to the route map of the Matcher instance

Generate the matcher

// src/index.js
constructor (options: RouterOptions = {}) {
    ...
    this.matcher = createMatcher(options.routes || [], this)... }Copy the code

Source: L42

Options. routes Indicates the incoming route record for the new VueRoute operation

CreateMatcher internal

CreateMatcher comes from import {createMatcher} from ‘./create-matcher’. It is used to convert routing addresses to routing objects, map routing records, and process routing parameters

// src/create-matcher.js
export function createMatcher (routes: Array
       
        , router: VueRouter
       ) :Matcher {... function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) }function match (raw: RawLocation, currentRoute? : Route, redirectedFrom? : Location) :Route {... }function _createRoute (record: ? RouteRecord, location: Location, redirectedFrom? : Location) :Route {... }return {
    match,
    addRoutes
  }
}
Copy the code

Source: L16

CreateRoute: converts the external route record into a unified route object. The $route passed into the component instance is the returned alias: processes the route alias nameMap: processes named routes route parameter parsing: Parsing route location.params, query parameters, hash, etc., where dynamic route matching comes from

Dynamic routing matching and nested routines

Dynamic Route Matching

Dynamic route matching refers to the ability to set multiple parameters in a path. The parameters will be set to $route.params, for example:

model Matching path $route.params
/user/:username /user/evan { username: ‘evan’ }
/user/:username/post/:post_id /user/evan/post/123 { username: ‘evan’, post_id: ‘123’ }

Reference: official website example

Embedded routines by

Nested routing refers to that routes can be nested like components. A routing record can nest an array of multiple child routing records through the children property, for example:

const router = new VueRouter({
  routes: [{path: '/user/:id'.component: User,
      children: [{// If /user/:id/profile matches successfully,
          // The UserProfile will be rendered in User's 
      
          path: 'profile'.component: UserProfile
        },
        {
          // when /user/:id/posts matches successfully
          // UserPosts will be rendered in User's 
      
          path: 'posts'.component: UserPosts
        }
      ]
    }
  ]
})
Copy the code

Reference: official website example

As long as vue-router is used in the project, it is almost inevitable to use dynamic routing matching and nested routines. It can be seen that two functions are very important in vue-Router, and these two functions must be studied when studying its source code. Below will explore how the above functions are implemented in Vue-Router.

Main implementation ideas

To achieve dynamic route matching, the path attribute of a route record is matched with the parameters of the actual routing path. To achieve nested routing, route records are resolved according to nested rules. Both of these are implemented in create-route-map.

The core code in create-route-map is as follows:

export function createRouteMap (routes: Array
       
        , oldPathList? : Array
        
         , oldPathMap? : Dictionary
         
          , oldNameMap? : Dictionary
          
         
        
       ) :{
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
 	...
 	routes.forEach(route= > {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })
  ...
  / * * *TODO:* Priority of routing: Which sorting algorithm is used to move routes represented by wildcards * to the end of routing records *? * /
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === The '*') {
      pathList.push(pathList.splice(i, 1) [0])
      l--
      i--
    }
  }
  ...
  / * * *TODO:* Route record, map all route records to pathMap, nameMap, pathMap: by path mapping, nameMap: by name mapping, pathList all route path array * processing nested routine by: Call this method recursively, parent means the parent route * Handles route aliases: the path alias is treated as a route record pointing to the same component, and this method processes the route composed of that alias * Handles route names: if a route name exists, the route is mapped to nameMap for storage */
  function addRouteRecord (pathList: Array
       
        , pathMap: Dictionary
        
         , nameMap: Dictionary
         
          , route: RouteConfig, parent? : RouteRecord, matchAs? : string
         
        
       ) {... }... return { pathList, pathMap, nameMap } }Copy the code

Source: about

The createRouteMap method mainly iterates the routes configuration and calls the addRouteRecord method to process the routes. After processing the routes, the pathList pathMap nameMap is obtained, which is formed into an object and returned.

Implementation of dynamic routing matching

In the implementation of addRouteRecord method to process routes, route.path is converted into a regular expression using path-to-regexp. Record is the value saved in the pahtMap nameMap map after processing.

constrecord: RouteRecord = { ... regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), ... }.../ * * *TODO:* Call path-to-regexp to generate the regex */ for route matching
function compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptions) :RouteRegExp {
  const regex = Regexp(path, [], pathToRegexpOptions)
  if(process.env.NODE_ENV ! = ='production') {
    const keys: any = Object.create(null)
    regex.keys.forEach(key= >{ warn( ! keys[key.name],`Duplicate param keys in route with path: "${path}"`
      )
      keys[key.name] = true})}return regex
}
Copy the code

Source: L178

Then, routes are matched according to route.name and route.path in the match method provided by Create-Matcher. During the match, the obtained regular expression is called to perform route matching and parameter parsing, so as to obtain routes matching path or route name and dynamic parameters.

Nested routines by the implementation

AddRouteRecord method to implement nested routines by part of the source code is as follows:

export function createRouteMap (routes: Array
       
        , oldPathList? : Array
        
         , oldPathMap? : Dictionary
         
          , oldNameMap? : Dictionary
          
         
        
       ) :{
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
 	...
  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

Source: L102

Children of the routing record represents the nested routing record under the current route. When it exists, the route is recursively processed. When child routes are processed, the complete route path is merged into the pathMap nameMap.

Therefore, no matter whether nested routines are used or not, the rated route mapping is performed at last, and the unified route match method is used for route matching.

Navigation guard mechanism

The navigation guard passes a custom hook method to the route user to control the jump logic of the route, and performs the matching logic of the next route through the next method. According to the position of the navigation guard, it can be divided into three categories: global navigation guard, route exclusive guard and component internal navigation guard.

Navigation guard registration

Use the registered navigation guard method or navigation guard configuration will be registered in the execution queue, in the route jump according to the route configuration mapping to calculate the component instance update, replacement, reuse, and so on, and then on the corresponding component traversal execution navigation guard queue.

Register the global navigation Guard

Global navigational guards are classified into global pre-navigational guards, global resolution guards, and global post-navigational guards. They are registered by router.beforeeach router.beforeresolve router.aftereach.

// src/index.js
beforeEach (fn: Function) :Function {
  return registerHook(this.beforeHooks, fn)
}

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

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

Source: L133

Registering a global navigation guard calls the registerHook method to push the hook function into the queue and return a method to delete the hook function.

This is a common queue on the stack out of the stack usage, vue source code is a very common use

The registerHook method is as follows:

// src/index.js
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

Registered route exclusive guard

Route exclusive guards are registered in the form of route configurations, such as:

const router = new VueRouter({
  routes: [{path: '/foo'.component: Foo,
      beforeEnter: (to, from, next) = > {
        // ...}}}])Copy the code

Register component internal guard

Component internal guards are registered by configuring the component’s navigational guard property, for example:

const Foo = {
  template: `... `,
  beforeRouteEnter (to, from, next) {
    // called before the corresponding route to render the component is confirmed
    / / no! Can!!!! Get component instance 'this'
    // Because the component instance has not been created before the guard executes
  },
  beforeRouteUpdate (to, from, next) {
    // Called when the current route changes but the component is being reused
    // For example, for a path /foo/:id with dynamic parameters, when jumping between /foo/1 and /foo/2,
    // Since the same Foo component will be rendered, the component instance will be reused. And the hook will be called in that case.
    // Access component instance 'this'
  },
  beforeRouteLeave (to, from, next) {
    // called when navigating away from the component's corresponding route
    // Access component instance 'this'}}Copy the code

Navigation guard parsing flow

Resolve the navigational guards registered globally, route configuration, and component internals and push them into the queue in the order of navigation hook resolution

const queue: Array<? NavigationGuard> = [].concat(// in-component leave guards
  extractLeaveGuards(deactivated),  // beforeRouterLeave of the failed component
  // global before hooks
  this.router.beforeHooks,					// Global front hook beforeEach
  // in-component update hooks
  extractUpdateHooks(updated),			// Reuse the component beforeRouteUpdate
  // in-config enter guards
  activated.map(m= > m.beforeEnter),// beforeRouteEnter for the route configuration
  // async components
  resolveAsyncComponents(activated) // Load resolution of asynchronous components in routing configuration
)
Copy the code

Source: L133

Navigate the hook resolution flow

The navigation hooks parse the corresponding source code

// Perform the front guard
runQueue(queue, iterator, () => {
  const postEnterCbs = []
  const isValid = (a)= > this.current === route
  const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
  const queue = enterGuards.concat(this.router.resolveHooks)
  // Perform parse guard
  runQueue(queue, iterator, () => {
    if (this.pending ! == route) {return abort()
    }
    this.pending = null
    onComplete(route)
    if (this.router.app) {
      this.router.app.$nextTick((a)= > {
        // Execute the rear guard
        postEnterCbs.forEach(cb= > {
          cb()
        })
      })
    }
  })
})
Copy the code

Source: L179

Route lazy loading

Route lazy loading is the subcontracting of page codes based on routes. When the corresponding routes are matched, the codes of the corresponding routing components are downloaded asynchronously. The code-splitting function of Webpack can be used directly for new projects created with VUE-CLI. An example of route lazy loading with the new vUE asynchronous component +ES syntax is as follows:

vue VueRouter({
	routes: [{path: '/foot'.component: (a)= > import('./my-async-component')}]})Copy the code

The loading state of asynchronous components needs to be paid attention to. In VUe-Router, the loading state of asynchronous components is only loading and error, while in Vue-Router, lazy loading of components is reimplemented to realize more detailed control of component loading state and route resolution.

Vue-router asynchronous component resolution in the navigation guard queue resolution process, which focuses on the analysis of asynchronous components of the source code is as follows:

// util/resolve-components.js
/ * * *TODO:* Asynchronous route resolution: Rewrite the resolve and reject methods of asynchronous components, add component loading state control and route resolution control; Compatibility with traditional writing of asynchronous components and promise writing
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++
        // Override the resolve and reject methods of vUE asynchronous components
        const resolve = once(resolvedDef= > {
          if (isESModule(resolvedDef)) {
            resolvedDef = resolvedDef.default
          }
          // save resolved on async factory in case it's used elsewhere
          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)
        }
        // Promise for compatible asynchronous components
        if (res) {
          if (typeof res.then === 'function') {
            res.then(resolve, reject)
          } else {
            // new syntax in Vue 2.3
            const comp = res.component
            if (comp && typeof comp.then === 'function') {
              comp.then(resolve, reject)
            }
          }
        }
      }
    })
  }
}
Copy the code

Source: L6

The resolve and Reject methods loaded by vUE asynchronous components are rewritten to control whether route resolution enters the next match, and the exception handling of route resolution failure is added. At the same time, the promise writing method of asynchronous components is also compatible.

The router – the view components

Router-view is one of the two core components provided by VUe-Router. It is a functional component that does not have its own component instance and is only responsible for calling keepAlive $route.match and other related attributes/methods stored on the parent component to control the rendering of the component corresponding to the route.

Router-view components can be nested to implement nesting routines, and their own page location is ultimately where the routing components they match are mounted.

The core source code of the Render part of the source code is as follows:

render (_, { props, children, parent, data }) {
  // Identifies the current component as router-view
  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
  // The router-view component is traversed up to the following component. If other router-view components are encountered, the route depth is +1
  // vnodeData.keepAlivepj
  while(parent && parent._routerRoot ! == parent) {const vnodeData = parent.$vnode ? parent.$vnode.data : {}
    if (vnodeData.routerView) {
      depth++
    }
    if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
      inactive = true
    }
    parent = parent.$parent
  }
  data.routerViewDepth = depth

	// Enable caching
  if (inactive) {
    const cachedData = cache[name]
    const cachedComponent = cachedData && cachedData.component
    if (cachedComponent) {
      if (cachedData.configProps) {
        fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
      }
      return h(cachedComponent, data, children)
    } else {
      return h()
    }
  }

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

  cache[name] = { component }

  // Register the registerRouteInstance method with the parent component
  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 } } ... return h(component, data, children) }Copy the code

Source: L13

Route cache judgment

Parent indicates the direct parent component instance of the router-View component. When the router-View traverses the outer component, it indicates that there is a nested route and the route depth is +1. If the condition is met, it indicates that the cache is enabled on the route.

The following structure uses the route cache

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

The cached route component instance is stored on the parent component instance. If route caching is enabled, the matched route component in the parent cache is used for rendering. If not, $route.match is used to match the matched route in matcher for rendering.

** Parent._inactive** Updated by vue core module’s Observer /scheduler ** Parent._directInactive** updated by Vue core module’s instance/lifecycle, Both are used to identify whether the current component is active. See issue#1212 for the difference.

The router – link component

Router-link is one of the two core components provided by Vue-Router. It is a common component, which internally cances the default jump behavior of the A label, and controls the compatibility of the component with the control and meta keys. It provides the style class of the current activated route matching.

To is used to determine the target route to click the event jump, and append Replace and other attributes are used to change the behavior of default route jump.

Distribute content through slot

const scopedSlot =
  !this.$scopedSlots.$hasNormal &&
  this.$scopedSlots.default &&
  this.$scopedSlots.default({
    href,
    route,
    navigate: handler,
    isActive: classes[activeClass],
    isExactActive: classes[exactActiveClass]
  })

if (scopedSlot) {
  if (scopedSlot.length === 1) {
    return scopedSlot[0]}else if (scopedSlot.length > 1| |! scopedSlot.length) {if(process.env.NODE_ENV ! = ='production') {
      warn(
        false.`RouterLink with to="The ${this.to
        }" is trying to use a scoped slot but it didn't provide exactly one child. Wrapping the content with a span element.`)}return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
  }
}
Copy the code

Source: L91

Unified handling of click event compatibility

function guardEvent (e) {
  // don't redirect with control keys
  if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
  // don't redirect when preventDefault called
  if (e.defaultPrevented) return
  // don't redirect on right click
  if(e.button ! = =undefined&& e.button ! = =0) return
  // don't redirect if `target="_blank"`
  if (e.currentTarget && e.currentTarget.getAttribute) {
    const target = e.currentTarget.getAttribute('target')
    if (/\b_blank\b/i.test(target)) return
  }
  // this may be a Weex event which doesn't have this method
  if (e.preventDefault) {
    e.preventDefault()
  }
  return true
}
Copy the code

Source: L158

Find rendered A tags

Recursively find the A tag in children as the default replacement for the component’s default slot

function findAnchor (children) {
  if (children) {
    let child
    for (let i = 0; i < children.length; i++) {
      child = children[i]
      if (child.tag === 'a') {
        return child
      }
      if (child.children && (child = findAnchor(child.children))) {
        return child
      }
    }
  }
}
Copy the code

Source: L177

conclusion

After all the above analysis, the realization of the core features of VUe-Router has been basically analyzed and completed. Due to the current author level is limited, part of the source code analysis is not thorough enough, such as: Router-view source code related to the vUE core part, and even some omissions or errors, but also invite readers to correct.

It took me almost a week to write this article, and it is already longer than I expected. If you stick to it, you should at least give yourself a thumbs-up.

If this article is a little help to you, please give a thumbs-up to encourage the author, after all, the original is not easy 🙂

Starting to finch: www.yuque.com/johniexu/fr…

The author’s blog address is blog.lessing.online/

Author: github github.com/johniexu