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