This is the 5th day of my participation in the August More Text Challenge

Navigation indicates that the route is changing. A navigational guard is essentially a set of routing-related hook functions that are executed when a route switch occurs. Vue-router provides navigation guards that are used to guard navigation by jumping or canceling.

preface

I wonder if students have encountered the following problems when using vue-Router for front-end routing development:

  • There are so many guards that provide navigation, but what is the order in which each navigation is called when I switch routes between two components?
  • Why do I need to provide the next function in each navigation to proceed to the next guard?
  • How do I get the current component instance in the guard: beforeRouteEnter component?

If you are using vue-Router for the first time, do you have any questions? But don’t panic. I believe that through the hand-written source code practice of this series of articles, we will thoroughly understand the underlying principles of vue-Router navigation guard.

Vue-router Navigation guard

Vue-router provides a total of seven navigational guards:

The guards describe
router.beforeEach Global front guard
router.beforeResolve Global parsing guard
router.afterEach Global post-hook
router.beforeEnter Route exclusive guard
router.beforeRouteEnter Guards within components
router.beforeRouteUpdate Guards within components
router.beforeRouteLeave Guards within components

Navigation Flow parsing

  1. Navigation is triggered.
  2. Call the beforeRouteLeave guard in the deactivated component.
  3. Call the global beforeEach guard.
  4. Call the beforeRouteUpdate guard in the reused component.
  5. Call beforeEnter in routing configuration.
  6. Parse the asynchronous routing component.
  7. Call beforeRouteEnter in the activated component.
  8. Call the global beforeResolve guard.
  9. Navigation confirmed.
  10. Call the global afterEach hook.
  11. Trigger a DOM update.
  12. Call the callback passed to Next in the beforeRouteEnter guard, and the created component instance is passed in as an argument to the callback.

The above is the analysis of the navigation process given in the official document. So now I will follow this process to lead students to implement the vue-Router advanced part: Vue-Router navigation guard.

The nature of the route guard and JavaScript asynchronous queue implementation: queue function

Asynchronous programming is very important in JavaScript, and the guard execution of the vue-Router is essentially executing an asynchronous queue. Realizing this view is crucial to our understanding of routing guards. Having said that, how to implement an asynchronous queue elegantly shows the skill of the developer.

There are many ways to implement asynchronous queues: callback, promise, generator, async/await, etc. These can be used to handle asynchronous queues.

For example, use promise:

a.then(
  b.then(
    c.then()
  )
)
Copy the code

The problem is that the code is heavily nested, and as soon as there are too many tasks, the code will drift infinitely to the right and give up. So we thought: It would be nice to have a queue that could execute itself sequentially and terminate the queue if necessary. Let’s look at the following code:

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

We have a function that automatically executes queues: runQueue. It takes three arguments:

  1. Queue: asynchronous tasks (or asynchronous functions) in a queue
  2. Fn: an iterator. Its job is to execute the asynchronous function in the queue, and when the asynchronous function is finished, the next asynchronous function is called back.
  3. Cb: This callback function can be used as an aid to perform some additional tasks.

Inside runQueue we define a step function. This ensures that the asynchronous functions in the queue are executed in order. Since our navigational guard hook can be treated as an asynchronous task, we can use it to implement our asynchronous queue.

Implement an iterative method: iterator

The iterator method iterates over our guard hook, for example:

router.beforeEach((to, from, next) = > {
  // ...
})
Copy the code

So let’s see how this iterator works:

const iterator = (hook, next) = > {
  try {
    NavigationGuard(to, from, next) NavigationGuard(to, from, next)
    hook(route, current, (to) = > {
        next(to);
    });
  } catch (error) {
    console.log(error); }};Copy the code

The iterator function takes two arguments, and both arguments are functions. A hook is our guard hook, and next can be understood as the switch that moves us to the next hook. The asynchronous queue does not proceed until the next() method is executed inside the hook.

The above runQueue and iterator methods work together to make asynchronous queues automatically execute in the desired order. So it’s important to understand both runQueue and iterator!

