# $route对象生成的时机

由以前的分析可以知道,我们在钩子中会使用init 方法会初始化整个 vue-router ,而实例化和初始化 vue-router 是有区 别的,实例化指的是通过 new Router 生成 vue-router 实例,初始化可以理解为进行全局第一次的路由跳转时, 让vue-router 实例和组件建立联系,使得路由能够接管组件

接下来我们来看 vue-router 是如何初始化的

// 初始化router实例
init (app: any /* Vue component instance */) {
  // app为在router对象初始化时执行init方法的参数(根实例),将根实例添加到apps数组中(用于多次执行VueRouter创建多个实例,比较少用)
  this.apps.push(app)
  // 保证app属性只有唯一一个
  if (this.app) {
    return
  }
  this.app = app
  const history = this.history
  // transitionTo这个方法,它是整个路由跳转的核心方法
  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation())
  } else if (history instanceof HashHistory) {
    const setupHashListener = () => {
      history.setupListeners()
    }
    history.transitionTo(
      history.getCurrentLocation(),// 例如获取hash值后面的路径
      setupHashListener, //成功回调(给哈希路由的模式监听浏览器的popState和hashchange)
      setupHashListener //取消回调
    )
  }
  // 注册回调,当history发生改变后会执行回调(src/history/base.js:221)
  // 即修改_route属性,因为_route属性是一个视图依赖的响应式变量,所以会触发视图的重新渲染
  // 至于触发 _route 的 setter 为什么会更新视图,请参考 router-view 组件
  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}

由上面的分析可以知道,transitionTo是路由跳转的核心方法

/**vue-router路由跳转的核心逻辑
 * 执行所有的路由钩子
 * 解析异步路由组件
 * **/
transitionTo (/*跳转的路由信息*/location: RawLocation,/*成功回调*/ onComplete?: Function, onAbort?: Function) {
  // this是history路由实例(HashHistory | HTML5History)
  // this.router是vueRouter实例
  // match方法会根据当前的location + 之前生成的路由映射表(nameMap,pathMap),生成$route对象(src/create-matcher.js:31)
  // current是切换前的$route对象
  // 这个match方法就会创建出一个$route对象,随后会进入confirmTransition这个方法,负责控制所有的路由守卫的执行
  const route = this.router.match(location, this.current)
  // transitionTo的核心,执行一系列路由钩子
  // 传入route对象,成功回调和失败回调
  this.confirmTransition(route, () => {
    this.updateRoute(route) //确认导航成功,更新视图以及执行afterEach钩子

    // 执行transitionTo成功的回调(src/index.js:116)
    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) })
    }
  })
}

使用以前的例子,当我们直接在浏览器的 url 中输入http://localhost:8080/#/comp1/comp1Child 时,可以观察到 location 参数为跳转目标的路径,并且此时是全局第一次调用 transitionTo 方法,vue-router 默认第一次跳转的current 参数为根路径的 $route 对象,而以后的跳转,current 会变成当前路由的 $route 对象

// 第一次 history.current 值为根路径转换而来的 $route 对象
constructor (router: Router, base: ?string) {
  this.current = START
}
export const START = createRoute(null, {
  path: '/'
})

# 路由守卫的原理

和组件的生命周期的钩子不同,路由守卫将重点放在路由上,能够控制路由跳转,一般用在页面级别的路由跳转时控制跳转的逻辑,比 如在路由守卫中检查用户是否有进入当前页面的权限,没有则跳转到授权页面,亦或是在离开页面时警告用户有未确认的信息,确认后 才能跳转等等

在路由守卫中,一般会接收3个参数,to,from,next,前两个分别是跳转后和跳转前页面路由的 $route 对象,第 三个参数 next 是一个函数,当执行 next 函数后会进行跳转,如果一个包含 next 参数的路由守卫里没有执行该函数,页面会无 法跳转,接下来我们来解密路由守卫背后的原理

