vue源码 | 导航守卫的整体逻辑

Posted winter

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue源码 | 导航守卫的整体逻辑相关的知识,希望对你有一定的参考价值。

一.结论先行

导航守卫其实就是将导航守卫的钩子函数放到一个数组queue中,然后通过runQueue一个一个执行,并且只有手动调用next()之后才会跳到下一个函数执行。

二、流程

1.VueRouter

//【代码块1】
//代码所在目录:src/index.js
export default class VueRouter {
    //...
    //matcher章节讲过createMatcher
    this.matcher = createMatcher(options.routes || [], this)
    //...
    //match章节讲过
    match (
        raw: RawLocation,
        current?: Route,
        redirectedFrom?: Location
    ): Route {
        return this.matcher.match(raw, current, redirectedFrom)
    }
    //...
    init(app){
        //...
        if(history instanceof html5History){
           //路径切换的一个入口,另一个入口是push()/replace()[this.history.push()/replace()]
           // transitionTo见【代码块2】
           history.transitionTo(history.getCurrentLocation()) 
        }
    }
}

2.History对象中的整体流程

//【代码块2】
//代码所在目录:src/history/base.js
export class Hisroty{
    //..
    current: Route;
    //...
    
    //在路由初始化的时候就会执行一次history.transitionTo()
    //同时路由切换的另一个入口push()/replace()方法里面也会执行transitionTo
    transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    //location为新的跳转地址,比如: \'/add\'
    //onComplete是跳转成功的回调
    //onAbort是跳转失败的回调

    //this.router.match是调用的matcher的match方法,上一章讲过,match方法在src/index.js中class VueRouter中定义
    //match方法返回一个route对象,route对象的属性比较丰富,比如:
    //fullPath、hash、matched(是一个对象,里面又包含了path、regex等属性)、meta、name、params、path、query等

    //这里传入的参数是location(要跳转的地址)、this.currrent(当前的路径)计算出一个新的路径
    //计算方法见match方法,大致如下
    //1.将location通过nomalizeLocation变为、一个有hash、path、query、_normalized等属性的对象
    //2.通过这个新对象去前面生成的路由映射表中匹配对应的路由对象record
    //3.在将record稍微做一些处理就得到了route,并利用freeze冻结之后返回
    const route = this.router.match(location, this.current)
    //comfirmTransition()方法完成一次真正的路径切换(见下文)
    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) })
      }
    })
  }
  
  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {

    //this.current是History对象需要维护的一个当前的路由对象,即不管我们切换多少次路径,在路劲切换成功后,都会改变当前的current
    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判断我当前的路径和我要跳转的路径是不是同一个路径(详情可以见【代码块3】)
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      route.matched.length === current.matched.length
    ) {

      //如果为像她的地址,就不需要执行操作,直接取消跳转就行
      //ensureURL后面讲,与URL有关
      this.ensureURL()
      return abort()
    }

    //导航守卫相关逻辑

    const {
      updated,
      deactivated,
      activated
    } = resolveQueue(this.current.matched, route.matched)
    //resolveQueue的参数值得看一下
    //首先this.current和route我们前面已经知道他们是Route类型的数据,而Route类型是由一个matched的属性的
    //详情见【代码块4】
    //结果:matched是一个存储了当前路径到根路径的所有路径的数组(通过不断的变量.parent)(顺序为先父后子)

    //resolveQueue见【代码块5】

    //结果: 
    //updated保存的是新老路径相同的部分
    //activated保存的是新路径新增的部分
    //deactivated保存的是老路径删除的部分

    //举例 /foo/bar==>/foo   updated: /foo的record  activated: 空  deactivated: /bar的record
    //    /foo==>/foo/bar   updated: /foo的record  activated: /bar的record   deactivated: 空
    
    //queue是一个NavigationGuard类型的数组
    //NavigationGuard类型见【代码块6】
    //结果,NavigationGuard是一个函数,有三个参数:to、from、next(next也是一个函数),to和from都是Route类型
    //利用[].concat将下面这个函数或者异步钩子拍平到一个一维数组中,即queue中
    const queue: Array<?NavigationGuard> = [].concat(
       // #完整的导航解析流程
      //     导航被触发。
      //     在失活的组件里调用 beforeRouteLeave 守卫。
      //     调用全局的 beforeEach 守卫。
      //     在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
      //     在路由配置里调用 beforeEnter。
      //     解析异步路由组件。
      //     在被激活的组件里调用 beforeRouteEnter。
      //     调用全局的 beforeResolve 守卫 (2.5+)。
      //     导航被确认。
      //     调用全局的 afterEach 钩子。
      //     触发 DOM 更新。
      //     调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

      //这里面的具体过程太绕了,先跳过,以后有精力再仔细看
      extractLeaveGuards(deactivated),   //在失活组件中调用离开守卫  拿到组件定义中beforeRouteLeave的定义
      // global before hooks
      this.router.beforeHooks,  //调用全局的beforeEach守卫     调用beforeEach的时候会往beforeHooks中添加一个
      // in-component update hooks
      extractUpdateHooks(updated),  //在重用的组件里调用beforeRouteUpdate守卫  拿到组件定义中beforeRouteUpdate的定义
      // in-config enter guards
      activated.map(m => m.beforeEnter),  //在路由配置里调用 beforeEnter  
      // async components
      resolveAsyncComponents(activated)  //解析异步路由组件
    )

    this.pending = route

    //定义了一个迭代器函数
    const iterator = (hook: NavigationGuard, next) => {
      if (this.pending !== route) {
        return abort()
      }
      try {
        //hook是NavigationGuard类型的一个函数,NavigationGuard类型前面提到过有to、from、next三个参数
        //所以route对应to,current对应from, to对应next函数
        //有runQueue调用中可以看出hook为queue中的一个函数,在导航守卫中即导航守卫相关的钩子函数,
        //而这些构造函数正好也对应了to、from和next三个参数。
        //当to为false的时候,就会取消路由跳转,当false为字符串、对象之类的时候,跳转路由,并取消,否则执行next(to),
        //也就是执行我们在钩子函数中的next中写的东西,在里面我们会手动调用一个next,这个next的执行就是iterator的第二个参数next,
        //也就是对应了async.js中runQueue中执行的step+1,即跳到下一个函数执行
        //所以如果我们不手动调用next,就不会执行队列中的下一个函数,程序就会卡住
        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
    //runQueue定义见【代码块7】
    //结论:queue里面其实是一些导航守卫相关的钩子函数,而runQueue就会以队列的形式
    //一个一个的执行queue里面的函数(具体执行方式是执行上面的迭代)
    //当一个函数执行完之后,并在回调函数里面手动调用next()才会跳到下一个函数执行。
    //也就是跳到下一个函数的掌控权在自己手中,不管在回调中同步还是异步的调用next,总之只有当手动调用了next()才会执行下一个函数
    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() })
          })
        }
      })
    })
  }

  updateRoute (route: Route) {
    const prev = this.current
    this.current = route
    this.cb && this.cb(route)
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }
}

