Vue-router 源码解析

Vue-Router

Vue-Router是vue的路由插件,能够方便我们进行路由管理,是单页面开发不可或缺的一部分。vue-router使用相对简单,编码格式化合理,能够极大地帮助我们开发单页面应用。
tips:本文不讲解vue-router的基本使用,只对源码进行解析
vue-router官方指导网站:点我

源码结构以及状态图

vue-router源码文件较多,但有部分是功能性模块,层次分明
在这里插入图片描述
关键文件解析:

源码结构:
	components:
		      link.js:RouterView组件定义
		      view.js:RouterLink组件定义
	history:
		      abstrct.js:AbstractHistory类定义
		      hash.js:HashHistory类定义
		      html5.js:Html5History类定义
		      base.js:History类定义
		      errors.js
	utils:
		      route.js:createRouter路由生产者
		      其他
	index.js:VueRouter类定义
	create-matcher.js:Matcher生产者
	create-router-map.js:创建路由映射表
	install.js:vue插件安装调用

原理结构图:
在这里插入图片描述

install.js

对于Vue插件,都是通过Vue实例创建调用install.js文件进行安装,而vue-router的install主要完成了5件事:

* Vue.use(插件)来注册插件的时候会找到插件的install方法进行执行
 * install主要有以下几个目的:
 *    (1)子组件通过从父组件获取router实例,从而可以在组件内进行路由切换、在路由钩子内设置callback、获取当前route等操作,所有router提供的接口均可以调用
 *    (2)设置代理,组件内使用this.router就等同于获得router实例,使用this._route就等同于获取当前的_route
 *    (3)设置_route设置数据劫持,也就是在数值变化时,可以notify到watcher
 *    (4)注册路由实例,router-view中注册route
 *    (5)注册全局组件RouterView、RouterLink

源码如下:

import View from './components/view'
import Link from './components/link'

export let _Vue