// transitionTo的核心,执行一系列路由钩子
// 传入route对象,成功回调和失败回调
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
  const current = this.current //切换前的route对象
  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()
  }

  /**计算出当前路由和跳转路由在路径上的相同点不同点(路由记录),来执行不同的导航守卫
   * 这个函数的作用是根据跳转前和跳转后 $route 对象的 matched 数组,返回这2个数组包含的路由记录的区别
   * $route 对象的 matched 属性是一个数组,通过 formatMatch 函数最终返回 $route 对象以及所有父级的路由记录
   * 返回3个数组,updated 代表跳转前后 matched 数组相同部分,deactivated 代表删除部分,activated 代表新增部分
   * 
   * 举个例子,当我们从 comp1Child 页面跳转到 comp2 页面,这3个数组分别对应的值
   * 跳转前[{comp1Record}, {comp1ChildRecord}]
   * 跳转后[{comp2Record}]
   * 
   * 相同部分: []
   * 新增部分:[{comp2Record}]
   * 删除部分:[{comp1Record}, {comp1ChildRecord}]
  */
  const {
    updated,
    deactivated,
    activated
    // this.current指的是当前路由,route是跳转路由
  } = resolveQueue(this.current.matched, route.matched)

  // queue是NavigationGuard组成的数组, NavigationGuard是路由守卫的函数,传入to,from,next3个参数
  // 经过 queue 数组内部这些函数的转换最终会返回路由守卫组成的数组,而这些函数就是将上节中的路由记录转换为路由守卫的函数
  // 同时数组中的守卫的排列顺序也是设计好的,对应开发文档中的顺序
  // https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E7%BB%84%E4%BB%B6%E5%86%85%E7%9A%84%E5%AE%88%E5%8D%AB
  const queue: Array<?NavigationGuard> = [].concat(
      // in-component leave guards
    extractLeaveGuards(deactivated), //返回离开组件的 beforeRouteLeave 钩子函数(数组,子 => 父)
    // global before hooks
    this.router.beforeHooks, //返回路由实例(全局)的 beforeEach 钩子函数(数组) (src/index.js:128)
    // in-component update hooks
    extractUpdateHooks(updated), //返回当前组件的 beforeRouteUpdate 钩子函数(数组,父 => 子),
    // in-config enter guards
    activated.map(m => m.beforeEnter), //返回当前组件的 beforeEnter 钩子函数(数组),
    // async components
    resolveAsyncComponents(activated)  // 解析异步组件(同样会返回一个导航守卫函数的签名,但是用不到 to,from 这 2 个参数)
  )

  this.pending = route

  //runQueue每次遍历都会执行iterator函数并且传入当前的路由守卫函数进行解析,解析后会执行next回调(即step+1)
  const iterator = (hook: NavigationGuard, next) => {
    if (this.pending !== route) {
      return abort()
    }
    try {
      //执行某个生命周期中的导航守卫
      hook(route, current, /*iterator next*/(to: any) => {
        if (to === false || isError(to)) {
          // next(false) -> abort navigation, ensure current URL
            // 如果传入的是next(false)会中断导航,并且会重置到form的路由
          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() //取消导航并且执行push/replace跳转到指定路由
          if (typeof to === 'object' && to.replace) {
            this.replace(to)
          } else {
            this.push(to)
          }
        } else {
          // confirm transition and pass on the value
          // 如果next没有参数则直接执行runQueue next
          // 即解析queue的下个元素
          next(to)
        }
      })
    } catch (e) {
      abort(e)
    }
  }

  // 等到队列中所有的组件(懒加载的组件)都解析完毕后,就会执行第三个参数回调
  // 即为什么beforeRouteEnter钩子需要在next回调中执行的原因
  runQueue(queue, iterator, /*队列遍历结束后,执行异步组件的回调(此时懒加载组件以及被解析完毕)*/() => {
    const postEnterCbs = [] // 保存beforeRouterEnter的next回调
    const isValid = () => this.current === route
    // wait until async components are resolved before
    // extracting in-component enter guards
    //返回当前组件的 beforeRouteEnter 钩子函数(数组)
    const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
    //将 beforeResolve 钩子放到 beforeRouteEnter 钩子数组的后面依次执行
    const queue = enterGuards.concat(this.router.resolveHooks)
    // 遍历队列执行 beforeRouteEnter 和 beforeResolve 钩子
    runQueue(queue, iterator, () => {
      if (this.pending !== route) {
        return abort()
      }
      this.pending = null
      // 确认导航,执行onComplete回调,包含:
      // $nextTick 后更新视图
      // 执行afterEach钩子(74)
      onComplete(route)
      if (this.router.app) {
        /**在nextTick后执行 postEnterCbs 数组即 beforeRouteEnter 的next方法的参数(函数)**/
        /**因为此时 nextTick 队列中存在一个 render watcher 所以先执行 render watcher 更新视图,再执行 beforeRouteEnter 的回调**/
        // 因此 beforeRouteEnter 需要通过回调传入this的值
        this.router.app.$nextTick(() => {
          postEnterCbs.forEach(cb => { cb() })
        })
      }
    })
  })
}

