vue-router学习笔记

260 阅读4分钟

前端路由本质原理

监听URL变化,不需要刷新页面的情况下,按路由规则展示不同的页面部分。

单页面应用实现路由,只能通过两种方式:

  • history
  • hash

vue-router 实现

目录结构

download.png

  • components: 存放了两个vue组件<router-link><router-view>
  • history:存放三种路由的实现方式
  • util:存放路由功能类和功能方法
  • create-matcher.jscreate-router-map.js是匹配表生产方法文件
  • index存放VueRouter类,也是整个插件的入口
  • install.js提供插件安装方法

引入方法

import Vue from 'vue'
import VueRouter from 'vue-router'
// 1. 注册插件,执行插件中install方法。
Vue.use(VueRouter)

// 2. 定义组件
const Home = { template: '<div>主页</div>' }
const Index = { template: '<div>首页</div>' }

// 定义路由,最终每个路由都会映射一个组件
const routes = [
  { path: '/home', component: Home },
  { path: '/index', component: Index }
]

// 3. 创建 router 实例,并传 `routes` 配置
const router = new VueRouter({
  routes 
})

// 4. 创建和挂载到vue实例上。
// router对象以参数形式注入到Vue,
// 使用 router-link 组件来导航
// 使用 router-view 组件来展示导航对应组件
const app = new Vue({
  router,
  template:
    `<div id="app">
      <ul>
        <li><router-link to="/home">主页</router-link></li>
        <li><router-link to="/index">首页</router-link></li>
        <router-link tag="li" to="/home">/主页</router-link>
      </ul>
      <router-view class="view"></router-view>
    </div>
  `
}).$mount('#app')

逐步分析

1. 注册插件,执行插件中install方法

import VueRouter from 'vue-router'

vue-router插件入口文件是index.jsindex.js包含VueRouter构造函数的声明和一些构造函数的方法,install方法就是在这里给添加上的VueRouter.install = install

Vue.use(VueRouter)

在这个方法里调用了VueRouterinstall函数。

最主要的两个实现:

  1. 利用Vue.mixin全局注入beforeCreatedestroyed钩子函数方法。
  2. Vue.prototype添加$router$route属性,从而使所有组件可以访问到$router$route
  3. 全局注册RouterViewRouterLink组件。
export function install (Vue) {
  // 重复安装校验
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  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({
  // 调用到这个函数的话,证明已经创建了一个vueRouter实例了,也就是说new VueRouter和new Vue都已经执行
    beforeCreate () {
      // 只有根组件会进入if,否则走else
      if (isDef(this.$options.router)) {
        // vue实例的_routerRoot属性就是vue实例它本身
        this._routerRoot = this
        // vue实例的_routerRoot属性是router实例
        this._router = this.$options.router
        // 初始化vueRouter实例
        this._router.init(this)
        // 利用vue的defineReactive方法,给vue实例添加_route属性,并且把这个属性设置为响应式的。就是说_route变化后,用到这个属性的所有watcher都要触发更新。
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 这里是给子组件实例设置_routerRoot属性,先获取父组件实例,没有就是当前this。
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      // 疑问???????
      registerInstance(this, this)
    },
    destroyed () {
      // 疑问???????
      registerInstance(this)
    }
  })
  
  // 给Vue.prototype绑定$router属性
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  // 给Vue.prototype绑定$route属性
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  // 全局引入组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  // 注册合并钩子函数 疑问???????
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

创建 router 实例,并传 routes 配置

export default class VueRouter {
  static install: () => void
  static version: string
  static isNavigationFailure: Function
  static NavigationFailureType: any
  static START_LOCATION: Route

  app: any
  apps: Array<any>
  ready: boolean
  readyCbs: Array<Function>
  options: RouterOptions
  mode: string
  history: HashHistory | HTML5History | AbstractHistory
  matcher: Matcher
  fallback: boolean
  beforeHooks: Array<?NavigationGuard>
  resolveHooks: Array<?NavigationGuard>
  afterHooks: Array<?AfterNavigationHook>

  constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    // 由路由配置数组生成map映射表
    this.matcher = createMatcher(options.routes || [], this)

    // 选择模式,默认hash
    let mode = options.mode || 'hash'
    this.fallback =
      mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode

    // 根据不同模式生成对应模式的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}`)
        }
    }
  }

  init (app: any /* Vue component instance */) {
    process.env.NODE_ENV !== 'production' &&
      assert(
        install.installed,
        `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
          `before creating root instance.`
      )

    this.apps.push(app)

    // set up app destroyed handler
    // https://github.com/vuejs/vue-router/issues/2639
    app.$once('hook:destroyed', () => {
      // clean out app from this.apps array once destroyed
      const index = this.apps.indexOf(app)
      if (index > -1) this.apps.splice(index, 1)
      // ensure we still have a main app or null if no apps
      // we do not release the router so it can be reused
      if (this.app === app) this.app = this.apps[0] || null

      if (!this.app) this.history.teardown()
    })

    // main app previously initialized
    // return as we don't need to set up new history listener
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history

    if (history instanceof HTML5History || history instanceof HashHistory) {
      const handleInitialScroll = routeOrError => {
        const from = history.current
        const expectScroll = this.options.scrollBehavior
        const supportsScroll = supportsPushState && expectScroll

        if (supportsScroll && 'fullPath' in routeOrError) {
          handleScroll(this, routeOrError, from, false)
        }
      }
      const setupListeners = routeOrError => {
        history.setupListeners()
        handleInitialScroll(routeOrError)
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupListeners,
        setupListeners
      )
    }

    history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })
  }
... 
}

这里最主要的两部分实现是:

  1. routes路由配置数组对象构造成Map映射表
  2. 根据mode类型不同,实例化history对象。

createMatcher

这个方法是用来生成路由配置对象映射表的。

主要逻辑是遍历routes属性,把每一个route生成一个record对象,再把这个对象放入到以pathkeymap中。还有以namekeymap中。

如果有children路由,就遍历生成record,形成一个树形结构。

最终返回一个matcher对象,对象里包含pathListnameMappathMap属性。

实例化history

根据参数mode和浏览器支持情况,实例化history对象。history可以是hashHistoryHTML5History

this._router.init(this)

VueRouter实例化后,就需要把实例化得来的对象以参数的形式传给Vue的实例化函数中,Vue对象的实例化必定会走到beforeCreate钩子函数,那么我们再回过头看下beforeCreate钩子里的_routerinit方法,这个方法只有在实例化根组件时才会调用。

init方法中,会调用history.transitionTo方法进行跳转。这个跳转方法在base.js中。

history.transitionTo里面还会调用history.confirmTransition方法,这两个方法实现的比较复杂,其主要逻辑如下:

  1. 获取到目标路由所对应的路由对象
  2. 判断目标路由和当前是否一致,如果一致,就不处理。
  3. 通过confirmTransition方法获取到需要更新的组件、激活的组件以及废弃的组件对象。
  4. 再将不同的钩子函数以及异步组件加载存入到数组 queue
  5. 通过 runQueue 方法顺序执行 queue 里的函数
  6. 整个执行完后调用 runQueue 中的回调函数,执行添加监听url变化的方法、after 钩子

Vue.util.defineReactive(this, '_route', this._router.history.current)

Vue实例添加_route响应式属性,属性值是当前路由对象。

监听路由变化的方法

如果支持popstate事件就监听popstate,不支持就监听hashchange事件。监听到变化后就调用history.transitionTo方法。

push

调用history.transitionTo方法

来源: juejin.cn/post/684490…