export function install(Vue) {                             // 当用户执行 Vue.use(VueRouter) 的时候,实际上就是在执行 install 函数

  // 避免重复安装
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue                              // 用一个全局的 _Vue 来接收参数 Vue, 通过这种方式拿到 Vue,不用单独 import

  const isDef = v => v !== undefined          // 判断参数是否为 undefined

  // 从父节点拿到registerRouteInstance,注册路由实例
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  // Vue.mixin 混入, 会把一些函数绑定给每个组件

  // 把 beforeCreate 和 destroyed 钩子函数注入到每一个组件中,每个组件都会在对应生命周期阶段执行
  Vue.mixin({

    // 对于根 Vue 实例而言,执行beforeCreate时定义了 this._routerRoot 表示它自身
    beforeCreate() {

      // 验证vue是否有router对象了,如果有,就不再初始化了
      if (isDef(this.$options.router)) {
        this._routerRoot = this

        // 将_router对象挂载到根组件元素_router上
        this._router = this.$options.router       // this._router 表示 VueRouter 的实例 router,它是在 new Vue 的时候传入的

        // 调用初始化init方法,建立路由监控          index.js中定义了
        this._router.init(this)

        // 设置 _route 为 响应式的   把 this._route 变成响应式对象
        // 劫持数据_route,一旦_route数据发生变化,通知router-view执行render方法
        Vue.util.defineReactive(this, '_route', this._router.history.current)   // defineReactive的作用就是给对象的属性进行简单的数据观测,一旦值获取或者设置就会触发一些行为.

      } else {
        // 子组件从父组件获取routeRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }

      registerInstance(this, this)
    },
    destroyed() {
      registerInstance(this)
    }
  })


  // 给 Vue 原型上定义了 $router 和 $route 2 个属性的 get 方法

  // 设置代理 this.$router === this._routerRoot._router,组件内部通过
  // this.$router调用_router
  Object.defineProperty(Vue.prototype, '$router', {
    get() { return this._routerRoot._router }
  })

  // 组件内部通过this.$route调用_route
  Object.defineProperty(Vue.prototype, '$route', {
    get() { return this._routerRoot._route }
  })


  // 注册RouterView、RouterLink组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)


  // 定义了路由中的钩子函数的合并策略,和普通的钩子函数一样
  const strats = Vue.config.optionMergeStrategies
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

可以看到,install时运用了Vue的响应式劫持_route,以确保路由变化时能够调用render更新router-view组件,同时再通过Object.defineProperty在Vue的原型上设置router和route,使得所有vue组件可以访问。而在混入的beforeCreate一开始对options.router进行判断,判断是否是根Vue实例,如果不是则去根vue实例中获取routeRoot,确保所有vue实例共用一个路由器。

class VueRouter

接着,我们看看VueRouter内部是怎样实现路由管理的。
首先,Vue.use()会调用构造函数,生成必须的变量,并且根据mode选项生成对应的History管理器:

  // 构造函数
  constructor(options: RouterOptions = {}) {
    this.app = null         // 根vue实例
    this.apps = []          // 存放正在被使用的组件(vue实例)   保存持有 $options.router 属性的 Vue 实例
    this.options = options    // vueRouter实例化时传来的参数,即 router.js 中配置的内容,mode,routes等   传入的路由配置
    this.beforeHooks = []     // 存放各组件的全局beforeEach钩子
    this.resolveHooks = []    // 存放各组件的全局beforeResolve钩子
    this.afterHooks = []     // 存放各组件的全局afterEach钩子
    this.matcher = createMatcher(options.routes || [], this)   // 路由匹配器   由createMatcher生成的matcher,里面有一些匹配相关方法

    let mode = options.mode || 'hash'             // router 模式mode

    // 模式的回退或者兼容方式,若设置的mode是history,而js运行平台不支持 supportsPushState 方法,自动回退到hash模式
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false       // 判断条件
    if (this.fallback) {
      mode = 'hash'           // 满足条件,回退到 hash 模式
    }

    // 若不在浏览器环境下,强制使用abstract模式
    if (!inBrowser) {
      mode = 'abstract'
    }
    // 赋值 mode
    this.mode = mode

    // 对于不同的模式mode ,使用对应的History管理器去管理history                 this.history 表示路由历史的具体的实现实例
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }

然后我们再看到install时调用的init方法,初始化需要通过这一方法第一次确定路由,渲染视图,因此我们可以通过它看到基本的过程:


  // 传入的是Vue实例,然后存储到 this.apps 中;只有根 Vue 实例会保存到 this.app 中
  init(app: any /* Vue component instance */) {

    // assert是个断言,测试install.install是否为真,为真,则说明vueRouter已经安装了
    process.env.NODE_ENV !== 'production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )

    // 将组件vue实例推入到apps中,install里面最初是将vue根实例推进去的
    this.apps.push(app)


    // app被destroyed时候,会$emit ‘hook:destroyed’事件,监听这个事件,执行下面方法
    // 从apps 里将app移除
    app.$once('hook:destroyed', () => {

      // 找到apps中这个app的index
      const index = this.apps.indexOf(app)
      // 移除app
      if (index > -1) this.apps.splice(index, 1)

      // 确保 this.app 为一个实例或null 而不是undefined
      if (this.app === app) this.app = this.apps[0] || null
    })

    // this.app 有指向,此实例不是根vue实例,返回
    if (this.app) {
      return
    }

    // 此实例是根vue实例
    // 新增一个history,并添加route监听器
    //并根据不同路由模式进行跳转。hashHistory需要监听hashchange和popshate两个事件,而html5History监听popstate事件
    this.app = app

    const history = this.history

    if (history instanceof HTML5History) {
      // 获取当前location并调用transitionTo做路由切换
      history.transitionTo(history.getCurrentLocation())        //HTML5History在constructor中包含了监听方法,因此这里不需要像HashHistory那样setupListner
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      // 获取当前location并调用transitionTo做路由切换
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }

    // 将apps中的组件中的 _route 全部更新至最新的
    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }

可以看到,在init中,把所有调用它的app也就是vue实例,放到了this.apps数组中,并且监听他们的destroyed声明周期。然后判断是不是根vue实例,如果是,则根据前面构造函数确定的history,调用方法获取当前location,然后再调用transitionTo做路由切换。

base.js

那我们再看到 history/base.js 中定义的transitionTo,confirmTransition,updateRoute,是怎么做到路由的切换的。

// transitionTo 首先根据目标 location 和当前路径 this.current 执行 this.router.match 方法去匹配到目标的路径
// 接下来就会执行 confirmTransition => updateRoute 方法去做真正的切换
transitionTo(
  location: RawLocation,
  onComplete ?: Function,
  onAbort ?: Function
) {
  // 调用match方法,找到匹配的新线路
  const route = this.router.match(location, this.current)   // 这里 this.current 是 history 维护的当前路径,它的初始值是在 history 的构造函数中初始化的:this.current = START
  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)
        })
      }
    }
  )
}