我们先分析 queue 数组里第一个执行的函数 extractLeaveGuards,经过一层封装最终会执行通用函数 extractGuards

// 此时 records 参数为删除的路由记录,name 为 beforeRouteLeave,即最终触发的是 beforeRouteLeave 守卫
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
// 根据records数组,返回当前这个组件对应的某个生命周期的路由守卫(数组)
function extractGuards (
  records: Array<RouteRecord>,
  name: string,
  bind: Function,
  reverse?: boolean
): Array<?Function> {
  // 扁平化 + 数组Map
  const guards = flatMapComponents(records, (def, instance, match, key) => {
    // 通过name(路由守卫的名字),获取到当前组件对应的路由守卫函数
    const guard = extractGuard(def, name)
    if (guard) {
      return Array.isArray(guard)
          // 绑定上下文this,传入当前路由守卫函数,实例,record和视图名字
        ? guard.map(guard => bind(guard, instance, match, key))
        : bind(guard, instance, match, key)
    }
  })
  // 倒序数组,之前是父=>子,如果reverse为true则为子 => 父
  // 对于离开某个路由时,由于子路由需要先离开所以要倒序数组,让子组件先触发beforeLeave钩子
  return flatten(reverse ? guards.reverse() : guards)
}

// 扁平化后执行fn作为返回值
function flatMapComponents (
  matched: Array<RouteRecord>,
  fn: Function
): Array<?Function> {
  // 数组扁平化
  return flatten(matched.map(m => {
    // 遍历components属性(一般为component,vue-router会把component变成components,因为有命名视图的可能)
    // 如果是component衍变的key为default,否则为自己定义的key值
    return Object.keys(m.components).map(key => fn(
        m.components[key], // 组件(key一般为default),当是路由懒加载时这个值为函数(()=> import(.....))
        m.instances[key], // 实例(实例默认为空对象,在registerInstance时,会在router-view中创建组件实例) (src/components/view.js:58)
        m, //路由记录
        key //视图名(一般为default)即使用默认组件
    ))
  }))
}
//def 为组件配置项,通过 Vue 核心库的函数 extend 将配置项转为组件构造器(虽然配置项中就能拿到对应的路由守卫,但是从
//官方注释发现只有转为构造器后才能拿到一些全局混入的钩子),在生成构造器时,Vue 会将配置项赋值给构造器的静态属
//性options最后返回配置项中对应的路由守卫函数,即如果我们在跳转后的组件中定义了 beforeRouteLeave 的话这里就会返回这个函数
function extractGuard (
  def: Object | Function,
  key: string //路由钩子的name
): NavigationGuard | Array<NavigationGuard> {
  // 非懒加载
  if (typeof def !== 'function') {
    // extend now so that global mixins are applied.
      /**将配置项变成组件构造器**/
    def = _Vue.extend(def)
  }
  return def.options[key] //返回组件构造器options配置项中对应的路由钩子函数
}

# 路由懒加载的原理

通俗来说就是使用路由懒加载返回的路由,我们可以使用 import () 这种语法去动态的加载 JS 文件,放到 vue-router 中,就 可以实现异步加载组件配置项(这里只讨论开发中使用较多的 import() 语法), 上面执行路由守卫最后一段queue就是异步组件 component:()=>import('./components/comp1')

//解析异步路由(传入当前新增的路由记录)
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
  return (to, from, next) => {
    let hasAsync = false
    let pending = 0
    let error = null

    flatMapComponents(matched, /*解析matched中的异步组件*/(def, _, match, key) => {
      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) // 这个组件构造器不知道哪里使用的。。。。
            // 将解析后的组件配置项赋值给路由中components属性(将组件配置项覆盖原来的()=>import(.....))
          match.components[key] = resolvedDef
          pending--
          if (pending <= 0) {
            // 当匹配到的route中的 matched属性里记录的路由组件都被解析成功后,执行iterator next ,在 runQueue 中解析 queue 的下个元素
            // iterator next(src/history/base.js:154)
            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)
            // 发生错误时,执行iterator next,最终会中断导航
            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()
  }
}
  • resolveAsyncComponents 函数最终会返回一个函数,并且符合路由守卫的函数签名(这里 vue-router 可能只是为 了保证返回函数的一致性,实质上在这个函数中,并不会用到 to,from 这2个参数)
  • 这个函数只是被定义了,并没有执行,但是我们可以通过函数体观察它是如何加载异步路由的。同样通过 flatMapComponents 遍历新增的路由记录,每次遍历都执行第二个回调函数
  • 在回调函数里,会定义一个 resolve 函数,当异步组件加载完成后,会通过 then 的形式解析 promise,最终会调用resolve 函数并传入异步组件的配置项作为参数, resolve 函数接收到组件配置项后会像 Vue 中一样将配置项转为构造器 ,同时将值赋值 给当前路由记录的 componts 属性中(key 属性默认为 default)
  • 另外 resolveAsyncComponents 函数会通过闭包保存一个 pending 变量,代表接收的异步组件数量,在flatMapComponents 遍历的过程中,每次会将 pending 加一,而当异步组件被解析完毕后再将 pending 减一,也就是说,当 pengding 为 0 时,代 表异步组件全部解析完成, 随即执行 next 方法,next 方法是 vue-router 控制整个路由导航顺序的核心方法

