乐趣区

关于前端: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)
}
退出移动版