关于vue-router:进阶篇Vue-Router-核心原理解析

45次阅读

共计 6908 个字符,预计需要花费 18 分钟才能阅读完成。

前言

此篇为进阶篇,心愿读者有 Vue.js,Vue Router 的应用教训,并对 Vue.js 外围原理有简略理解;

不会大篇幅手撕源码,会贴最外围的源码,对应的官网仓库源码地址会放到超上,能够配合着看;

对应的源码版本是 3.5.3,也就是 Vue.js 2.x 对应的 Vue Router 最新版本;

Vue Router 是规范写法,为了简略,上面会简称 router。

本文将用以下问题为线索开展讲 router 的原理:

  1. this.$router,this.$route 哪来的
  2. router 怎么晓得要渲染哪个组件
  3. this.$router.push 调用了什么原生 API
  4. router-view 渲染的视图是怎么被更新的
  5. router 怎么晓得要切换视图的

文末有 总结大图

以下是本文应用的简略例子:

// main.js
import Vue from 'vue'
import App from './App'
import router from './router'

new Vue({
  el: '#app',
  // 挂载 Vue Router 实例
  router,
  components: {App},
  template: '<App/>'
})

// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import About from '@/components/About'
import Home1 from '@/components/Home1'

// 应用 Vue Router 插件
Vue.use(Router)
// 创立 Vue Router 实例
export default new Router({
  routes: [
    {
      path: '/',
      redirect: '/home'
    },
    {
      path: '/home',
      name: 'Home',
      component: Home,
      children: [
        {
          path: 'home1',
          name: 'Home1',
          component: Home1
        }
      ]
    },
    {
      path: '/about',
      name: 'About',
      component: About
    }
  ]
})
// App.vue
<template>
  <div id="app">
    <router-link to="/home">Go to Home</router-link>
    <router-link to="/about">Go to About</router-link>
    <router-link to="/home/home1">Go to Home1</router-link>
    <router-view/>
  </div>
</template>
<script>
export default {name: 'App'}
</script>

页面体现举例:

this.$router,this.$route 哪来的

咱们在组件里应用 this.$router 去跳转路由、应用 this.$route 获取以后路由信息或监听路由变动,那它们是从哪里来的?答案是路由注册

路由注册

路由注册产生在 Vue.use 时,而 use 的就是 router 在 index.js 裸露的 VueRouter 类:

// demo 代码:import Router from 'vue-router'

// 应用 Vue Router 插件
Vue.use(Router)
// router 的 index.js
import {install} from './install'

// VueRouter 类
export default class VueRouter {

}
VueRouter.install = install

// install.js
export function install (Vue) {
  // 全局混入钩子函数
  Vue.mixin({beforeCreate () {
      // 有 router 配置项,代表是根组件,设置根 router
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
      } else {
    // 非根组件,通过其父组件拜访,一层层直到根组件
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
    },
  })
  // Vue 原型上减少 $router 和 $route
  Object.defineProperty(Vue.prototype, '$router', {get () {return this._routerRoot._router}
  })
  Object.defineProperty(Vue.prototype, '$route', {get () {return this._routerRoot._route}
  })
  // 全局注册了 router-view 组件和 router-link 组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
}

所以 this.$router,this.$route 就是在注册路由时混入了全局的 beforeCreate 钩子,钩子里进行了 Vue 原型的拓展。

同时也分明了 router-view 和 router-link 的起源。

VueRouter 类

咱们先看最外围局部