# 执行路由守卫

在分析 next 方法之前,我们先来看一下 vue-router 是如何处理 queue 数组中的元素的,在上文中,虽然定义了 queue 数 组,其中包括了路由守卫以及解析异步组件的函数,但是还没有执行,

// 因为路由之间的切换可能是异步的(可能会写setTimeout(next,2000))
// 所以设计了一个队列,当前面一个元素被解析后调用next方法才继续解析下个元素
export function runQueue (queue: Array<?NavigationGuard>, /*fn指iterator*/fn: Function, cb: Function) {
  const step = index => {
    if (index >= queue.length) {
      cb() //遍历结束后执行回调
    } else {
      if (queue[index]) {
          // queue[index]即hook函数
          // 第二个参数即 next 方法,解析队列中下个元素
          // 执行iterator函数,传入NavigationGuard(函数)组成的数组的每个元素,执行完后执行回调(index+1)
        fn(queue[index], /*runQueue next*/() => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}

runQueue 内部声明了一个 step 的函数,它一个是控制 runQueue 是否继续遍历的函数,当我们第一次执行时,给 step 函数 传入参数 0 表示开始遍历 queue 第 1 个元素,通过 step 函数内部可以发现,它最终会执行参数 fn,也就是 iterator 这 个迭代器函数,给它传入当前遍历的 queue 元素以及一个回调函数,这个回调函数里保存着遍历下个元素的逻辑,也就是说runQueue 将是否需要继续遍历的控制权传入了 iterator 函数中

runQueue 函数只负责遍历数组,并不会执行逻辑,它依次遍历 queue 数组的元素,每次遍历时会将当前元素交给外部定义的 iterator 迭代器去执行,而 iterator 迭代器一旦处理完当前元素就让 runQueue 遍历下个元素,且当数组全部遍历结束 时,会执行作为回调的参数 cb

接下来我们看下迭代器函数

//runQueue每次遍历都会执行iterator函数并且传入当前的路由守卫函数进行解析,解析后会执行next回调(即step+1)
const iterator = (hook: NavigationGuard, next) => {
  if (this.pending !== route) {
    return abort()
  }
  try {
    //执行某个生命周期中的导航守卫,其实这里就是对应我们自己写的钩子中to from next三个参数
    //当在路由守卫中如果没有执行 next 函数,路由将无法跳转,原因是因为没有去执行 hook 的第三个
    //回调函数,也就不会执行 iterator 的第三个参数 next,最终导致不会通知 runQueue 继续往下遍历
    hook(route, current, /*iterator next*/(to: any) => {
      // 另外当我们给 next 函数传入另一个路径时,会取消原来的导航,取而代之跳转到指定的路径,原
      // 因是因为满足上图的 true 逻辑,执行 abort 函数取消导航,随后会调用 push/replace 将路由重新跳转到指定的页面
      if (to === false || isError(to)) {
        // next(false) -> abort navigation, ensure current URL
          // 如果传入的是next(false)会中断导航,并且会重置到form的路由
        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() //取消导航并且执行push/replace跳转到指定路由
        if (typeof to === 'object' && to.replace) {
          this.replace(to)
        } else {
          this.push(to)
        }
      } else {
        // confirm transition and pass on the value
        // 如果next没有参数则直接执行runQueue next
        // 即解析queue的下个元素
        next(to)
      }
    })
  } catch (e) {
    abort(e)
  }
}

当 queue 最后一个元素也就是异步组件被解析完成后,runQueue 会执行传入的第三个参数,即执行遍历成功回调,分析代码可知 成功回调里 vue-router 又往 queue 中添加了路由守卫,同时会开启第二轮遍历,这个下期讲解.