// 路由的切换
// 首先定义了 abort 函数,然后判断如果满足计算后的 route 和 current 是相同路径的话,则直接调用 this.ensureUrl 和 abort
// 接着又根据 current.matched 和 route.matched 执行了 resolveQueue 方法解析出 3 个队列
confirmTransition(route: Route, onComplete: Function, onAbort ?: Function) {
  const current = this.current
  const abort = err => {
    if (!isExtendedError(NavigationDuplicated, err) && 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(new NavigationDuplicated(route))
  }

  // 根据 current.matched 和 route.matched 执行了 resolveQueue 方法解析出 3 个队列
  const { updated, deactivated, activated } = resolveQueue(
    this.current.matched,                                    // 一个 RouteRecord 的数组
    route.matched
  )
  // 拿到 updated、activated、deactivated 3 个 RouteRecord 数组后,接下来就是路径变换后的一个重要部分,执行一系列的钩子函数


  /**
   * 首先构造一个队列 queue,依次序concat钩子函数数组
   * 然后再定义一个迭代器函数 iterator;
   * 最后再执行 runQueue 方法来执行这个队列
   */
  const queue: Array<?NavigationGuard> = [].concat(
    // in-component leave guards
    extractLeaveGuards(deactivated),           // 在失活的组件里调用离开守卫
    // global before hooks
    this.router.beforeHooks,                  // 调用全局的 beforeEach 守卫
    // in-component update hooks
    extractUpdateHooks(updated),              // 在重用的组件里调用 beforeRouteUpdate 守卫
    // in-config enter guards
    activated.map(m => m.beforeEnter),          // 在激活的路由配置里调用 beforeEnter
    // async components
    resolveAsyncComponents(activated)         // 解析异步路由组件
  )

  this.pending = route

  // 执行每一个 导航守卫 hook,并传入 route、current 和匿名函数
  // 当执行了匿名函数,会根据一些条件执行 abort 或 next,只有执行 next 的时候,才会前进到下一个导航守卫钩子函数中
  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)
    }
  }

  // 调用 util/async.js  中的runQueue
  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)
  })

transitionTo中调用的match方法定义在了index.js中

  // 输入参数raw,current,redirectedForm,结果返回匹配 route
  match(
    raw: RawLocation,           //  /user/4739284722这种形式,类似route的path
    current?: Route,
    redirectedFrom?: Location
  ): Route {
    return this.matcher.match(raw, current, redirectedFrom)
  }

可以看到,先通过match方法匹配出新路由的路径,然后在confirmTransition中与旧路径做对比,如果不同,则根据两个路径解析出三个队列分别表示更新路径,新增路径,消灭路径,对应方法如下:

/**
 * 由于路径是由 current 变向 route,那么就遍历对比 2 边的 RouteRecord,找到一个不一样的位置 i
 * 那么 next 中从 0 到 i 的 RouteRecord 是两边都一样,则为 updated 的部分;
 * 从 i 到最后的 RouteRecord 是 next 独有的,为 activated 的部分;
 * 而 current 中从 i 到最后的 RouteRecord 则没有了,为 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)
  }
}

拿到 updated、activated、deactivated 3 个 RouteRecord 数组后,接下来就是执行一系列的钩子函数,最后再是调用updateRoute更新history.current属性,触发根组件的_route的变化,最后更新视图。

向外暴露的方法

vue-router同时向外部暴露一些方法供开发者扩展,比如添加钩子函数,主动跳转路由等,定义如下

/**
   * 全局路由钩子:
   *       在路由切换的时候被调用,可以自定义fn。可以在main.js中使用VueRoute实例router进行调用。比如:
   *       router.beforeEach((to,from,next)=>{})
   * 
   */

  // 将回调方法fn注册到beforeHooks里。registerHook会返回fn执行后的callback方法,功能是将fn从beforeHooks删除
  beforeEach(fn: Function): Function {
    return registerHook(this.beforeHooks, fn)
  }

  // 将回调方法fn注册到resolveHooks里。registerHook会返回fn执行后的callback方法,功能是将fn从resolveHooks删除。
  beforeResolve(fn: Function): Function {
    return registerHook(this.resolveHooks, fn)
  }

  // 将回调方法fn注册到afterHooks里。registerHook会返回fn执行后要调用的callback方法,功能是将fn从afterHooks删除
  afterEach(fn: Function): Function {
    return registerHook(this.afterHooks, fn)
  }

  // 首次路由跳转完成时被调用
  onReady(cb: Function, errorCb?: Function) {
    this.history.onReady(cb, errorCb)
  }

  // 报错
  onError(errorCb: Function) {
    this.history.onError(errorCb)
  }

  // 新增路由跳转
  push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // $flow-disable-line
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort)
    }
  }

  // 路由替换
  replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // $flow-disable-line
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.replace(location, resolve, reject)
      })
    } else {
      this.history.replace(location, onComplete, onAbort)
    }
  }

  // 前进n条路由
  go(n: number) {
    this.history.go(n)
  }

  // 后退一步
  back() {
    this.go(-1)
  }

  // 前进一步
  forward() {
    this.go(1)
  }

  // 根据路径或者路由获取匹配的组件,返回目标位置或是当前路由匹配的组件数组
  getMatchedComponents(to?: RawLocation | Route): Array<any> {
    const route: any = to
      ? to.matched
        ? to
        : this.resolve(to).route
      : this.currentRoute
    if (!route) {
      return []
    }
    return [].concat.apply([], route.matched.map(m => {
      return Object.keys(m.components).map(key => {
        return m.components[key]
      })
    }))
  }

结语

小结:

路由更新方法:

	(1)主动触发
	         router-link绑定了click方法,触发history.push或者history.replace,从而触发history.transitionTo
	         transitionTo用于处理路由的转换,其中包含了updateRoute用于更新_route
	         在beforeCreate中有劫持_route的方法,当_route变化后,触发router-view变化

	(2)地址变化
	         HashHistory和HTML5History会分别监控hashchange和popstate来对路由变化作对应的处理
	         HashHistory和HTML5History捕获到变化后会执行push或replace方法,从而调用transitionTo,
	         然后就是transitionTo用于处理路由的转换,其中包含了updateRoute用于更新_route
	         在beforeCreate中有劫持_route的方法,当_route变化后,触发router-view变化


源码概述:

	1 安装插件:
	   混入beforeCreate生命周期处理,初始化 _routerRoot,_router,_route等
	   全局设置vue静态访问route,方便在组件中调用方法改变_route
	   完成了router-link和 router-view 两个组件的注册,router-link用于触发路由的变化,router-view作 为功能组件,用于触发对应路由视图的变化

	2 根据路由配置生成router实例:
	   根据配置数组routes生成路由配置记录表
	   根据mode生成监控路由变化的history对象

	3 将router实例传入根vue实例
	   根据beforeCreate混入,为根vue对象设置了劫持字段_route,用户触发route-view的变化
	   调用init()函数,完成首次路由的渲染,首次渲染的调用路径是调用history.transitionTo方法,
	   根据router的match函数,生成一个新的route对象
	   接着通过confirmTransition对比一下新生成的route和当前的route对象是否改变,
	   改变的话触发updateRoute,更新history.current属性,触发根组件的_route的变化,
	   从而导致组件调用render函数,更新router-view

	   另一种更新路由的方式是主动触发
	   router-link绑定了click方法,触发history.push或history.replace,从而触发history.transitionTo,
	   同时会监控hashchange和popstate来对路由变化作对应的处理

本文仅分析了源码中最为核心的部分,很多如递归路由,不同路由模式history的异同等,都没有分析,也欢迎大家和我一起讨论学习

编者github地址:传送
qq:1073490398
wechat:carfiedfeifei

发布了3 篇原创文章 · 获赞 3 · 访问量 69

猜你喜欢

转载自blog.csdn.net/weixin_43783814/article/details/105713541