Based on review of

The last article has introduced the overall operation process of VUE-Router in detail. If you want to know the specific content of the last article, you can click here. The previous post left the navigation guard (commonly known as the hook) unanalyzed. This article will focus on the implementation principle of navigation guard.

Before we look at the source code, let’s do a basic review of the Navguard API, which the source code does most of the work to implement.

Explain its use using the global front-guard router. BeforeEach as an example, and the other navigational guards in turn.

The global front-guard uses a lot of API in practice. Router.beforeeach wrapped function is executed every jump to the page (code below).

To is the destination route object to enter, and from is the route the current navigation is leaving.

The next function is very powerful. It can decide whether to pass to the next level of guard or truncate to jump directly to another page.

  • Next (false) or next(error) interrupts current navigation.

  • Next ({path: ‘/’}) or next({path: ‘/’, replace: true}) means to jump or redirect to the path path.

  • Next () indicates that the current navigator releases and goes to the next hook.

const router = new VueRouter({ ... }) router.beforeEach((to, from, next) => { // ... next({ path: '/login', replace: true }); // Redirect to login page})Copy the code

The other main navigational guard parameters are also made up of to, from, and next, and are used the same as router.beforeeach, but at different times.

  • BeforeRouteLeave: The beforeRouteLeave hook defined within a page component fires when the navigation is about to leave the component.

  • BeforeRouteUpdate: For a path /foo/:id with dynamic parameters, a jump between /foo/1 and /foo/2 triggers the beforeRouteUpdate hook defined in the foo component because the same foo component is rendered.

  • BeforeEnter: Hook function added in the developer-written routing configuration. It is executed before entering a page component.

  • Const Foo = () => import(/* webpackChunkName: “group-foo” */ ‘./ foo.vue ‘).

  • BeforeRouteEnter: This is executed before entering a page component and is defined within the component in addition to the timing of execution compared to beforeEnter guards.

These navigational guards are executed in the following order. A call to next() within the previous navigation guard function triggers the next navigation guard to continue execution.

BeforeRouteLeave // Call in deactivated components beforeEach // Global define beforeRouteUpdate // call in reusable components beforeEnter // Call parsing asynchronous routing components in routing configuration BeforeRouteEnter // called in the active componentCopy the code

Call scenario

As described in the previous article, vue-Router ultimately performs all jumps by calling the following transitionTo function.

Location is the jump path, and onComplete and onAbort represent successful or failed jump callback functions, respectively.

ConfirmTransition this. ConfirmTransition contains the navigational guard’s logic. The navigational guard controls each layer of the jump path and only executes the second parameter of this.

History.prototype.transitionTo = function transitionTo ( location, onComplete, onAbort ) { var this$1 = this; var route = this.router.match(location, this.current); this.confirmTransition( route, function () { this$1.updateRoute(route); onComplete && onComplete(route); . }, function (err) { if (onAbort) { onAbort(err); }... }); };Copy the code

We can start by trying to guess how this. ConfirmTransition blocks layers of access routes.

The basic concept of a navigational guard is already known, and it is a dev-defined interceptor function. Now assume that we have collected all of our defined navigators into a Queue array. So how do we implement a navigation guard that runs next() to jump to the next navigation hook?

Next to realize

BeforeEach was introduced above, this time we want to explore how next is implemented in the function (code below).

  • The next() function, if nothing is passed, lets go directly to the next navigation hook.

  • If next fills in an object that contains only the path property, the navigation pushes to that path. If the object has replace:true in addition to path, the navigation redirects to the path path.

  • Finally, when next(false) passes a false or Error instance, the navigation terminates the jump and the subsequent hook chain stops executing.

router.beforeEach((to, from, next) => { if (! User_info) {// No login jump to login page next({path:"/login"}); } else {// next(); }})Copy the code

Next () will handle different types of arguments internally. Let’s take a look at how the source code handles parameters (see below).

When next is executed, the following anonymous functions are called: to corresponds to each of the above three cases. Abort the jump if to is false or Error. If to is an object object or string, the jump is performed using push or replace. If to is empty, execute next.

function (to) { if (to === false || isError(to)) { abort(to); / / terminate the jump} else if (typeof to = = = 'string' | | (typeof to = = = 'object' && (typeof to. The path = = = 'string' | | typeof to.name === 'string')) ) { abort(); if (typeof to === 'object' && to.replace) { this$1.replace(to); } else { this$1.push(to); } } else { // confirm transition and pass on the value next(to); }Copy the code

We can see that in this anonymous function, the last else also contains a next, which is the next call that actually triggers the next navigation hook.

The question now is what kind of mechanism should be designed to satisfy such a chain call. The first step is to collect all the navigation hooks defined by the developer and store them in an array in order of execution. For example queue = [fn1,fn2,fn3,fn4]. Assume that fn1 corresponds to the beforeRouteLeave hook and fn2 corresponds to the beforeEach hook.

Then set the initial index index = 0 and let the array perform the index fetch function. Why does calling next trigger fn2 execution in fN1? That’s because when index = 0 executes fn1, the execution mechanism of queue [index+1] is wrapped as a function parameter and passed into fn1 to control it. So an internal call to next by FN1 triggers the execution of fn2. The source code implementation is as follows.

function runQueue (queue, fn, cb) { var step = function (index) { if (index >= queue.length) { cb(); } else { if (queue[index]) { fn(queue[index], function () { step(index + 1); }); } else { step(index + 1); }}}; step(0); }Copy the code

Queue stores all navigational hook functions, and the first function to fetch the queue is put into fn for execution. An anonymous function is placed in fn’s second argument. This anonymous function is the reason why next() in the navigational guard finally triggers the next navigational hook execution.

Fn is the iterator function below.

The hook argument corresponds to the hook function fetched from the queue above, and the hook is called with three arguments: route is the next routing object,current is the current routing object, and the third function is the anonymous function we described above for the to argument.

Assume a hook corresponds to a global front-guard router. BeforeEach ((to, from, next) => {… }, the three parameters to,from, and next of the route guard are passed in by the following route,current, and anonymous functions, respectively.

When router.beforeEach next executes, the anonymous function of the third hook argument executes. The anonymous function evaluates to the data type of to, and when to is null, the anonymous function executes the second argument to the iterator, next.

Function () {step(index + 1)} is passed in as an argument from runQueue. The next navigation hook is triggered.

var iterator = function (hook, next) { if (this$1.pending ! == route) { return abort() } try { hook(route, current, function (to) { if (to === false || isError(to)) { 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$1.replace(to); } else { this$1.push(to); } } else { next(to); }}); } catch (e) { abort(e); }};Copy the code

The whole process

Now let’s review the entire execution process. Navigation To perform a jump call transitionTo(location), and this.router.match converts the jump path location to the route object to be jumped.

ConfirmTransition this. ConfirmTransition starts with the navigational guard’s logic and only enters the successful callback function if all navigational guards allow access to route objects.

History.prototype.transitionTo = function transitionTo ( location, onComplete, onAbort ) { var route = this.router.match(location, this.current); This. ConfirmTransition (route, function () {// successful callback}, function (err) {// failed callback}); };Copy the code

ConfirmTransition this. ConfirmTransition collects all navigation hooks into a queue in the order they are executed, and prepares route and current.

Route is the argument passed in by this. ConfirmTransition, and current is retrieved from the History instance, which stores the current route object.

History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) { var current = this.current; . }Copy the code

When the application is initialized, current is given a fixed value (as shown below) and path points to the root path. The route object to jump is assigned to the current route object only when the navigation successfully calls the above onComplete function.

current = { 
   fullPath: "/",
   hash: "",
   matched: [],
   meta: {},
   name: null,
   params: {},
   path: "/",
   query: {} 
}
Copy the code

The core code for this.confirmTransition is shown below. After collecting the queue array, the program begins to execute the runQueue function. After runQueue is executed, it takes the hook functions from the queue and executes them in iterator. When all the hook functions in queue pass, the third runQueue argument is executed, which is equivalent to passing all the hook functions into the successful callback.

The runQueue is executed again in the callback function, this time by collecting the beforeRouteEnter and the globally defined beforeResolve and assigning them to the queue. After the navigation hook in the queue completes, the successful callback onComplete is executed. This step indicates that all navigators have passed and this path allows jump.

History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) { var current = this.current; var queue = ... Var iterator = function (hook, next) {... } runQueue(queue, iterator, function () {} runQueue(queue, iterator, function () {} runQueue(queue, iterator, function () {} ; // Collect internal beforeRouteEnter hook functions and global beforeResolve hook runQueue(queue, iterator, function () {... onComplete(route); . }); }Copy the code

Navigation Guard collection

The execution logic of the navigation guard has been combed through, but how to collect the queue has not been expanded, so let’s take a closer look at the queue collection process.

The resolveQueue function passes in the matched attributes of the current path object and the route object to be switched.

[{path:”/login”,meta:{},name:”login”,components: component object}], as described in detail in the previous article,matched data structure is like {path:”/login”,meta:{},name:”login”,components: component object}.

History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) { var current = this.current; const { updated,deactivated, activated } = resolveQueue( this.current.matched, route.matched ); Var queue = []. Concat (beforeRouteLeave extractLeaveGuards(deactivated), // Global beforeEach hook this.router. BeforeHooks, // beforeRouteUpdate hook extractUpdateHooks(updated), Map (function (m) {return m.befioreenter; function (m) {return m.befioreenter; }, // asyncComponents resolveAsyncComponents(activated)); var iterator = function (hook, next) { ... } runQueue(queue, iterator, function () { ... }Copy the code

After the resolveQueue function is executed, the updated,deactivated, and activated parameters are returned. These parameters are used for queue data collection.

ResolveQueue (resolveQueue); resolveQueue (resolveQueue); resolveQueue (resolveQueue)

  • Update: Retrieves the part that overlaps the current route from the new route
  • Activated: Indicates that the value of the new route is larger than the value of the current route. If there is no more value, the value is an empty array
  • Deactivated: indicates the value possessed by the current route but not by the new route.
Function resolveQueue (resolveQueue (resolveQueue (resolveQueue (resolveQueue (resolveQueue (resolveQueue (resolveQueue (resolveQueue (resolveQueue (resolveQueue (resolveQueue))); var 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), next-.slice (I), // Deactivated: Current. Slice (I) // deactivate array}}Copy the code

Once you have the three data points above, you can use them to collect all the navigation guard hooks.

Var queue = []. Concat (beforeRouteLeave extractLeaveGuards(deactivated), // Global beforeEach hook this.router. BeforeHooks, // beforeRouteUpdate hook extractUpdateHooks(updated), Map (function (m) {return m.befioreenter; function (m) {return m.befioreenter; }, // asyncComponents resolveAsyncComponents(activated));Copy the code

According to the execution order of navigation guard, according to the beforeRouteLeave, beforeEach, beforeRouteUpdate, beforeEnter and routing guard in the order collection.

beforeRouteLeave

The flatMapComponents function iterates through Records, fetching the component object def for each page inside it, and executes it in the right-hand callback.

The extractGuard function takes the function defined by beforeRouteLeave from def and returns it as an array. In short, the deactivated page component takes the function defined by beforeRouteLeave and returns it as an array.

extractLeaveGuards(deactivated); Function extractLeaveGuards (deactivated) {return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true) } function extractGuards ( records, name, bind, // def for each component object,instance = {},key = "default" var Guards = flatMapComponents(records, function (def, instance, match, key) { var guard = extractGuard(def, name); If (guard) {return array.isarray (guard)? guard.map(function (guard) { return bind(guard, instance, match, key); }) : bind(guard, instance, match, key) } }); // Guard collects all the routing functions defined and changes the context object. Finally, return The Guards array flatten(Reverse? guards.reverse() : guards) }Copy the code

beforeEach

Router. beforeEach can be obtained directly from this.router.beforeHooks.

beforeRouteUpdate

The beforeRouteUpdate collection process is similar to the above, with the following code for fetching it.

function extractUpdateHooks (updated) {
  return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}
Copy the code

beforeEnter

Route entry guard in the routing configuration table can be obtained directly from the route object activated by Activated.

activated.map(function (m) { return m.beforeEnter; }),
Copy the code

Asynchronous components

The route configuration for asynchronous components is as follows. The component value is a function that uses the dynamic import method of WebPack.

{ path: '/search', name: 'search', component: () => import(/* webpackChunkName: "search" */ '.. /views/Search/ search.vue '), // Search page component}Copy the code

ResolveAsyncComponents is the core function that handles asynchronous components (code below). The function returned by resolveAsyncComponents still conforms to the parameter format of the navigation guard, including to,from, and next. This part of the logic is completed by the source code itself.

In the flatMapComponents wrapped callback function,def is the corresponding component parameter above. It is treated as an asynchronous component when it is a function. We can take a look at def compiled by Webpack (the code below).

Def = function component() {return __webpack_require__. E (/*! import() | search*/ "search").then(__webpack_require__.bind(null, /*! . /views/Search/Search.vue */ "./src/views/Search/Search.vue")); }Copy the code

The application then defines two functions, resolve and reject, that are passed into def to execute, and the application asynchronously requests component content.

The resolve function is called after the request succeeds and the component object from the asynchronous request is passed to the resolvedDef parameter. The newly acquired component object resolvedDef is assigned to Match.ponents. default and stored to complete the asynchronous loading of the component. When all asynchronous components are loaded, execute next() to enter the next navigation guard.

function resolveAsyncComponents (matched) { return function (to, from, next) { var hasAsync = false; var pending = 0; var error = null; Function (def, _, match, key) { if (typeof def === 'function' && def.cid === undefined) { hasAsync = true; // asynchronous components pending++; Var resolve = once(function (resolvedDef) {if (isESModule(resolvedDef)) { resolvedDef = resolvedDef.default; } // Save resolved on async factory in case it's used elsewhere // vue.extend def. Resolved = typeof resolvedDef === 'function' ? resolvedDef : _Vue.extend(resolvedDef); Match.ponents [key] = resolvedDef; // Match is the routing object extracted from matched. pending--; if (pending <= 0) { next(); }}); Var reject = once(function (reason) {var 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); }}); var res; // get a promise res = def(resolve, reject); } catch (e) { reject(e); } if (res) {if (typeof res.then === 'function') {// Call promise.then res.then(resolve, reject); } else {// new syntax in Vue 2.3 var comp = res.coment; if (comp && typeof comp.then === 'function') { comp.then(resolve, reject); }}}}}); if (! hasAsync) { next(); }}}Copy the code

beforeRouteEnter

BeforeRouteEnter is a navigational guard defined inside the component, but it is not quite the same as the previous navigational guard. The beforeRouteEnter function does not get the component instance inside the beforeRouteEnter function, but the next callback can get the component instance through the parameter VM.

{ data(){ return {}; } beforeRouteEnter (to, from, next) {// Can't get component instance here... Next (vm = > {/ / by ` vm ` access component instance})}, the methods: {}}Copy the code

Take a look at the runQueue source code (below). After executing the first round of the navigational guard, the source code enters the runQueue callback function and executes the next phase of the navigational guard logic. In the second phase, beforeRouteEnter and beforeResolve hooks are collected before runQueue is executed. In this process, extractEnterGuards is the handler that collects beforeRouteEnter hooks.

runQueue(queue, iterator, function () { var postEnterCbs = []; var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid); var queue = enterGuards.concat(this$1.router.resolveHooks); runQueue(queue, iterator, function () { onComplete(route); if (this$1.router.app) { this$1.router.app.$nextTick(function () { postEnterCbs.forEach(function (cb) { cb(); }); }); }}); });Copy the code

ExtractEnterGuards function third parameter and the previous navigation guard is not the same, it calls the bindEnterGuard function. Guard is extracted from the inside of the component beforeRouteEnter function.

The beforeRouteEnter function does not return directly, but instead has a layer of bindEnterGuard. BindEnterGuard returns functions that are actually returned to the queue.

When the last navguard calls next(), the thread slowly executes into the routeEnterGuard function. The routeEnterGuard function directly calls the guard, which as mentioned above is the collected beforeRouteEnter function. The key point is that routeEnterGuard recreates the Guard’s next method.

In the case of beforeRouteEnter, whose next can pass a function and retrieve the component instance as an argument, Guard’s next will determine cb if it is a function, which is exactly what we are analyzing. We need to find a way to pass the component instance to CB.

There is a tricky problem at this point, the thread gets to the point where the component instance has not yet been created. So we first store cb as an anonymous function wrapped in CBS (the empty array postEnterCbs defined above).

function extractEnterGuards ( activated, cbs, isValid ) { return extractGuards( activated, 'beforeRouteEnter', function (guard, _, match, key) { return bindEnterGuard(guard, match, key, cbs, isValid) } ) } function bindEnterGuard ( guard, match, key, cbs, isValid ) { return function routeEnterGuard (to, from, next) { return guard(to, from, function (cb) { if (typeof cb === 'function') { cbs.push(function () { poll(cb, match.instances, key, isValid); }); } next(cb); })} function poll (instances, key); If (instances[key] &&!) {if (instances[key] &&! instances[key]._isBeingDestroyed // do not reuse being destroyed instance ) { cb(instances[key]); Else if (isValid()) {// It will listen here. If the group value is not created, it will execute later. // This isValid is what runQueue defines up here Var isValid = function () {return this$1.current === route; }; setTimeout(function () { poll(cb, instances, key, isValid); }, 16); }}Copy the code

After this$1.router.app.$nextTick completes, the program iterates through the postEnterCbs array and executes cb. Poll. instances (cb, match-. Instances, key, isValid) ¶

You can see from this that next(vm=>{… }) the wrapped callback is triggered after this$1.router.app.$nextTick and the VM is passed from the poll function above.

runQueue(queue, iterator, function () { ... runQueue(queue, iterator, function () { ... onComplete(route); If (this$1.router.app) {this$1.router.app.$nextTick(function () { cb(); }); }); }}); });Copy the code

Stern said

This completes the main collection of navigational guards and chain calls between guards. When a jump path passes all navigators, the application then executes the onComplete callback to transform the URL and trigger the page rendering.