3.判断新旧路径是否相同

//【代码块3】
//代码所在目录:src/util/route.js

export function isSameRoute (a: Route, b: ?Route): boolean {
  if (b === START) {
    return a === b
  } else if (!b) {
    return false
  } else if (a.path && b.path) {
    return (
      a.path.replace(trailingSlashRE, \'\') === b.path.replace(trailingSlashRE, \'\') &&
      a.hash === b.hash &&
      isObjectEqual(a.query, b.query)
    )
  } else if (a.name && b.name) {
    return (
      a.name === b.name &&
      a.hash === b.hash &&
      isObjectEqual(a.query, b.query) &&
      isObjectEqual(a.params, b.params)
    )
  } else {
    return false
  }
}

4.Route对象的定义及route对象的matched属性

//【代码块4】
//代码所在目录:src/util/route.js
  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || \'/\',
    hash: location.hash || \'\',
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
    //matched属性:我们在执行createRoute的时候会传入一个匹配到的路由对象record,然后通过record计算出matched
    //具体计算方法见formatMatch方法(下文)
    //结果:matched是一个存储了当前路径到根路径的所有路径的数组(通过不断的变量.parent)(顺序为先父后子)
  }
  
  //farmatMatch方法即通过当前的record向上遍历它的parent,把所有的parent遍历出来放入到一个数组中并返回
  //注意这个数组中的顺序的先父后子(unshift)
  //即记录到当前路径到根路径的所有路径
  function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
    const res = []
    while (record) {
      res.unshift(record)
      record = record.parent
    }
    return res
  }

5.通过新旧路径找到不同

//【代码块5】
//代码所在目录:src/history/base.js
function resolveQueue (
  current: Array<RouteRecord>,
  next: Array<RouteRecord>
): {
  updated: Array<RouteRecord>,
  activated: Array<RouteRecord>,
  deactivated: Array<RouteRecord>
} {
  let i
  //由前面matched属性的分析我们可以知道current是一个存储了当前路径到其根路径的所有路径的record的数组,顺序为先父后子
  //next为我们要去到的地址到其根路径的所有路径的record的数组,属性为先父后子

  //举例: /foo ==>  /foo/bar
  //curent:[/foo的record]
  //next:[/foo的record, /bar的record]

  //取到两个数组的最长长度,例子中为2
  const max = Math.max(current.length, next.length)
  //找打第一个不同的地方就跳出,所以这里的i为1就跳出了
  for (i = 0; i < max; i++) {
    if (current[i] !== next[i]) {
      break
    }
  }
  return {
    updated: next.slice(0, i),  //  /foo的record
    activated: next.slice(i),   //  /bar的record
    deactivated: current.slice(i)  //  空
  }

  //所以updated保存的是当前路径与要去到的路径之间相同的部分
  //activated保存的是新路径相较于老路径多出来的部分
  //deactivated保存的是老路径删除的部分   (比如/foo/bar ==> /foo deactivated就是/bar)
}

6.NavigationGuard类型

//【代码块6】
//代码所在目录:flow/declarations.js
declare type NavigationGuard = (
  to: Route,
  from: Route,
  next: (to?: RawLocation | false | Function | void) => void
) => any

7.runQueue方法

//【代码块7】
//代码所在目录:src/util/async.js
//参数queue是一个由异步函数啊钩子函数啊之类的函数组成的一个一维数组
//上面这些函数也就是导航守卫相关的一些钩子函数
//参数fn是base.js中调用runQueue的位置上面定义的迭代器iterator
//cb是一个回调函数,详细见runQueu调用的地方[base.js]
//runQueue是一个比较经典的队列的实现,相当于上面这个函数一个一个执行,这一个执行完,再跳到下一个执行

export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => {
    //index>=queue.length即队列中的所有函数都执行完了,就调用第三个参数这个回调函数
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        //如果当前索引对应的函数存在,就执行fn(即传入的迭代器),见base.js
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}

以上是关于vue源码 | 导航守卫的整体逻辑的主要内容,如果未能解决你的问题,请参考以下文章

vue-router路由导航守卫

vue-router路由导航守卫

Vue 导航守卫

Vue 教程(四十五)Vue 导航守卫

Vue 教程(四十五)Vue 导航守卫

vue导航守卫和axios拦截器的区别