Modified code

We have understood the nature of routing through the previous foreshadowing. So we followed the last series of handwritten vue-Router source code series four: implementation of global components router-link, router-View basis to continue to transform the code.

Start by adding attributes to the route record that hold component instances and route guard hooks:

// src/vRouter/create-route-map.js

const record = {
  path: normalizedPath,
  regex: regexPath,
  components: route.components || { default: route.component }, //平时我们其实用的default
  beforeEnter: route.beforeEnter,   // Route exclusive guard, new attribute
  instances: {},                    // The new attribute
  enteredCbs: {},                   // Save the beforeRouteEnter hook callback
  name,
  parent,
  meta: route.meta || {},
};
Copy the code

Then modify our route transition method history.transitionto. In the base.js file:

// src/vRouter/history/base.js

transitionTo(location, onComplete) {
    // Calculate the destination route
    const route = this.router.match(location, this.current);
    const prev = this.current;
    this.confirmTransition(route, () = > {
      // Update the current route
      this.updateRoute(route);
      onComplete && onComplete(route);
      this.ensureURL();
      // Execute the global post-hook
      this.router.afterHooks.forEach((hook) = > {
        hook && hook(route, prev);
      });
    });
  }
Copy the code

When the URL changes, we call the confirmTransition method and pass in two parameters: the destination route and the callback function. This callback will be called when appropriate, and it will internally update the route and execute the global afterEach hook.

And the History. ConfirmTransition:

// src/vRouter/history/base.js

  confirmTransition(route, onComplete) {
    const current = this.current;
    // Updated Routes to be reused
    // activated Indicates the activated route
    // deactivated Route

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

    const queue = [].concat(
      // Extract the beforeRouteLeave hook from the deactivated route
      extractLeaveGuards(deactivated),
      // Get the global front hook
      this.router.beforeHooks,
      // Extract the beforeRouteUpdate hook from the reused route
      extractUpdateHooks(updated),
      // Extracts the beforeEnter hook from the activated route
      activated.map((m) = > m.beforeEnter),
      // Extract the beforeRouteEnter hook
      extractEnterGuards(activated),
      // Extract the beforeResolve hook
      this.router.resolveHooks,
    );

    // The hook is our routing-guard hook. The next hook is executed only when the next callback is executed
    const iterator = (hook, next) = > {
      try {
        NavigationGuard(to, from, next) NavigationGuard(to, from, next)
        hook(route, current, (to) = > {
          if (to === false) {
            this.ensureURL(true);
          } else if (isError(to)) {
            this.ensureURL(true);
          } else if (
            typeof to === "string"| | -typeof to === "object" &&
              (typeof to.path === "string" || typeof to.name === "string"))) {if (typeof to === "object" && to.replace) {
              this.replace(to);
            } else {
              this.push(to); }}else{ next(to); }}); }catch (error) {
        console.log(error); }}; runQueue(queue, iterator,() = > {
      // Perform route updates and global post-hooks
      onComplete && onComplete();
      if (this.router.app) {
        // Call next's callback when the component instance is loaded
        this.router.app.$nextTick(() = > {
          // Wait until the new component is created to execute the handleRouteEntered function, which is equivalent to executing the beforeRouteEnter hookhandleRouteEntered(route); }); }}); }Copy the code

In the confirmTransition method, resolveQueue is called to calculate the updated, deactivated, and matched routes from the destination route. Activated. They are the re-used route, the activated route, and the deactivated route. Because when we switch routes we usually go from a deactivated route to an activated route. So we need to extract the navigation hooks in the two routing components and perform the guards in a certain order.

Let’s look at the resolveQueue code:

function resolveQueue(current, next) {
  let i;
  const max = Math.max(current.length, next.length);
  for (i = 0; i < max; i++) {
    if(current[i] ! == next[i]) {break; }}return {
    updated: next.slice(0, i),
    activated: next.slice(i),
    deactivated: current.slice(i),
  };
}
Copy the code

Since Route. matched is an array of RouteRecord, and the path is changed from current to route. A different position I is found by traversing and comparing the RouteRecord on both sides. Therefore, the next RouteRecord from 0 to I is the same on both sides, so it is updated. RouteRecord from I to the final RouteRecord is unique to Next and is activated. In current, the value from I to the last RouteRecord is deactivated. The updated, activated, and deactivated ReouteRecord arrays are updated, activated, and deactivated. The next important part of the path transformation is to execute a series of hook functions.

We store guard hooks by defining a queue array.


const queue = [].concat(
  // Extract the beforeRouteLeave hook from the deactivated route
  extractLeaveGuards(deactivated),
  // Get the global front hook
  this.router.beforeHooks,
  // Extract the beforeRouteUpdate hook from the reused route
  extractUpdateHooks(updated),
  // Extracts the beforeEnter hook from the activated route
  activated.map((m) = > m.beforeEnter),
  // Extract the beforeRouteEnter hook
  extractEnterGuards(activated),
  // Extract the beforeResolve hook
  this.router.resolveHooks,
);

Copy the code

Finally, the asynchronous queue is executed using the runQueue method.


runQueue(queue, iterator, () = > {
  // Perform route updates and global post-hooks
  onComplete && onComplete();
  if (this.router.app) {
    // Call next's callback when the component instance is loaded
    this.router.app.$nextTick(() = > {
      // Wait until the new component is created to execute the handleRouteEntered function, which is equivalent to executing the beforeRouteEnter hookhandleRouteEntered(route); }); }});Copy the code

The iterator method is also used in the runQueue method

const iterator = (hook, next) = > {
  try {
    NavigationGuard(to, from, next) NavigationGuard(to, from, next)
    hook(route, current, (to) = > {
      if (to === false) {
        this.ensureURL(true);
      } else if (isError(to)) {
        this.ensureURL(true);
      } else if (
        typeof to === "string"| | -typeof to === "object" &&
          (typeof to.path === "string" || typeof to.name === "string"))) {if (typeof to === "object" && to.replace) {
          this.replace(to);
        } else {
          this.push(to); }}else{ next(to); }}); }catch (error) {
    console.log(error); }};Copy the code

The first hook argument to iterator refers to our route-guard hook, passed in route, current, and anonymous functions. These parameters correspond to to, from, and next in the document. When an anonymous function is executed, the unnavigation or next function will be executed depending on some condition. Only when next is executed will the next navigation guard hook function be advanced. This is why the official documentation only emphasizes executing the next method to resolve the hook function.

Refer to the base.js file for the complete code:

// src/vRouter/history/base.js

import { START } from ".. /util/route";
import { inBrowser } from ".. /util/dom";
import { _Vue } from ".. /install";

export class History {
  constructor(router, base) {
    // The base path of the application. For example, if the entire single-page application service is under /app/, then base should be set to "/app/".
    this.base = normalizeBase(base);
    this.router = router; // Save the router column
    this.current = START;
    this.listeners = [];
  }

  listen(cb) {
    // Save the method for future History calls
    this.cb = cb;
  }

  teardown() {
    // Clear the event listener
    this.listeners.forEach((cleanupListener) = > {
      cleanupListener();
    });
    this.listeners = [];

    // Reset the current route
    this.current = START;
  }

  updateRoute(route) {
    this.current = route;
    this.cb && this.cb(route);
  }

  transitionTo(location, onComplete) {
    // Calculate the destination route
    const route = this.router.match(location, this.current);
    const prev = this.current;
    this.confirmTransition(route, () = > {
      // Update the current route
      this.updateRoute(route);
      onComplete && onComplete(route);
      this.ensureURL();
      // Execute the global post-hook
      this.router.afterHooks.forEach((hook) = > {
        hook && hook(route, prev);
      });
    });
  }

  confirmTransition(route, onComplete) {
    const current = this.current;
    // Updated Routes to be reused
    // activated Indicates the activated route
    // deactivated Route

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

    const queue = [].concat(
      // Extract the beforeRouteLeave hook from the deactivated route
      extractLeaveGuards(deactivated),
      // Get the global front hook
      this.router.beforeHooks,
      // Extract the beforeRouteUpdate hook from the reused route
      extractUpdateHooks(updated),
      // Extracts the beforeEnter hook from the activated route
      activated.map((m) = > m.beforeEnter),
      // Extract the beforeRouteEnter hook
      extractEnterGuards(activated),
      // Extract the beforeResolve hook
      this.router.resolveHooks,
    );

    // The hook is our routing-guard hook. The next hook is executed only when the next callback is executed
    const iterator = (hook, next) = > {
      try {
        NavigationGuard(to, from, next) NavigationGuard(to, from, next)
        hook(route, current, (to) = > {
          if (to === false) {
            this.ensureURL(true);
          } else if (isError(to)) {
            this.ensureURL(true);
          } else if (
            typeof to === "string"| | -typeof to === "object" &&
              (typeof to.path === "string" || typeof to.name === "string"))) {if (typeof to === "object" && to.replace) {
              this.replace(to);
            } else {
              this.push(to); }}else{ next(to); }}); }catch (error) {
        console.log(error); }}; runQueue(queue, iterator,() = > {
      onComplete && onComplete();
      if (this.router.app) {
        // Call next's callback when the component instance is loaded
        this.router.app.$nextTick(() = > {
          // Wait until the new component is created to execute the handleRouteEntered function, which is equivalent to executing the beforeRouteEnter hookhandleRouteEntered(route); }); }}); }}function isError(err) {
  return Object.prototype.toString.call(err).indexOf("Error") > -1;
}

function normalizeBase(base) {
  if(! base) {if (inBrowser) {
       
      const baseEl = document.querySelector("base");
      base = (baseEl && baseEl.getAttribute("href")) || "/";
      // "https://foo/" >> "/"
      base = base.replace(/^https? : \ \ [^ \] / / / + /."");
    } else {
      base = "/"; }}// Make sure base starts with "/"
  if (base.charAt(0)! = ="/") {
    base = "/" + base;
  }
  // Remove the "/" at the end of the path
  return base.replace(/ / / $/."");
}

Serialization executes asynchronous functions, and only the next callback executes the next hook
function runQueue(queue, fn, cb) {
  const step = (index) = > {
    if (index >= queue.length) {
      cb();
    } else {
      if (queue[index]) {
        fn(queue[index], () = > {
          step(index + 1);
        });
      } else {
        step(index + 1); }}}; step(0);
}

function resolveQueue(current, next) {
  let i;
  const max = Math.max(current.length, next.length);
  for (i = 0; i < max; i++) {
    if(current[i] ! == next[i]) {break; }}return {
    updated: next.slice(0, i),
    activated: next.slice(i),
    deactivated: current.slice(i),
  };
}

/** * "bind" the extracted component guard hook to the corresponding component instance *@param {*} guard
 * @param {*} instance
 * @returns* /
function bindGuard(guard, instance) {
  if (instance) {
    //const iterator = (hook, next) const hook = boundRouteGuard()
    return function boundRouteGuard() {
      return guard.apply(instance, arguments); }; }}function bindEnterGuard(guard, match, key) {
  return function routeEnterGuard(to, from, next) {
    //guard => beforeRouteEnter
    // cb is the callback we wrote in beforeRouteEnter next
    // next(vm => {
    // Access component instances through 'VM'
    // console.log('vm: ', vm);
    // })
    return guard(to, from.(cb) = > {
      if (typeof cb === "function") {
        if(! match.enteredCbs[key]) { match.enteredCbs[key] = []; } match.enteredCbs[key].push(cb); } next(cb); }); }; }// Extract the beforeRouteEnter hook
function extractEnterGuards(activated) {
  return extractGuards(
    activated,
    "beforeRouteEnter".(guard, _, match, key) = > {
      returnbindEnterGuard(guard, match, key); }); }// Extract the beforeRouteLeave hook
function extractLeaveGuards(deactivated) {
  return extractGuards(deactivated, "beforeRouteLeave", bindGuard, true);
}

// Extract the beforeRouteUpdate hook
function extractUpdateHooks(updated) {
  return extractGuards(updated, "beforeRouteUpdate", bindGuard);
}

/** * extractGuards will eventually return an array containing the corresponding component routing guard hooks * bind => bindGuard(guard, instance) */
function extractGuards(records, name, bind, reverse) {
  / / circulation records to traverse each records. The record = > {path: "/ foo", the regex: / ^ \ / foo / # \ \ /? ? $/ I, components: {... }, the instances: {... }, beforeEnter: ƒ,... }
  / / so you can get record.components.com ponent eventually is the def, namely we write. Vue components
  const guards = flatMapComponents(records, (def, instance, match, key) = > {
    // The final hook function can be obtained by passing in the.vue component and routing guard hook name.
    const guard = extractGuard(def, name);
    if (guard) {
      return Array.isArray(guard)
        ? Bind (guard, instance, match, key) => guard. Apply (instance, match, key) arguments)
          guard.map((guard) = >bind(guard, instance, match, key)) : bind(guard, instance, match, key); }});return flatten(reverse ? guards.reverse() : guards);
}

function extractGuard(def, key) {
  if (typeofdef ! = ="function") {
    // Using the base Vue constructor, create a "subclass". The parameter is an object that contains component options. NavigationGuard [NavigationGuard] NavigationGuard [NavigationGuard] NavigationGuard [NavigationGuard
    def = _Vue.extend(def);
  }
  return def.options[key];
}

function flatMapComponents(matched, fn) {
  return flatten(
    matched.map((m) = > {
      Fn (components. Default, instances, components, 'default')
      return Object.keys(m.components).map((key) = >fn(m.components[key], m.instances[key], m, key) ); })); }function flatten(arr) {
  return Array.prototype.concat.apply([], arr);
}

// Execute the beforeRouteEnter hook function
function handleRouteEntered(route) {
  for (let i = 0; i < route.matched.length; i++) {
    // Obtain the routing record from route.matched
    const record = route.matched[i];
    / / traverse
    for (const name in record.instances) {
      // Get the component
      const instance = record.instances[name];
      const cbs = record.enteredCbs[name];
      if(! instance || ! cbs) {continue;
      }
      delete record.enteredCbs[name];
      for (let i = 0; i < cbs.length; i++) {
        if(! instance._isBeingDestroyed) {// If the component is not destroyed, execute next in beforeRouteEnter (vm => {// Through 'vm 'access the component instance console.log('vm: ', vm); }) method that takes the current component instance.
          // So the official documentation says that the beforeRouteEnter guard cannot access this, but can access the component instance by passing a callback to Next.
          cbs[i](instance);
        }
      }
    }
  }
}

Copy the code

Add beforeEach, beforeResolve, afterEach hook functions to the VueRouter class

Modify the index.js file and add three arrays to hold guard hooks: beforeHooks, resolveHooks, afterHooks.

// src/vRouter/index.js

import { cleanPath } from "./util/path";
import { createMatcher } from "./create-matcher";
import { install } from "./install";
import { HashHistory } from "./history/hash";
import { normalizeLocation } from "./util/location";

export default class VueRouter {
  constructor(options = {}) {
    // Get the configuration passed in by the user
    this.options = options;
    // this.app represents the root Vue instance
    this.app = null;
    //this.apps saves Vue instances of all child components
    this.apps = [];
    // An array of three global hook functions
    this.beforeHooks = [];
    this.resolveHooks = [];
    this.afterHooks = [];
    CreateMatcher returns an object {match, addRoutes, getRoutes, addRoutes}
    this.matcher = createMatcher(options.routes || [], this);
    this.mode = options.mode || "hash";
    // Implement front-end routing in different modes
    switch (this.mode) {
      case "hash":
        this.history = new HashHistory(this, options.base);
        break;
      default:
        return new Error(`invalid mode: The ${this.mode}`); }}match(raw, current) {
    return this.matcher.match(raw, current);
  }

  push(location, onComplete) {
    this.history.push(location, onComplete);
  }

  replace(location, onComplete) {
    this.history.replace(location, onComplete);
  }

  init(app) {
    this.apps.push(app);
    // Only the root Vue instance will be saved to this.app
    if (this.app) {
      return;
    }
    // Save the Vue instance
    this.app = app;
    const history = this.history;
    // if (history instanceof HTML5History || history instanceof HashHistory) {
    if (history instanceof HashHistory) {
      // Add a route event listener
      const setupListeners = () = > {
        history.setupListeners();
      };
      // Perform route transition
      history.transitionTo(history.getCurrentLocation(), setupListeners);
    }

    /** * Registers a function that takes a currentRoute as an argument. * Vue._route = currentRoute is executed each time a route is switched. * So that each VUE component instance gets currentRoute and updates the view */
    history.listen((route) = > {
      this.apps.forEach((app) = > {
        // Update _route on app
        app._route = route;
      });
    });
  }

  // Global front-guard
  beforeEach(fn) {
    return registerHook(this.beforeHooks, fn);
  }

  // Global parse guard
  beforeResolve(fn) {
    return registerHook(this.resolveHooks, fn);
  }

  // Global post-hook
  afterEach(fn) {
    return registerHook(this.afterHooks, fn);
  }

  resolve(to, current, append) {
    current = current || this.history.current;
    const location = normalizeLocation(to, current, append, this);
    const route = this.match(location, current);
    const fullPath = route.redirectedFrom || route.fullPath;
    const base = this.history.base;
    const href = createHref(base, fullPath, this.mode);
    return {
      location,
      route,
      href,
      normalizedTo: location,
      resolved: route, }; }}function createHref(base, fullPath, mode) {
  var path = mode === "hash" ? "#" + fullPath : fullPath;
  return base ? cleanPath(base + "/" + path) : path;
}

function registerHook(list, fn) {
  list.push(fn);
  return () = > {
    // Waiting to be called from it
    const i = list.indexOf(fn);
    if (i > -1) list.splice(i, 1);
  };
}

VueRouter.install = install;

Copy the code

How do I get a component instance from the beforeRouteEnter hook?

BeforeRouteEnter is called before the corresponding route to render the component is confirmed. The component instance cannot be retrieved at this time because the component instance has not been created before the guard is executed.

However, you can access the component instance by passing a callback to Next. The callback is executed when the navigation is validated, and the component instance is taken as an argument to the callback method.

beforeRouteEnter (to, from, next) {
  next(vm= > {
    // Access component instances through 'VM'})}Copy the code

Let’s take a look at why the component instance can be retrieved using this callback method.

First, we saved the anonymous functions in next((VM =>{})) in Record.enteredcbs during extractEnterGuards(activated) execution.

Reference code:

function extractEnterGuards(activated) {
  return extractGuards(
    activated,
    "beforeRouteEnter".(guard, _, match, key) = > {
      returnbindEnterGuard(guard, match, key); }); }function bindEnterGuard(guard, match, key) {
  return function routeEnterGuard(to, from, next) {
    //guard => beforeRouteEnter
    // cb is the callback we wrote in beforeRouteEnter next
    // next(vm => {
    // Access component instances through 'VM'
    // console.log('vm: ', vm);
    // })
    return guard(to, from.(cb) = > {
      if (typeof cb === "function") {
        if(! match.enteredCbs[key]) { match.enteredCbs[key] = []; } match.enteredCbs[key].push(cb); } next(cb); }); }; }Copy the code

The runQueue callback is then executed in the last step of all queues in the runQueue method. And at this point we try to get the component instance, if we get it. We call the $nextTick() method provided by Vue and pass route as an argument to the handleRouteEntered function. Inside handleRouteEntered we traverse the Route. matched array. And an array of previously saved anonymous functions is retrieved via Record.enteredcbs, which we call CBS.

Reference code:

runQueue(queue, iterator, () = > {
  onComplete && onComplete();
  if (this.router.app) {
    // Call next's callback when the component instance is loaded
    this.router.app.$nextTick(() = > {
      // Wait until the new component is created to execute the handleRouteEntered function, which is equivalent to executing the beforeRouteEnter hookhandleRouteEntered(route); }); }});Copy the code

Finally we pass instance as an argument to an anonymous function by iterating through the array: CBS [I](instance); So we can get the component instance in the anonymous function of the Next method of the beforeRouteEnter hook.

Record schematic screenshot 1:

Record schematic screenshot 2:

Reference code:

function handleRouteEntered(route) {
  for (let i = 0; i < route.matched.length; i++) {
    // Obtain the routing record from route.matched
    const record = route.matched[i];
    / / traverse record. The instances
    for (const name in record.instances) {
      // Get the component
      const instance = record.instances[name];
      const cbs = record.enteredCbs[name];
      if(! instance || ! cbs) {continue;
      }
      delete record.enteredCbs[name];
      for (let i = 0; i < cbs.length; i++) {
        if(! instance._isBeingDestroyed) {// If the component is not destroyed, execute next in beforeRouteEnter (vm => {// Through 'vm 'access the component instance console.log('vm: ', vm); }) method that takes the current component instance.
          // So the official documentation says that the beforeRouteEnter guard cannot access this, but can access the component instance by passing a callback to Next.
          cbs[i](instance);
        }
      }
    }
  }
}
Copy the code

The question is, when is the record.instance object saved?

The answer is that when we do updateRoute the view will be re-rendered. At this point we can retrieve the currently rendered component instance from context.parent in the RouterView component’s Render function.

Refer to the view. Js:

// src/vRouter/components/view.js

export default {
  name: "RouterView".functional: true.props: {
    name: {
      type: String.default: "default",}},// Use render to generate the virtual DOM. The vnode = render. Call (vm) _renderProxy, vm. $createElement method), defined in the vue source directory: SRC/core/instance/render. Js
  // Therefore, the first argument to the render function is createElement (h)
  // vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  Render (h, {props, children, parent, data}) === render(h, context)
  // render(h, { props, children, parent, data }) {
  render(h, { props, parent, data }) {
    // Everything the component needs is passed through the context argument
    const name = props.name;
    const route = parent.$route;
    const depth = 0; // Do not consider nested routines
    const matched = route.matched[depth];
    const component = matched && matched.components[name];
    if (component) {
      / / save the instance
      matched.instances[name] = component;
    }
    returnh(component, data); }};Copy the code

Verify the navigation resolution process

With the above code modified, let’s verify that the complete navigation parsing process is in accordance with the official guidelines.

Add a new routing line, baz, to the app.vue file to demonstrate the re-used routing components.

// src/App.vue

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <p>
      <router-link to="/foo">/foo</router-link>
      <router-link to="/bar">/bar</router-link>
      <router-link to="/baz/1">/baz/1</router-link>
      <router-link to="/baz/2">/baz/2</router-link>
    </p>
    <router-view></router-view>
  </div>
</template>

<script>

export default {
  name: 'App',
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
a {
  margin:20px;
}
</style>

Copy the code

Add global guard hooks and route exclusive hooks:

// src/router/index.js

import Vue from "vue";
import VueRouter from "@/vRouter/";
import Foo from ".. /views/Foo.vue";
import Bar from ".. /views/Bar.vue";
import Baz from ".. /views/Baz.vue";

Vue.use(VueRouter);

const routes = [
  {
    path: "/foo".name: "foo".component: Foo,
    beforeEnter: (to, from, next) = > {
      console.log("Call beforeEnter in routing configuration, from foo"); next(); }, {},path: "/bar".name: "bar".component: Bar,
    beforeEnter: (to, from, next) = > {
      console.log("Call beforeEnter in routing configuration, from bar"); next(); }, {},path: "/baz/:id".component: Baz,
    beforeEnter: (to, from, next) = > {
      console.log("Call beforeEnter in routing configuration, from bar"); next(); }},];let router = new VueRouter({
  routes,
});

router.beforeEach((to, from, next) = > {
  console.log("Global front-guard: beforeEach");
  next();
});

router.beforeResolve((to, from, next) = > {
  console.log("Global parsing Guard: beforeResolve");
  next();
});

router.afterEach((to, from) = > {
  console.log(Global afterhook: afterEach);
});
export default router;

Copy the code

Add a guard within a component:

Bar. Vue file

// src/views/Bar.vue <template> <h1>bar</h1> </template> <script> export default { name: "Bar ", beforeRouteEnter(to, from, next) {console.log(' guard in bar component: beforeRouteEnter'); // Call the render component before the corresponding route is confirmed // no! Can!!!! Next (vm => {// Use 'VM' to access component instance console.log('vm: ', vm); })}, beforeRouteLeave(to, from, next) {console.log(' Guard in bar component: beforeRouteLeave'); // Access component instance 'this' next()}}; </script> <style></style>Copy the code

Baz. Vue file

// src/views/Baz.vue <template> <h1>baz</h1> </template> <script> export default { name: "Baz ", beforeRouteEnter(to, from, next) {console.log(' Guard in baz component: beforeRouteEnter'); // Call the render component before the corresponding route is confirmed // no! Can!!!! Next (vm => {// Use 'VM' to access component instance console.log('vm: ', vm); })}, beforeRouteUpdate(to, from, next) {console.log(' Guard within baz: beforeRouteUpdate'); // is called when the current route changes but the component is being reused. For example, for a path /foo/:id with dynamic parameters, jumping between /foo/1 and /foo/2 will reuse the component instance because the same foo component will be rendered. And the hook will be called in that case. // This' next()}, beforeRouteLeave(to, from, next) {console.log(' Baz component guard: beforeRouteLeave'); // Access component instance 'this' next()}}; </script> <style></style>Copy the code

Foo vue file

// src/views/Foo.vue <template> <h1>foo</h1> </template> <script> export default { name: "foo", data(){ return { name: "Foo"}}, beforeRouteEnter(to, from, next) {console.log(' guard in foo: beforeRouteEnter'); // Call the render component before the corresponding route is confirmed // no! Can!!!! Next (vm => {// Use 'VM' to access component instance console.log('vm', vm); })}, beforeRouteLeave(to, from, next) {console.log(' Guard in foo: beforeRouteLeave'); This' next()}} </script> <style> </style>Copy the code

Now to run the project, let’s first navigate from Foo to bar to see which hooks are executed:

As you can see from the figure, when we navigate from Foo to bar we call the following hook:

  1. Guard in foo: beforeRouteLeave
  2. Global front-guard: beforeEach
  3. Call beforeEnter, from bar, in routing configuration
  4. Guard inside bar component: beforeRouteEnter
  5. Global resolution guard: beforeResolve
  6. Global post-hook: afterEach

Be consistent with the official guidance on the navigation parsing process!

Which guards are triggered when the component is being reused. Let’s go to baz/1, go to Baz /2, and then go back to baz/1. Take a look at how the navguard executes from baz/2 back to baz/1:

  1. Global front-guard: beforeEach
  2. Guard within the baz component: beforeRouteUpdate
  3. Global resolution guard: beforeResolve
  4. Global post-hook: afterEach

We see that the BAZ component is reactivated during this process. So the beforeRouteUpdate hook is called.

Don’t say anything, clap haha

Next up

There is one thing we haven’t considered in this installment, and that is how the navigational guard for asynchronous routing components is implemented. See you next time!

Vue-router source code

  • Handwritten VueRouter source code series A: implement VueRouter
  • Handwritten Vue Router source code series two: implementation matcher
  • Handwritten vue-Router source code series three: implement changes to the hash after updating the view
  • Handwritten VUe-Router source code series four: implementation of global components router-link, router-View
  • Handwritten VUe-Router source code series five: achieve navigation guard
  • Handwritten VUE-Router source code series six: implementation of asynchronous components of the navigation guard