vue-router源码分析(四)

572 阅读3分钟

导航解析流程

  1. 在被激活的组件里调用 beforeRouteEnter。
  2. 调用全局的 beforeResolve 守卫 (2.5+)。
  3. 导航被确认。
  4. 调用全局的 afterEach 钩子。
  5. 触发 DOM 更新。
  6. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

runQueue函数第一次执行完毕,导航守卫的前6步就执行完毕了,之后会触发传入的第三个回调函数。回调函数首先执行const enterGuards = extractEnterGuards(activated)extractEnterGuards(activated)会拿到activated数组中的record中的beforeRouteEnter。为什么要在6.解析异步路由组件之后执行beforeRouteEnter,是因为activated是新创建的组件,他很有可能是一个异步组件,如果不进行第6步解析,是拿不到他定义的beforeRouteEnter钩子。在执行extractGuards的时候,传入的第三个参数,并不是bindGuard函数,而是(guard, _, match, key) => { return bindEnterGuard(guard, match, key) }

  • bindGuard函数在执行runQueue函数的时候,会执行guard.apply(instance, arguments)instance是当前的vuecomponent,也就是说在执行beforeRouteLeavebeforeRouteUpdate的时候,this被指到了当前的vuecomponent上,所以可以在这两个导航守卫钩子中访问到this。

  • bindEnterGuard函数在执行runQueue函数的时候,会判断在调用next的时候,是否传入了函数,如果传入了函数,那么会执行match.enteredCbs[key].push(cb),官方的描述是无法在beforeRouteEnter中访问到this,因为此时路由的跳转并没有被确认,当执行了next,确认跳转之后,会创建组件实例,然后会通知next中传入的函数来访问到this。最终会通过调用handleRouteEntered函数来调用传入的cbs,并把record.instances[name]作为参数传入(也就是组件实例),调用handleRouteEntered是在RouterView组件中执行data.hook.init也就是组件初始化之后,进行回调函数的调用。

// src/history/base.js
 runQueue(queue, iterator, () => {
      // wait until async components are resolved before
      // extracting in-component enter guards
      
      const enterGuards = extractEnterGuards(activated);
      const queue = enterGuards.concat(this.router.resolveHooks);
      runQueue(queue, iterator, () => {
        if (this.pending !== route) {
          return abort(createNavigationCancelledError(current, route))
        }
        this.pending = null;
        onComplete(route);
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            console.log(444)
            handleRouteEntered(route);
          });
        }
      });
    });
...

function extractEnterGuards (
  activated: Array<RouteRecord>
): Array<?Function> {
  return extractGuards(
    activated,
    'beforeRouteEnter',
    (guard, _, match, key) => {
      return bindEnterGuard(guard, match, key)
    }
  )
}

function bindEnterGuard (
  guard: NavigationGuard,
  match: RouteRecord,
  key: string
): NavigationGuard {
  return function routeEnterGuard (to, from, next) {
    return guard(to, from, cb => {
      if (typeof cb === 'function') {
        if (!match.enteredCbs[key]) {
          match.enteredCbs[key] = []
        }
        match.enteredCbs[key].push(cb)
      }
      next(cb)
    })
  }
}

拿到activated中的beforeRouteEnter后,会和this.router.resolveHooks进行concat最终成为新的queue,也就是router实例的beforeResolve钩子也就是8.调用全局的 beforeResolve 守卫 (2.5+)

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

拿到新的queue,会去执行runQueue函数,执行完毕后触发回调函数,触发onComplete(route),也就是在最初执行confirmTransition传入的第二个参数,第二个参数是一个回调函数,首先他会执行updateRoute函数,把this.current置为route,这样就完成了当前路径的一个切换,也就是9. 导航被确认,执行完updateRoute后会遍历this.router.afterHooks,也就是全局的afterEach钩子,这对应了10. 调用全局的 afterEach 钩子。更新dom在之后会专门抽一篇文章去分析。最后调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。这一步在v3.4.9版本中,调用的时机已经被修改为了router-view中创建组件实例之后。最后在nextTick之后会再执行一次handleRouteEntered,这块目前没有用到,可能是为了防止某些意外的情况,导致钩子的next中的函数未被执行。

this.confirmTransition(
      route,
      () => {
        this.updateRoute(route);
        onComplete && onComplete(route);
        this.ensureURL();
        this.router.afterHooks.forEach(hook => {
          hook && hook(route, prev);
        });

        // fire ready cbs once
        if (!this.ready) {
          this.ready = true;
          this.readyCbs.forEach(cb => {
            cb(route);
          });
        }
      },
      err => {
        if (onAbort) {
          onAbort(err);
        }
        if (err && !this.ready) {
          // Initial redirection should not mark the history as ready yet
          // because it's triggered by the redirection instead
          // https://github.com/vuejs/vue-router/issues/3225
          // https://github.com/vuejs/vue-router/issues/3331
          if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
            this.ready = true;
            this.readyErrorCbs.forEach(cb => {
              cb(err);
            });
          }
        }
      }
    );