export default class VueRouter {constructor (options) {
    // 确定路由模式,浏览器环境默认是 hash,Node.js 环境默认是 abstract
    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}`)
        }
    }
  }
}

constructor 里重要的两个事件:1. 确定路由模式,2. 依据模式创立 History 实例。

如上,history 类有 base 基类,不同模式有对应的 abstract 类、hash 类、html5 类,继承于 base 类,history 实例解决路由切换、路由跳转等等事件。

init

VueRouter 的 init 产生在方才说的 beforeCreate 钩子里

// beforeCreate 钩子里调用了 init
this._router.init(this)

// VueRouter 类的 init 实例办法
init(app) {
  // 保留 router 实例
  this.app = app
  const history = this.history
  if (history instanceof HTML5History || history instanceof HashHistory) {
    const setupListeners = routeOrError => {
      // 待揭秘
      history.setupListeners()}
    // 路由切换
    history.transitionTo(history.getCurrentLocation(),
      setupListeners,
      setupListeners
    )
  }
}

init 里最次要解决了 history.transitionTo,transitionTo 有调用了 setupListeners,先有个印象即可。

router 怎么晓得要渲染哪个组件

用户传入路由配置后,router 是怎么晓得要渲染哪个组件的,答案是 Matcher

Matcher

Matcher 是匹配器,解决路由匹配,创立 matcher 产生在 VueRouter 类的构造函数里

this.matcher = createMatcher(options.routes || [], this)

// create-matcher.js
export function createMatcher(routes, router){
  // 创立映射表
  const {pathList, pathMap, nameMap} = createRouteMap(routes)
  // 依据咱们要跳转的路由匹配到组件,比方 this.$router.push('/about')
  function match() {}
}

createRouteMap

createRouteMap 负责创立路由映射表

export function createRouteMap(routes, oldPathList, oldPathMap, oldNameMap){const pathList: Array<string> = oldPathList || []
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  ...
    
  return {
    pathList,
    pathMap,
    nameMap
  }
}

其中的解决细节先不必关注,打印一下例子里的路由映射表就很分明有什么内容了:

pathList【path 列表】、pathMap【path 到 RouteRecord 的映射】、nameMap【name 到 RouteRecord 的映射】,有了路由映射表之后想定位到 RouteRecord 就很容易了

其中 router 一些数据结构如下:源码

match 办法

match 办法就是从方才生成的路由映射表外面取出 RouterRecord

// create-matcher.js
function match(raw, currentRoute, redirectedFrom){const location = normalizeLocation(raw, currentRoute, false, router)
    const {name} = location

    if (name) {
       // name 的状况
       ...
    } else if (location.path) {
       // path 的状况
       ...
    }
}

this.$router.push 调用了什么原生 API

this.$router.push 用于跳转路由,外部调用的是 transitionTo 做路由切换,

在 hash 模式的源码,在 history 模式的源码

以 hash 模式为例

// history/hash.js
// push 办法
push (location, onComplete, onAbort) {
    // transitionTo 做路由切换,在外面调用了方才的 matcher 的 match 办法匹配路由
    // transitionTo 第 2 个和第 3 个参数是回调函数
    this.transitionTo(
      location,
      route => {pushHash(route.fullPath)
        onComplete && onComplete(route)
      },
      onAbort
    )
}
// 更新 url,如果反对 h5 的 pushState api,就应用 pushState 的形式,// 否则设置 window.location.hash
function pushHash (path) {if (supportsPushState) {pushState(getUrl(path))
  } else {window.location.hash = path}
}

function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

history 模式就是调用 pushState 办法

pushState 办法

源码

export function pushState (url, replace) {
  // 获取 window.history
  const history = window.history
  try {if (replace) {const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      // 调用 replaceState
      history.replaceState(stateCopy, '', url)
    } else {
      // 调用 pushState
      history.pushState({key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {...}
}

router-view 渲染的视图是怎么被更新的

router-view 用于渲染传入路由配置对应的组件

export default {
    name: 'RouterView',
    functional: true,
    render(_, { props, children, parent, data}) {
        ...
    // 标识
    data.routerView = true
    // 通过 depth 由 router-view 组件向上遍历直到根组件,// 遇到其余的 router-view 组件则路由深度 +1 
    // 用 depth 帮忙找到对应的 RouterRecord
    let depth = 0
    while (parent && parent._routerRoot !== parent) {const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      if (vnodeData.routerView) {depth++}
      parent = parent.$parent
    }
    data.routerViewDepth = depth
        // 获取匹配的组件
        const route = parent.$route
        const matched = route.matched[depth]
        const component = matched && matched.components[name]

        ...
        // 渲染对应的组件
        const h = parent.$createElement
        return h(component, data, children)
   }
}

比方例子中的二级路由 home1

因为是二级路由,所以深度 depth 是 1,找到如下图的 home1 组件

更新

那么每次路由切换之后,怎么触发了渲染新视图呢?

每次 transitionTo 实现后会执行增加的回调函数,回调函数里更新了以后路由信息

在 VueRouter 的 init 办法里注册了回调:

history.listen(route => {
  this.apps.forEach(app => {
        // 更新以后路由信息 _route
    app._route = route
  })
})

而在组件的 beforeCreate 钩子里把 _route 变成了响应式的,在 router-view 的 render 函数里拜访到了 parent.$route,也就是拜访到了 _route,

所以一旦 _route 扭转了,就触发了 router-view 组件的从新渲染

// 把 _route 变成响应式的
Vue.util.defineReactive(this, '_route', this._router.history.current)

router 怎么晓得要切换视图的

到当初咱们曾经分明了 router 是怎么切换视图的,那当咱们点击浏览器的后退按钮、后退按钮的时候是怎么触发视图切换的呢?

答案是 VueRouter 在 init 的时候做了事件监听 setupListeners

setupListeners

popstate 事件:在做出浏览器动作时,才会触发该事件,调用 window.history.pushState 或 replaceState 不会触发,文档

hashchange 事件:hash 变动时触发

外围原理总结

本文从 5 个问题登程,解析了 Vue Router 的外围原理,而其它分支比方导航守卫是如何实现的等等能够本人去理解,先理解了外围原理再看其余局部也是瓜熟蒂落。

自身前端路由的实现并不简单,Vue Router 更多的是思考怎么和 Vue.js 的外围能力联合起来,利用到 Vue.js 生态中去。

对 Vue Router 的原理有哪一部分想和我聊聊的,能够在评论区留言

正文完
 0