学习vue-router源码记录-2

继上一遍文章大概介绍了vue-router里面的概念,这一篇文章主要详细介绍路由跳转中发生了什么。

路由跳转执行的代码主要在./base.js文件里,详细看transitionTo方法。

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const route = this.router.match(location, this.current)
    this.confirmTransition(route, () => {
      this.updateRoute(route)
      onComplete && onComplete(route)
      this.ensureURL()

      // 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) {
        this.ready = true
        this.readyErrorCbs.forEach(cb => { cb(err) })
      }
    })
  }
复制代码

transitionTo代码非常简单,执行this.route.match,通过比较新的路由和就得路由拿到一个route对象,然后执行confirmTransition确认路由跳转。

下面看看这个route对象的定义

declare type Route = {
  path: string;
  name: ?string;
  hash: string;
  query: Dictionary<string>;
  params: Dictionary<string>;
  fullPath: string;
  matched: Array<RouteRecord>;
  redirectedFrom?: string;
  meta?: any;
}
复制代码

对照代码,我们主要看matched这个属性,在上一篇文章里面已经介绍过RouteRecord对象的定义。路由一开始会执行createMatcher生成一个路由映射表,因此matched里面存放就是我们将要跳转到的路由匹配上的路由配置对象

举一个简单的例子说明:

[{
    path: '/parent',
    component: Parent,
    children: [
    { path: 'foo', component: Foo },
    { path: 'bar', component: Bar },
    ],
}]
复制代码

假设我们配置了以下的路由,createMatcher会生成根据path建立的映射表

pathMap = {
    '/parent': RouteRecord,
    '/parent/foo': RouteRecord,
    '/parent/bar': RouteRecord,
}
复制代码

假如我们发生了一个从/parent路由跳转到/parent/foo路由,首先执行以下代码生成的route对象

const route = this.router.match(location, this.current)
复制代码

因此根据我们假设的配置,这里的route里的matched将会包含/parent/parent/fooRouteRecord。至于具体的match方法代码就不详细解释了。

继续讲路由的跳转,生成route对象后将会执行一个确认的方法confirmTransition

由于这个方法代码比较长,我们拆开来说明,首先看这个方法的入参说明,接受三个参数,route对象在前面已经生成过了,另外两个是执行完成的回调方法和退出的回调方法。

confirmTransition (route: Route, onComplete: Function, onAbort?: Function)
复制代码

在代码的一开始首先判断当前路由与跳转的路由是否是同一个路由,如果是则直接退出。

    const current = this.current
    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 (
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      route.matched.length === current.matched.length
    ) {
      this.ensureURL()
      return abort()
    }

复制代码

这里的ensureURL方法定义在HTML5History的原型链上,实际上执行的是保存路由变化历史记录,根据pushtruefalse来确定执行pushState还是replaceState。这一方法在执行完路由跳转后同样会执行一次。

  HTML5History.prototype.ensureURL = function ensureURL (push) {
    if (getLocation(this.base) !== this.current.fullPath) {
      var current = cleanPath(this.base + this.current.fullPath);
      push ? pushState(current) : replaceState(current);
    }
  };
复制代码

继续看后面的代码,首先通过resolveQueue对比this.currentroute对象的matched提取三种变化的组件队列。根据命名我们直接可得知updateddeactivatedactivated分别对应更新的组件、失效的组件、激活的组件。然后生成一个需要执行方法的队列queue,根据这个队列的生成定义,我们可以看出执行方法的顺序,至于通过extractLeaveGuardsextractUpdateHooks方法提取组件里的守卫函数就不细说了。

  1. 在失活的组件里调用离开守卫。

  2. 调用全局的 beforeEach 守卫。

  3. 在重用的组件里调用 beforeRouteUpdate 守卫

  4. 在激活的路由配置里调用 beforeEnter。

  5. 解析异步路由组件。

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

    const queue: Array<?NavigationGuard> = [].concat(
      // in-component leave guards
      extractLeaveGuards(deactivated),
      // global before hooks
      this.router.beforeHooks,
      // in-component update hooks
      extractUpdateHooks(updated),
      // in-config enter guards
      activated.map(m => m.beforeEnter),
      // async components
      resolveAsyncComponents(activated)
    )
复制代码

看看resolveQueue是如何提取变化的组件。比较currentnext确定一个变化的位置inext里的从0i则是updated的部分,i之后的则是activated的部分,而currenti之后的则是deactivated的部分。

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++) {
    if (current[i] !== next[i]) {
      break
    }
  }
  return {
    updated: next.slice(0, i),
    activated: next.slice(i),
    deactivated: current.slice(i)
  }
}
复制代码

接下来就是生成迭代器方法iterator,执行runQueue方法。

    this.pending = route
    const iterator = (hook: NavigationGuard, next) => {
      if (this.pending !== route) {
        return abort()
      }
      try {
        hook(route, current, (to: any) => {
          if (to === false || isError(to)) {
            // next(false) -> abort navigation, ensure current URL
            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 {
            // confirm transition and pass on the value
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }
    
    runQueue(queue, iterator, () => {
      const postEnterCbs = []
      const isValid = () => this.current === route
      // wait until async components are resolved before
      // extracting in-component enter guards
      const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
      const queue = enterGuards.concat(this.router.resolveHooks)
      runQueue(queue, iterator, () => {
        if (this.pending !== route) {
          return abort()
        }
        this.pending = null
        onComplete(route)
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            postEnterCbs.forEach(cb => { cb() })
          })
        }
      })
    })
复制代码

runQueue方法的代码并不复杂,一个递归执行队列的方法,使用iterator(fn参数)执行queueiterator里给hook传入的参数分别代表tofromnext,在队列执行完后执行传入的回调方法。这里执行过程代表了vue-router的守卫函数的执行函数。

export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => {
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}
复制代码

综上所述,路由跳转的变化大概上已经解释完,当然这并不是完整的执行逻辑,只是路由跳转大概的过程差不多就是如此。

转载于:https://juejin.im/post/5d05ab27518825064000762c

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值