Routing principle
Before analyzing the source code, let's understand the implementation principle of front-end routing. The implementation of front-end routing is actually very simple. Its essence is to monitor the change of URL, then match the routing rules, display the corresponding page, and there is no need to refresh. At present, there are only two ways to realize the routing used by a single page
- hash mode
- history mode
www.test.com / # / is the Hash URL. When # the following hash value changes, it will not request data from the server. You can monitor the change of URL through hashchange event to jump to the page.
History mode is a new feature of HTML5, which is more beautiful than Hash URL
Vueroter source code analysis
Route registration
Before we start, we recommend you to compare a copy of clone source code. Because of the long space, there are many jumps between functions.
Before using routing, you need to call Vue Use (vuerouter), which is because Vue can be used by plug-ins
export function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { // Determine whether the plug-in is installed repeatedly const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])) if (installedPlugins.indexOf(plugin) > -1) { return this } const args = toArray(arguments, 1) // Insert Vue args.unshift(this) // Generally, plug-ins have an install function // This function enables plug-ins to use Vue if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, args) } installedPlugins.push(plugin) return this } }
Next, let's look at the partial implementation of the install function
export function install (Vue) { // Ensure that install is called once if (install.installed && _Vue === Vue) return install.installed = true // Assign Vue to global variable _Vue = Vue const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { i(vm, callVal) } } // Mixed implementation of hook function for each component // It can be found that when the 'beforeCreate' hook is executed // The route is initialized Vue.mixin({ beforeCreate () { // Judge whether the component has a router object, which is only available on the root component if (isDef(this.$options.router)) { // Set the root route to itself this._routerRoot = this this._router = this.$options.router // Initialize route this._router.init(this) // Very important, for_ The route attribute implements bidirectional binding // Trigger component rendering Vue.util.defineReactive(this, '_route', this._router.history.current) } else { // Used for router view level judgment this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } registerInstance(this, this) }, destroyed () { registerInstance(this) } }) // Global registration components router link and router view Vue.component('RouterView', View) Vue.component('RouterLink', Link) }
For route registration, the core is to call Vue Use (VueRouter), so that VueRouter can use Vue. Then call the install function of vueroter through Vue. In this function, the core is to mix two routing components: hook function and global registration.
Vueroter instantiation
After installing the plug-in, instantiate vueroter.
const Home = { template: '<div>home</div>' } const Foo = { template: '<div>foo</div>' } const Bar = { template: '<div>bar</div>' } // 3. Create the router const router = new VueRouter({ mode: 'hash', base: __dirname, routes: [ { path: '/', component: Home }, // all paths are defined without the hash. { path: '/foo', component: Foo }, { path: '/bar', component: Bar } ] })
Take a look at the constructor of vueroter
constructor(options: RouterOptions = {}) { // ... // Route matching object this.matcher = createMatcher(options.routes || [], this) // Different routing methods are adopted according to the mode let mode = options.mode || 'hash' 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}`) } } }
In the process of instantiating vueroter, the core is to create a route matching object and adopt different routing methods according to the mode.
Create route matching object
export function createMatcher ( routes: Array<RouteConfig>, router: VueRouter ): Matcher { // Create routing mapping table const { pathList, pathMap, nameMap } = createRouteMap(routes) function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) } // Route matching function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { //... } return { match, addRoutes } }
The function of createMatcher is to create a routing mapping table, and then enable addRoutes and match functions to use several objects of the routing mapping table through closure, and finally return a Matcher object.
Next, let's look at how to create a mapping table when creating the createMatcher function
export function createRouteMap ( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord> ): { pathList: Array<string>; pathMap: Dictionary<RouteRecord>; nameMap: Dictionary<RouteRecord>; } { // Create mapping table const pathList: Array<string> = oldPathList || [] const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null) const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null) // Traverse the routing configuration and add routing records for each configuration routes.forEach(route => { addRouteRecord(pathList, pathMap, nameMap, route) }) // Make sure the wildcard is at the end for (let i = 0, l = pathList.length; i < l; i++) { if (pathList[i] === '*') { pathList.push(pathList.splice(i, 1)[0]) l-- i-- } } return { pathList, pathMap, nameMap } } // Add routing record function addRouteRecord ( pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) { // Get properties under routing configuration const { path, name } = route const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {} // Format url, replace/ const normalizedPath = normalizePath( path, parent, pathToRegexpOptions.strict ) // Generate record 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 (route.children) { // The children attribute of recursive routing configuration and add routing records route.children.forEach(child => { const childMatchAs = matchAs ? cleanPath(`${matchAs}/${child.path}`) : undefined addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs) }) } // If the route has an alias // Add routing records to the alias as well 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 ) }) } // Update mapping table if (!pathMap[record.path]) { pathList.push(record.path) pathMap[record.path] = record } // Add record to named route if (name) { if (!nameMap[name]) { nameMap[name] = record } else if (process.env.NODE_ENV !== 'production' && !matchAs) { warn( false, `Duplicate named routes definition: ` + `{ name: "${name}", path: "${record.path}" }` ) } } }
The above is the whole process of creating route matching objects. The corresponding route mapping table is created through the user configured route rules.
Route initialization
When the root component calls the beforeCreate hook function, the following code is executed
beforeCreate () { // Only the root component has the router attribute, so the route will be initialized when the root component is initialized if (isDef(this.$options.router)) { this._routerRoot = this this._router = this.$options.router this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } registerInstance(this, this) }
Next, let's see what route initialization will do
init(app: any /* Vue component instance */) { // Save component instance this.apps.push(app) // If the root component already exists, return if (this.app) { return } this.app = app // Assignment routing mode const history = this.history // Judge the routing mode, taking the hash mode as an example if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { // Add hashchange listener const setupHashListener = () => { history.setupListeners() } // Route jump history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } // The callback will be invoked in transitionTo. // Of components_ Assign a value to the route attribute to trigger component rendering history.listen(route => { this.apps.forEach(app => { app._route = route }) }) }
During route initialization, the core is to jump the route, change the URL, and then render the corresponding components. Next, let's take a look at how the route jumps.
Route jump
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { // Get matching routing information const route = this.router.match(location, this.current) // Confirm switching route this.confirmTransition(route, () => { // The following are callbacks for successful or failed route switching // Update the routing information and modify the components_ Assign a value to the route attribute to trigger component rendering // Call the hook function in afterHooks this.updateRoute(route) // Add hashchange listener onComplete && onComplete(route) // Update URL this.ensureURL() // Execute the ready callback only once if (!this.ready) { this.ready = true this.readyCbs.forEach(cb => { cb(route) }) } }, err => { // error handling if (onAbort) { onAbort(err) } if (err && !this.ready) { this.ready = true this.readyErrorCbs.forEach(cb => { cb(err) }) } }) }
In route jump, you need to get the matching route information first, so let's see how to get the matching route information first
function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { // Serialize url // For example, / ABC? foo=bar&baz=qux#hello // The serialization path is / abc // Hash to #hello // The parameters are foo: 'bar', baz: 'qux' const location = normalizeLocation(raw, currentRoute, false, router) const { name } = location // If it is a named route, judge whether there is the named route configuration in the record if (name) { const record = nameMap[name] // No route found indicating no match if (!record) return _createRoute(null, location) const paramNames = record.regex.keys .filter(key => !key.optional) .map(key => key.name) // Parameter processing if (typeof location.params !== 'object') { location.params = {} } if (currentRoute && typeof currentRoute.params === 'object') { for (const key in currentRoute.params) { if (!(key in location.params) && paramNames.indexOf(key) > -1) { location.params[key] = currentRoute.params[key] } } } if (record) { location.path = fillParams(record.path, location.params, `named route "${name}"`) return _createRoute(record, location, redirectedFrom) } } else if (location.path) { // Unnamed routing processing location.params = {} for (let i = 0; i < pathList.length; i++) { // find record const path = pathList[i] const record = pathMap[path] // If the route matches, the route is created if (matchRoute(record.regex, location.path, location.params)) { return _createRoute(record, location, redirectedFrom) } } } // No matching route return _createRoute(null, location) }
Next, let's look at how to create a route
// Create different routes according to conditions 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) } export function createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: ?Location, router?: VueRouter ): Route { const stringifyQuery = router && router.options.stringifyQuery // Clone parameters let query: any = location.query || {} try { query = clone(query) } catch (e) {} // Create routing object 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) } // Make the routing object unmodifiable return Object.freeze(route) } // Get the route record containing all nested path segments of the current route // Contains matching records from the root route to the current route, from top to bottom function formatMatch(record: ?RouteRecord): Array<RouteRecord> { const res = [] while (record) { res.unshift(record) record = record.parent } return res }
So far, the matching route has been completed. Let's return to the transitionTo function, and then execute confirmTransition
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { // Confirm switching route this.confirmTransition(route, () => {} } confirmTransition(route: Route, onComplete: Function, onAbort?: Function) { const current = this.current // Interrupt jump routing function 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 it is the same route, it will not jump if ( isSameRoute(route, current) && route.matched.length === current.matched.length ) { this.ensureURL() return abort() } // By comparing routes, analyze reusable components, components to be rendered and inactive components const { updated, deactivated, activated } = resolveQueue( this.current.matched, route.matched ) 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++) { // When the current routing path and the jump routing path are different, jump out of traversal if (current[i] !== next[i]) { break } } return { // Routes corresponding to reusable components updated: next.slice(0, i), // Route corresponding to the component to be rendered activated: next.slice(i), // Route corresponding to inactive components deactivated: current.slice(i) } } // Navigation guard array const queue: Array<?NavigationGuard> = [].concat( // Inactive component hook extractLeaveGuards(deactivated), // Global beforeEach hook this.router.beforeHooks, // Called when the current route changes but the component is reused extractUpdateHooks(updated), // Need render component enter guard hook activated.map(m => m.beforeEnter), // Parsing asynchronous routing components resolveAsyncComponents(activated) ) // Save route this.pending = route // Iterator, which is used to execute the navigation guard hook in the queue const iterator = (hook: NavigationGuard, next) => { // If the routes are not equal, the route will not jump if (this.pending !== route) { return abort() } try { // Execution hook hook(route, current, (to: any) => { // The next hook function will not be executed until next in the hook function is executed // Otherwise, the jump will be suspended // The following logic is used to determine the parameters passed in next() if (to === false || isError(to)) { // next(false) this.ensureURL(true) abort(to) } else if ( typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string')) ) { // next('/') or next({path: '/'}) - > redirect abort() if (typeof to === 'object' && to.replace) { this.replace(to) } else { this.push(to) } } else { // Execute next here // That is, execute the step(index + 1) in the following function runQueue next(to) } }) } catch (e) { abort(e) } } // Classic synchronous execution asynchronous function runQueue(queue, iterator, () => { const postEnterCbs = [] const isValid = () => this.current === route // When all asynchronous components are loaded, the callback here will be executed, that is, cb() in runQueue // Next, execute the navigation guard hook that needs to render the component const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) const queue = enterGuards.concat(this.router.resolveHooks) runQueue(queue, iterator, () => { // Jump complete if (this.pending !== route) { return abort() } this.pending = null onComplete(route) if (this.router.app) { this.router.app.$nextTick(() => { postEnterCbs.forEach(cb => { cb() }) }) } }) }) } export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) { const step = index => { // After all the functions in the queue are executed, the callback function is executed if (index >= queue.length) { cb() } else { if (queue[index]) { // The iterator is executed, and the user executes the next() callback in the hook function // If there is no problem, execute next(), that is, the second parameter in fn function fn(queue[index], () => { step(index + 1) }) } else { step(index + 1) } } } // Get the first hook function in the queue step(0) }
Next, the navigation guard is introduced
const queue: Array<?NavigationGuard> = [].concat( // Inactive component hook extractLeaveGuards(deactivated), // Global beforeEach hook this.router.beforeHooks, // Called when the current route changes but the component is reused extractUpdateHooks(updated), // Need render component enter guard hook activated.map(m => m.beforeEnter), // Parsing asynchronous routing components resolveAsyncComponents(activated) )
The first step is to execute the hook function of the inactivated component first
function extractLeaveGuards(deactivated: Array<RouteRecord>): Array<?Function> { // Pass in the name of the hook function to be executed return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true) } function extractGuards( records: Array<RouteRecord>, name: string, bind: Function, reverse?: boolean ): Array<?Function> { const guards = flatMapComponents(records, (def, instance, match, key) => { // Find the corresponding hook function in the component const guard = extractGuard(def, name) if (guard) { // Add a context object to each hook function for the component itself return Array.isArray(guard) ? guard.map(guard => bind(guard, instance, match, key)) : bind(guard, instance, match, key) } }) // Reduce the dimension of the array and judge whether to flip the array // Because some hook functions need to be executed from the child to the parent return flatten(reverse ? guards.reverse() : guards) } export function flatMapComponents ( matched: Array<RouteRecord>, fn: Function ): Array<?Function> { // Array dimensionality reduction return flatten(matched.map(m => { // Pass the object in the component into the callback function to obtain the hook function array return Object.keys(m.components).map(key => fn( m.components[key], m.instances[key], m, key )) })) }
Step 2: execute the global beforeEach hook function
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) } }
There is the above code in the vueroter class. Whenever the beforeEach function is added to the vueroter instance, the function will be push ed into beforeHooks.
The third step is to execute the beforeRouteUpdate hook function. The calling method is the same as that in the first step, except that the passed in function name is different. This object can be accessed in this function.
Step 4: execute the beforeEnter hook function, which is the exclusive hook function for routing.
The fifth step is to parse asynchronous components.
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function { return (to, from, next) => { let hasAsync = false let pending = 0 let error = null // The function has been introduced before flatMapComponents(matched, (def, _, match, key) => { // Determine whether it is an asynchronous component if (typeof def === 'function' && def.cid === undefined) { hasAsync = true pending++ // Successful callback // The once function ensures that the asynchronous component is loaded only once const resolve = once(resolvedDef => { if (isESModule(resolvedDef)) { resolvedDef = resolvedDef.default } // Determine whether it is a constructor // If not, generate the component constructor through Vue def.resolved = typeof resolvedDef === 'function' ? resolvedDef : _Vue.extend(resolvedDef) // Assignment component // If all components are resolved, continue to the next step match.components[key] = resolvedDef pending-- if (pending <= 0) { next() } }) // Failed callback 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 { // Executing asynchronous component functions res = def(resolve, reject) } catch (e) { reject(e) } if (res) { // Download complete execution callback if (typeof res.then === 'function') { res.then(resolve, reject) } else { const comp = res.component if (comp && typeof comp.then === 'function') { comp.then(resolve, reject) } } } } }) // Not an asynchronous component, go to the next step directly if (!hasAsync) next() } }
The above is the logic in the first runQueue. After the fifth step is completed, the callback function in the first runQueue will be executed
// This callback is used to save the callback function in the 'beforeRouteEnter' hook const postEnterCbs = [] const isValid = () => this.current === route // beforeRouteEnter navigation guard hook const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) // beforeResolve navigation guard hook const queue = enterGuards.concat(this.router.resolveHooks) runQueue(queue, iterator, () => { if (this.pending !== route) { return abort() } this.pending = null // After each navigation guard hook will be executed here onComplete(route) if (this.router.app) { this.router.app.$nextTick(() => { postEnterCbs.forEach(cb => { cb() }) }) } })
The sixth step is to execute the beforeRouteEnter navigation guard hook. The beforeRouteEnter hook cannot access this object because the hook is called before navigation confirmation, and the components to be rendered have not been created. However, this hook function is the only one that supports obtaining this object in the callback, which will be executed after route confirmation.
beforeRouteEnter (to, from, next) { next(vm => { // Access component instances through 'vm' }) }
Let's take a look at how to get this object in the callback
function extractEnterGuards( activated: Array<RouteRecord>, cbs: Array<Function>, isValid: () => boolean ): Array<?Function> { // This is basically the same as before calling the navigation guard. 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 => { // Judge whether cb is a function // If yes, push into postEnterCbs next(cb) if (typeof cb === 'function') { cbs.push(() => { // Loop until you get the component instance poll(cb, match.instances, key, isValid) }) } }) } } // This function is used to solve the problem of issus #750 // When the router view is wrapped with the transition component with mode out in // When the component navigates for the first time, the component instance object cannot be obtained function poll( cb: any, // somehow flow cannot infer this is a function instances: Object, key: string, isValid: () => boolean ) { if ( instances[key] && !instances[key]._isBeingDestroyed // do not reuse being destroyed instance ) { cb(instances[key]) } else if (isValid()) { // setTimeout 16ms is basically the same as nextTick setTimeout(() => { poll(cb, instances, key, isValid) }, 16) } }
The seventh step is to execute the beforeResolve navigation guard hook. If the global beforeResolve hook is registered, it will be executed here.
The eighth step is to confirm the navigation and call the afterEach navigation guard hook.
After the above execution is completed, the rendering of components will be triggered
history.listen(route => { this.apps.forEach(app => { app._route = route }) })
The callback will be invoked in updateRoute.
updateRoute(route: Route) { const prev = this.current this.current = route this.cb && this.cb(route) this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) }
So far, the route jump has been fully analyzed. The core is to judge whether the route to jump exists in the record, then execute various navigation guard functions, and finally complete the URL change and component rendering.