vue-router源码分析(三)

484 阅读1分钟

四、路径切换和导航守卫

vue-router在全局mixin混入的beforeCreate会执行,init方法,init方法会执行history.transitionTo进行路由的跳转,同样当执行this.$router.push方法,也同样会去执行transitionTotransitionTo函数首先用current变量定义了当前的route,然后通过this.pending保留传入的路径。然后保留了,route.matched.length - 1current.matched.length - 1,route.matchedcreateRoute函数中的定义是 record ? formatMatch(record) : []formatMatch函数的目的是创建一个res数组,把当前的record,unshift到res这个数组中,如果发现当前的record有parent,那么会递归执行,也就是说matched保留了当前route的record和他所有的父路由的record。之后会做一个判断。首先通过isSameRoute(route, current)来判断当前的route和传入的route是否是相同,然后比较刚才保留的length是否相同,以及他们对应的最后一位的record是否相同,如果相同的话抛出异常,然后通过resolveQueue( this.current.matched, route.matched )拿到要触发updated, deactivated, activated生命周期的路由,resolveQueue函数接收两个参数,当前的route的matched和要跳转路由的matched,通过for循环比对路由在哪个index发生了不同,以此index,划分出哪些route是更新的,哪些是销毁的,些是创建的。然后创建了queue来记录类型为NavigationGuard的数组,在types/router.d.ts下可以看到,NavigationGuard是接受了to,form,next三个参数的函数,这其实和router.beforeEach这样的导航守卫钩子传入的函数是相同的。之后定义了iterator这样一个迭代器函数,最后执行了runQueue这个函数

// src/history/base.js
export class History {
  ...
  transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    let route
    // catch redirect option https://github.com/vuejs/vue-router/issues/3201
    try {
      route = this.router.match(location, this.current)
    } catch (e) {
      this.errorCbs.forEach(cb => {
        cb(e)
      })
      // Exception should still be thrown
      throw e
    }
    const prev = this.current
    this.confirmTransition(
      route,
      ...
    )
  }

  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    this.pending = route
    const abort = err => {
      ...
    }
    const lastRouteIndex = route.matched.length - 1
    const lastCurrentIndex = current.matched.length - 1
    if (
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      lastRouteIndex === lastCurrentIndex &&
      route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
    ) {
      this.ensureURL()
      return abort(createNavigationDuplicatedError(current, route))
    }

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

    const queue: Array<?NavigationGuard> = ...

    const iterator = (hook: NavigationGuard, next) => {
      ...
    }
    runQueue(queue, iterator,...)
  }
}
...

runQueue函数中定义了step为一个匿名函数,这个匿名函数接受一个index,如果index>= queue.length也就是队列中的全部执行完,会执行传入的cb回调函数。否则,根据传入的当前index,把queue中的数据作为参数,去执行传入的fn函数,fn函数执行完毕,让index+1,再去执行step,这是比较典型的一个队列的实现。

// src/util/async.js
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)
}

runQueue的第二个参数传入的是之前提到的iterator,iterator的第一个参数是传的的queue[index],也就是NavigationGuard类型的函数,对应到我们导航守卫钩子beforeEach的三个参数,to,form和next,当我们调用beforeEach的next函数,那么会触发iterator中定义的的next函数,如果执行没有问题,那么最终会调用hook函数的next(to),也就是iterator传入的第二个参数next,也就是runQueue函数中的fn的第二个参数, step(index + 1),所以在导航守卫中为什么一定需要执行next,是因为如果不执行next,那么就不会执行queue数组之后数据的处理。

// src/history/base.js
const iterator = (hook: NavigationGuard, next) => {
    ...
    try {
      hook(route, current, (to: any) => {
        if (to === false) {
          // next(false) -> abort navigation, ensure current URL
          this.ensureURL(true)
          abort(createNavigationAbortedError(current, route))
        } else if (isError(to)) {
          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(createNavigationRedirectedError(current, route))
          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)
    }
  }

queue通过类型Array<?NavigationGuard>可以判断出他是一个导航守卫钩子组成的数组,那么queue是如何生成的,他首先调用了extractLeaveGuards函数,把deactivated也就是要销毁的record队列传入,extractLeaveGuards函数返回extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)是一个函数数组,vue-router的官方文档是这样描述导航守卫的

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
    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)
    )

extractGuards中文意思是解析守卫,首先会调用flatMapComponents函数,flatMapComponents函数第一个参数传入records,第二个参数传入函数,flatMapComponents函数会拿到records进行map操作,取到records中的components进行遍历,执行fn(m.components[key],m.instances[key],m,key),components[key]就是命名视图中的具体component,m.instances[key]是具体的import的对象,m就是当前的record,key就是components定义的key(默认为default)。flatMapComponents的第二个参数是一个函数,这个函数先调用了extractGuardextractGuard函数会判断传入的def,也就是import的内容,判断他是否是一个函数,如果不是函数会通过_Vue.extend把他扩展成一个构造函数,然后通过传入的name,去尝试拿到他定义的生命周期函数,在extractLeaveGuards函数中,name是beforeRouteLeave也就是说,他会尝试拿到deactivatedrecord数组中的beforeRouteLeave这个生命周期,如果拿到的话,那么会去执行bind(guard, instance, match, key),在extractLeaveGuards中,bind是定义的bindGuard函数,bindGuard函数首先会判断是否有instance也就是VueComponent,如果有,那么会返回boundRouteGuard函数。flatMapComponents函数执行完,最终返回这样的拥有boundRouteGuard函数的一维数组,在extractGuards最后,因为extractLeaveGuards执行extractGuards传入的第四个参数reverse为true,所以会return guards.reverse()deactivatedrecord数组的顺序是先父后子,所以最终reverse之后,extractLeaveGuards的顺序是先子后父,也就是说queue执行的时候会先触发子组件的beforeRouteLeave,然后再触发父组件的beforeRouteLeave,这样就如同官方的描述相同 在触发导航守卫,接着会在失活的组件里调用 beforeRouteLeave 守卫,对应的就是queue数组中的第一个值extractLeaveGuards(deactivated)

// src/history/base.js
function extractGuards (
  records: Array<RouteRecord>,
  name: string,
  bind: Function,
  reverse?: boolean
): Array<?Function> {
  const guards = flatMapComponents(records, (def, instance, match, key) => {
    const guard = extractGuard(def, name)
    if (guard) {
      return Array.isArray(guard)
        ? guard.map(guard => bind(guard, instance, match, key))
        : bind(guard, instance, match, key)
    }
  })
  return flatten(reverse ? guards.reverse() : guards)
}

function extractGuard (
  def: Object | Function,
  key: string
): NavigationGuard | Array<NavigationGuard> {
  if (typeof def !== 'function') {
    // extend now so that global mixins are applied.
    def = _Vue.extend(def)
  }
  return def.options[key]
}

function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

// src/util/resolve-components.js
export function flatMapComponents (
  matched: Array<RouteRecord>,
  fn: Function
): Array<?Function> {
  return flatten(matched.map(m => {
    return Object.keys(m.components).map(key => fn(
      m.components[key],
      m.instances[key],
      m, key
    ))
  }))
}

queue的第二个值是this.router.beforeHooks,VueRouter实例方法beforeEach会执行registerHook(this.beforeHooks, fn)registerHook函数会往传入的this.beforeHooks数组push传入的回调函数。也就是定义的beforeEach函数的回调,作为了queue的第二个值,这也对应了3. 调用全局的 beforeEach 守卫。。queue的第三个值extractUpdateHooks(updated)和第一个值 extractLeaveGuards(deactivated)的逻辑几乎是相同的,他去寻找的是updatedrecord数组中,带有beforeRouteUpdate生命周期的record也就是对应4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。第四个值 activated.map(m => m.beforeEnter),也就是对新创建的record执行beforeEnter,对应5. 在路由配置里调用 beforeEnter。第五个值resolveAsyncComponents(activated)resolveAsyncComponents函数也是返回了一个导航守卫的函数,返回的这个函数也是通过flatMapComponents,他会遍历record所有的components,去判断 if (typeof def === 'function' && def.cid === undefined)这说明这是一个异步组件,执行的逻辑和之前提到的异步组件实现的原理是相同的。执行完resolve之后,他会重新定义给match.components[key],最终全部执行完成后,会执行next,也就是说这个next是异步的,因为他可能会遇到解析懒加载的组件,这也对应了6. 解析异步路由组件

export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
  return (to, from, next) => {
    let hasAsync = false
    let pending = 0
    let error = null

    flatMapComponents(matched, (def, _, match, key) => {
      // if it's a function and doesn't have cid attached,
      // assume it's an async component resolve function.
      // we are not using Vue's default async resolving mechanism because
      // we want to halt the navigation until the incoming component has been
      // resolved.
      if (typeof def === 'function' && def.cid === undefined) {
        hasAsync = true
        pending++

        const resolve = once(resolvedDef => {
          if (isESModule(resolvedDef)) {
            resolvedDef = resolvedDef.default
          }
          // save resolved on async factory in case it's used elsewhere
          def.resolved = typeof resolvedDef === 'function'
            ? resolvedDef
            : _Vue.extend(resolvedDef)
          match.components[key] = resolvedDef
          pending--
          if (pending <= 0) {
            next()
          }
        })

        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 {
          res = def(resolve, reject)
        } catch (e) {
          reject(e)
        }
        if (res) {
          if (typeof res.then === 'function') {
            res.then(resolve, reject)
          } else {
            // new syntax in Vue 2.3
            const comp = res.component
            if (comp && typeof comp.then === 'function') {
              comp.then(resolve, reject)
            }
          }
        }
      }
    })

    if (!hasAsync) next()
  }
}