likes
comments
collection
share

史上最全 vue-router 讲解 !!!

作者站长头像
站长
· 阅读数 12

前言

最近有朋友正在准备面试,刚到公司就收到他的消息,接连几个 vue-router 也把我给问懵了 😶

这大早上的人都还没醒就已经梦游到工位上了,眼睛是迷离的,脑子也是糊的,怎么会有人一大早就开始学习的呢?

反正我是不行的😂,这一开头还那么劲爆,直接给我来了几道面试题,求求了 放过我吧😭 让我摸会儿鱼吧

盯着屏幕上的题过了好一会。。。

我猛然发觉不正是我之前整理过的知识点嘛 !!!于是我就这么把库存发了出来。

朋友啊 你自己看去吧,我得赶紧码别的文去了,不然 六月更文计划 要接不上了😂。

这已经我本月更文的第15篇啦,已经赛程即将过半,参加更文的友友们加油啊,坚持就是胜利 💪!!!

好了不说了赶紧开始今天的学习吧😜

前端路由

前端路由是后来发展到SPA(单页应用)时才出现的概念。 SPA 就是一个WEB项目只有一个 HTML 页面,一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转。

前端路由在SPA项目中是必不可少的,页面的跳转、刷新都与路由有关,通过不同的url显示相应的页面。

优点:前后端的彻底分离,不刷新页面,用户体验较好,页面持久性较好。

后端路由

当在地址栏切换不同的url时,都会向服务器发送一个请求,服务器接收并响应这个请求,在服务端拼接好html文件返回给页面来展示。

优点:减轻了前端的压力,html都由后端拼接;

缺点:依赖于网络,网速慢,用户体验很差,项目比较庞大时,服务器端压力较大,

不能在地址栏输入指定的url访问相应的模块,前后端不分离。

路由模式

前端路由实现起来其实很简单,本质是监听 URL 的变化,然后匹配路由规则,在不刷新的情况下显示相应的页面。

hash模式(对应HashHistory)

  • 把前端路由的路径用井号 # 拼接在真实 url 后面的模式,但是会覆盖锚点定位元素的功能,通过监听 URL 的哈希部分变化,相应地更新页面的内容。
  • 前端路由的处理完全在客户端进行,在路由发生变化时,只会改变 URL 中的哈希部分(井号 # 后面的路径),且不会向服务器发送新的请求,而是触发 onhashchange 事件。
  • hash 只有#符号之前的内容才会包含在请求中被发送到后端,如果 nginx 没有匹配得到当前的 url 也没关系。hash 永远不会提交到 server 端。
  • hash值的改变,都会在浏览器的访问历史中增加一个记录,所以可以通过浏览器的回退、前进按钮控制hash的切换。
  • hash 路由不会造成 404 页面的问题,因为所有路由信息都在客户端进行解析和处理,服务器只负责提供应用的初始 HTML 页面和静态资源,不需要关心路由的匹配问题。
// onhashchage事件,可以在window对象上监听这个事件
window.onhashchange = function(event){
  console.log(event.oldURL, event.newURL)
  let hash = location.hash.slice(1)
}
  • 通过location.hash修改hash值,触发更新。
  • 通过监听hashchange事件监听浏览器前进或者后退,触发更新。

history模式 (对应HTML5History)

  • 是 html5 新推出的功能,比 Hash url 更美观
  • 在 history 模式下浏览器在刷新页面时,会按照路径发送真实的资源请求。如果 nginx 没有匹配得到当前的 url ,就会出现 404 的页面。
  • 在使用 history 模式时,需要通过服务端支持允许地址可访问,如果没有设置,就很容易导致出现 404 的局面。
  • 改变url: history 提供了 pushState 和 replaceState 两个方法来记录路由状态,这两个方法改变 URL 不会引起页面刷新。
  • 监听url变化:通过监听popstate事件监听history变化,也就是点击浏览器的前进或者后退功能时触发。
history.replaceState({}, null, '/b') // 替换路由
history.pushState({}, null, '/a') // 路由压栈
history.back() // 返回
history.forward() // 前进
history.go(-2) // 后退2次
  • 通过 history.pushState 修改浏览器地址,触发更新。
  • 通过监听 popstate 事件监听浏览器前进或者后退,触发更新。
history 模式下 404 页面的处理

在 history 模式下,浏览器会向服务器发起请求,服务器根据请求的路径进行匹配:

如果服务器无法找到与请求路径匹配的资源或路由处理器,服务器可以返回 /404 路由,跳转到项目中配置的 404 页面,指示该路径未找到。

对于使用历史路由模式的单页应用(SPA),通常会在服务器配置中添加一个通配符路由,将所有非静态资源的请求都重定向到主页或一个自定义的 404 页面,以保证在前端处理路由时不会出现真正的 404 错误页面。

在项目中配置对应的 404 页面:

export const publicRoutes = [
  {
    path: '/404',
    component: () => import('src/views/404/index'),
  },
]

vueRouter

Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,允许你在 Vue 应用中构建单页面应用(SPA),并且提供了灵活的路由配置和导航功能。让用 Vue.js 构建单页应用变得轻而易举。功能包括:

  • 路由映射:可以将 url 映射到 Vue组件,实现不同 url 对应不同的页面内容。
  • 嵌套路由映射:可以在路由下定义子路由,实现更复杂的页面结构和嵌套组件的渲染。
  • 动态路由:通过路由参数传递数据。你可以在路由配置中定义带有参数的路由路径,并通过 $route.params 获取传递的参数。
  • 模块化、基于组件的路由配置:路由配置是基于组件的,每个路由都可以指定一个 Vue 组件作为其页面内容,将路由配置拆分为多个模块,在需要的地方引入。。
  • 路由参数、查询、通配符:通过路由参数传递数据,实现页面间的数据传递和动态展示。
  • 导航守卫:Vue Router 提供了全局的导航守卫和路由级别的导航守卫,可以在路由跳转前后执行一些操作,如验证用户权限、加载数据等。
  • 展示由 Vue.js 的过渡系统提供的过渡效果:可以为路由组件添加过渡效果,使页面切换更加平滑和有动感。
  • 细致的导航控制:可以通过编程式导航(通过 JavaScript 控制路由跳转)和声明式导航(通过 组件实现跳转)实现页面的跳转。
  • 路由模式设置:Vue Router 支持两种路由模式:HTML5 history 模式或 hash 模式
  • 可定制的滚动行为:当页面切换时,Vue Router 可以自动处理滚动位置。定制滚动行为,例如滚动到页面顶部或指定的元素位置。
  • URL 的正确编码:Vue Router 会自动对 URL 进行正确的编码

路由组件

  • **router-link:**通过 router-link 创建链接 其本质是a标签,这使得 Vue Router 可以在不重新加载页面的情况下更改 URL,处理 URL 的生成以及编码。
  • **router-view:**router-view 将显示与 url 对应的组件。

$router$route

$route: 是当前路由信息对象,获取和当前路由有关的信息。 route 为属性是只读的,里面的属性是 immutable (不可变) 的,不过可以通过 watch 监听路由的变化。

fullPath: ""  // 当前路由完整路径,包含查询参数和 hash 的完整路径
hash: "" // 当前路由的 hash 值 (锚点)
matched: [] // 包含当前路由的所有嵌套路径片段的路由记录 
meta: {} // 路由文件中自赋值的meta信息
name: "" // 路由名称
params: {}  // 一个 key/value 对象,包含了动态片段和全匹配片段就是一个空对象。
path: ""  // 字符串,对应当前路由的路径
query: {}  // 一个 key/value 对象,表示 URL 查询参数。跟随在路径后用'?'带的参数

$router 是 vueRouter 实例对象,是一个全局路由对象,通过 this.$router 访问路由器, 可以获取整个路由文件或使用路由提供的方法。

// 导航守卫
router.beforeEach((to, from, next) => {
  /* 必须调用 `next` */
})
router.beforeResolve((to, from, next) => {
  /* 必须调用 `next` */
})
router.afterEach((to, from) => {})

动态导航到新路由
router.push
router.replace
router.go
router.back
router.forward

routes 是 router 路由实例用来配置路由对象 可以使用路由懒加载(动态加载路由)的方式

  • 把不同路由对应的组件分割成不同的代码块,当路由被访问时才去加载对应的组件 即为路由的懒加载,可以加快项目的加载速度,提高效率
const router = new VueRouter({
  routes: [
    {
      path: '/home',
      name: 'Home'
      component:() = import('../views/home')
		}
  ]
})

vueRouter的使用

页面中路由展示位置

<div id="app">
  <!-- 添加路由 -->
  <!-- 会被渲染为 <a href="#/home"></a> -->
  <router-link to="/home">Home</router-link>
  <router-link to="/login">Login</router-link>
  <!-- 展示路由的内容 -->
  <router-view></router-view>
</div>

路由模块 引入 vue-router,使用 Vue.use(VueRouter) 注册路由插件 定义路由数组,并将数组传入VueRouter 实例,并将实例暴露出去

import Vue from 'vue'
import VueRouter from 'vue-router'
import { hasVisitPermission, isWhiteList } from './permission'

// 注册路由组件
Vue.use(VueRouter)

// 创建路由: 每一个路由规则都是一个对象
const routers =[
  // path 路由的地址
  // component 路由的所展示的组件
  {
      path: '/',
      // 当访问 '/'的时候 路由重定向 到新的地址 '/home'
      redirect: '/home',
  },     
  {
      path: '/home',
      component: home,
  },
  {
      path: '/login',
      component: login,
  },
],

// 实例化 VueRouter 路由
const router = new VueRouter({
  mode: 'history',
  base: '/',
  routers
})

// 路由守卫
router.beforeEach(async (to, from, next) => {
  // 清除面包屑导航数据
  store.commit('common/SET_BREAD_NAV', [])
  // 是否白名单
  if (isWhiteList(to)) {
    next()
  } else {
    // 未登录,先登录
    try {
      if (!store.state.user.userInfo) {
        await store.dispatch('user/getUserInfo')
      }

      // 登录后判断,是否有访问页面的权限
      if (!hasVisitPermission(to, store.state.user.userInfo)) {
        next({ path: '/404' })
      } else {
        next()
      }
    } catch (err) {
      $error(err)
    }
  }
})

export default router

在 main.js 上挂载路由 将VueRouter实例引入到main.js,并注册到根Vue实例上

import router from './router'

new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app')

动态路由

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。我们可以在 vueRrouter 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果。

  • 动态路由的创建,主要是使用 path 属性过程中,使用动态路径参数,路径参数 用冒号 : 表示。

当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.query 的形式暴露出来。因此,我们可以通过更新 User 的模板来呈现当前的用户 ID:

const routes = [
  {
    path: '/user/:id'
    name: 'User'
    components: User
	}
]

_vue-router _通过配置 _params __query _来实现动态路由

params 传参

  • 必须使用 命名路由 name 传值

  • 参数不会显示在 url 上

  • 浏览器强制刷新时传参会被清空

// 传递参数
this.$router.push({
  name: Home
  params: {
    number: 1 ,
    code: '999'
  }
})
// 接收参数
const p = this.$route.params

query 传参

  • 可以用 name 也可以使用 path 传参
  • 传递的参数会显示在 url 上
  • 页面刷新是传参不会丢失
// 方式一:路由拼接
this.$router.push('/home?username=xixi&age=18')

// 方式二:name + query 传参
this.$router.push({
  name: Home
  query: {
    username: 'xixi',
    age: 18
	}
})


// 方式三:path + name 传参
this.$router.push({
  path: '/home'
  query: {
    username: 'xixi',
    age: 18
	}
})

// 接收参数
const q = this.$route.query

keep-alive

keep-alive是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。

keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

和 transition 相似,keep-alive 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

keep-alive 可以设置以下props属性:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
  • max - 数字。最多可以缓存多少组件实例

在不缓存组件实例的情况下,每次切换都会重新 render,执行整个生命周期,每次切换时,重新 render,重新请求,必然不满足需求。 会消耗大量的性能

keep-alive 的基本使用

只是在进入当前路由的第一次render,来回切换不会重新执行生命周期,且能缓存router-view的数据。 通过 include 来判断是否匹配缓存的组件名称: 匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配

<keep-alive>
	<router-view></router-view>
</keep-alive>

<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

路由配置 keepAlive

在路由中设置 keepAlive 属性判断是否需要缓存

{
  path: 'list',
  name: 'itemList', // 列表页
  component (resolve) {
    require(['@/pages/item/list'], resolve)
 	},
   meta: {
    keepAlive: true,
    compName: 'ItemList'
    title: '列表页'
   }
}

{
  path: 'management/class_detail/:id/:activeIndex/:status',
  name: 'class_detail',
  meta: {
    title: '开班详情',
    keepAlive: true,
    compName: 'ClassInfoDetail',
    hideInMenu: true,
  },
  component: () => import('src/views/classManage/class_detail.vue'),
},

使用

<div id="app" class='wrapper'>
  <keep-alive>
      <!-- 需要缓存的视图组件 --> 
      <router-view v-if="$route.meta.keepAlive"></router-view>
   </keep-alive>
    <!-- 不需要缓存的视图组件 -->
   <router-view v-if="!$route.meta.keepAlive"></router-view>
</div>

keepAlive 对生命周期的影响

设置缓存后组件加载的生命周期会新增 actived 与 deactived

  • 首次进入组件时也会触发 actived 钩子函数:beforeRouteEnter > beforeCreate > created> beforeMount > beforeRouteEnter 的 next 回调> mounted > activated > ... ... > beforeRouteLeave > deactivated
  • 再次进入组件时直接获取actived的组件内容:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated

keep-alive 组件监听 include 及 exclude 的缓存规则,若发生变化则执行 pruneCache (遍历cache 的name判断是否需要缓存,否则将其剔除) 且 keep-alive 中没有 template,而是用了 render,在组件渲染的时候会自动执行 render 函数,

  • 若命中缓存则直接从缓存中拿 vnode 的组件实例,
  • 若未命中缓存且未被缓存过则将该组件存入缓存,
  • 当缓存数量超出最大缓存数量时,删除缓存中的第一个组件。

动态路由缓存的的具体表现在:

  • 由动态路由配置的路由只能缓存一份数据。
  • keep-alive 动态路由只有第一个会有完整的生命周期,之后的路由只会触发 actived 和 deactivated这两个钩子。
  • 一旦更改动态路由的某个路由数据,期所有同路由下的动态路由数据都会同步更新。

如何删除 keep-alive 中的缓存

vue2 中清除路由缓存

在组件内可以通过 this 获取 vuerouter 的缓存
vm.$vnode.parent.componentInstance.cache

或者通过 ref 获取 外级 dom 史上最全 vue-router 讲解 !!!

<template>
  <el-container id="app-wrapper">
    <Aside />
    <el-container>
      <el-header id="app-header" height="45px">
        <Header @removeCacheRoute="removeCacheRoute" />
      </el-header>
      <!-- {{ includeViews }} -->
      <el-main id="app-main">
        <keep-alive :include="includeViews">
          <router-view ref="routerViewRef" :key="key" />
        </keep-alive>
      </el-main>
    </el-container>
  </el-container>
</template>

<script>
import Aside from './components/Aside'
import Header from './components/Header'
import { mapGetters } from 'vuex'
export default {
  name: 'Layout',
  components: {
    Aside,
    Header,
  },
  data () {
    return {
    }
  },
  computed: {
    ...mapGetters(['cacheRoute', 'excludeRoute']),
    includeViews () {
      return this.cacheRoute.map(item => item.compName)
    },
    key () {
      return this.$route.fullPath
    },
  },
  methods: {
    removeCacheRoute (fullPath) {
      const cache = this.$refs.routerViewRef.$vnode.parent.componentInstance.cache
      delete cache[fullPath]
    },
  },
}
</script>

路由守卫

导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。

通俗来讲:路由守卫就是路由跳转过程中的一些生命周期函数(钩子函数),我们可以利用这些钩子函数帮我们实现一些需求。

路由守卫又具体分为 全局路由守卫独享守卫组件路由守卫

全局路由守卫

  • 全局前置守卫router.beforeEach
  • 全局解析守卫:router.beforeResolve
  • 全局后置守卫:router.afterEach

beforeEach(to,from, next)

在路由跳转前触发,参数包括to,from,next 三个,这个钩子作用主要是用于登录验证。

前置守卫也可以理解为一个路由拦截器,也就是说所有的路由在跳转前都要先被前置守卫拦截。


router.beforeEach(async (to, from, next) => {
  // 清除面包屑导航数据
  store.commit('common/SET_BREAD_NAV', [])
  // 是否白名单
  if (isWhiteList(to)) {
    next()
  } else {
    // 未登录,先登录
    try {
      if (!store.state.user.userInfo) {
        await store.dispatch('user/getUserInfo')
        // 登录后判断,是否有角色, 无角色 到平台默认页
        if (!store.state.user.userInfo.permissions || !store.state.user.userInfo.permissions.length) {
          next({ path: '/noPermission' })
        }
      }

      // 登录后判断,是否有访问页面的权限
      if (!hasVisitPermission(to, store.state.user.userInfo)) {
        next({ path: '/404' })
      } else {
        next()
      }
    } catch (err) {
      $error(err)
    }
  }
})

beforeResolve(to,from, next)

在每次导航时都会触发,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被正确调用。

即在 beforeEach 和 组件内 beforeRouteEnter 之后,afterEach之前调用。

router.beforeResolve 是获取数据或执行任何其他操作的理想位置

router.beforeResolve(async to => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission()
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... 处理错误,然后取消导航
        return false
      } else {
        // 意料之外的错误,取消导航并把错误传给全局处理器
        throw error
      }
    }
  }
})

afterEach(to,from)

和beforeEach相反,他是在路由跳转完成后触发,参数包括to, from 由于此时路由已经完成跳转 所以不会再有next。

全局后置守卫对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。

router.afterEach((to, from) => {
	// 在路由完成跳转后执行,实现分析、更改页面标题、声明页面等辅助功能
	sendToAnalytics(to.fullPath)
})

独享路由守卫

beforeEnter(to,from, next) 独享路由守卫可以直接在路由配置上定义,但是它只在进入路由时触发,不会在 params、query 或 hash 改变时触发。

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    // 在路由配置中定义守卫
    beforeEnter: (to, from,next) => {
      next()
    },
  },
]

或是使用数组的方式传递给 beforeEnter ,有利于实现路由守卫的重用

function removeQueryParams(to) {
  if (Object.keys(to.query).length)
    return { path: to.path, query: {}, hash: to.hash }
}

function removeHash(to) {
  if (to.hash) return { path: to.path, query: to.query, hash: '' }
}

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: [removeQueryParams, removeHash],
  },
  {
    path: '/about',
    component: UserDetails,
    beforeEnter: [removeQueryParams],
  },
]

组件路由守卫

在组件内使用的钩子函数,类似于组件的生命周期, 钩子函数执行的顺序包括

  • beforeRouteEnter(to,from, next) -- 进入前
  • beforeRouteUpdate(to,from, next) -- 路由变化时
  • beforeRouteLeave(to,from, next) -- 离开后

组件内路由守卫的执行时机:


<template>
  ...
</template>
export default{
  data(){
    //...
  },
  
  // 在渲染该组件的对应路由被验证前调用
  beforeRouteEnter (to, from, next) {
    // 此时 不能获取组件实例 this
    // 因为当守卫执行前,组件实例还没被创建
    next((vm)=>{
      // next 回调 在 组件 beforeMount 之后执行 此时组件实例已创建,
      // 可以通过 vm 访问组件实例
      console.log('A组件中的路由守卫==>> beforeRouteEnter 中next 回调 vm', vm)
    )
  },

  // 可用于检测路由的变化
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用  此时组件已挂载完可以访问组件实例 `this`
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    console.log('组件中的路由守卫==>> beforeRouteUpdate')
    next()
  },

  // 在导航离开渲染该组件的对应路由时调用
  beforeRouteLeave (to, from, next) {
    // 可以访问组件实例 `this`
    console.log('A组件中的路由守卫==>> beforeRouteLeave')
    next()
  }
}
<style>
...
</style>

注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持 传递回调,因为没有必要了

路由守卫触发流程

页面加载时路由守卫触发顺序: 史上最全 vue-router 讲解 !!!

  1. 触发全局的路由守卫 beforeEach
  2. 组件在路由配置的独享路由 beforeEnter
  3. 进入组件中的 beforeRouteEnter,此时无法获取组件对象
  4. 触发全局解析守卫 beforeResolve
  5. 此时路由完成跳转 触发全局后置守卫 afterEach
  6. 组件的挂载 beforeCreate --> created --> beforeMount
  7. 路由守卫 beforeRouterEnter 中的 next回调, 此时能够获取到组件实例 vm
  8. 完成组件的挂载 mounted

当点击切换路由时: A页面跳转至B页面触发的生命周期及路由守卫顺序: 史上最全 vue-router 讲解 !!!

  1. 导航被触发进入其他路由。
  2. 在离开的路由组件中调用 beforeRouteLeave 。
  3. 调用全局的前置路由守卫 beforeEach 。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫。
  5. 调用被激活组件的路由配置中调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件中调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫。
  9. 导航被确认。
  10. 调用全局后置路由 afterEach 钩子。
  11. 触发 DOM 更新,激活组件的创建及挂载 beforeCreate (新)-->created (新)-->beforeMount(新) 。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
  13. 失活组件的销毁 beforeDestory(旧)-->destoryed(旧)
  14. 激活组件的挂载 mounted(新)

路由守卫的触发顺序 beforeRouterLeave-->beforeEach-->beforeEnter-->beforeRouteEnter-->beforeResolve-->afterEach--> beforeCreate (新)-->created (新)-->beforeMount(新) -->beforeRouteEnter中的next回调 -->beforeDestory(旧)-->destoryed(旧)-->mounted(新)

当路由更新时:触发 beforeRouteUpdate

注意: 但凡涉及到有next参数的钩子,必须调用next() 才能继续往下执行下一个钩子,否则路由跳转等会停止。

vueRouter 实现原理

vueRouter 实现的原理就是 监听浏览器中 url 的 hash值变化,并切换对应的组件

1.路由注册

通过vue.use()安装vue-router插件,会执行install方法,并将Vue当做参数传入install方法 Vue.use(VueRouter) === VueRouter.install()

src/install.js

export function install (Vue) {
  // 确保 install 调用一次
  if (install.installed && _Vue === Vue) return
  install.installed = true
  // 把 Vue 赋值给全局变量
  _Vue = Vue
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  // 为每个组件混入 beforeCreate 钩子
  // 在 `beforeCreate` 钩子执行时 会初始化路由
  Vue.mixin({
    beforeCreate () {
      // 判断组件是否存在 router 对象,该对象只在根组件上有
      if (isDef(this.$options.router)) {
        // 根路由设置为自己
        this._routerRoot = this
        //  this.$options.router就是挂在根组件上的 VueRouter 实例
        this._router = this.$options.router
        // 执行VueRouter实例上的init方法,初始化路由
        this._router.init(this)
        // 很重要,为 _route 做了响应式处理
        //   即访问vm._route时会先向dep收集依赖, 而修改_router 会触发组件渲染
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 用于 router-view 层级判断
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
  
  /* 在Vue的prototype上面绑定 $router,
     这样可以在任意Vue对象中使用this.$router访问,同时经过Object.defineProperty,将 $router 代理到 Vue
     访问this.$router 即访问this._routerRoot._router */
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  /* 同理,访问this.$route即访问this._routerRoot._route */
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  // 全局注册组件 router-link 和 router-view
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
}
  1. 使用 Vue.mixin 为每个组件混入 beforeCreate 钩子,全局混入添加组件选项  挂载 router 配置项
  2. 通过 defineReactive 为vue实例实现数据劫持  让_router能够及时响应页面更新
  3. router、router 、routerroute 代理到 Vue 原型上
  4. 全局注册 router-view 及 router-link 组件

2. VueRouter 实例化

在安装插件后,对 VueRouter 进行实例化。

//用户定义的路由配置数组
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 3. Create the router
const router = new VueRouter({
  mode: 'hash',
  base: __dirname,
  routes: [
    { path: '/', component: Home }, // all paths are defined without the hash.
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar }
  ]
})

VueRouter 构造函数

src/index.js

// VueRouter 的构造函数
constructor(options: RouterOptions = {}) {
    // ...
    // 路由匹配对象 -- 路由映射表
    this.matcher = createMatcher(options.routes || [], this)

    // 根据 mode 采取不同的路由方式
    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

    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}`)
        }
    }
  }

在实例化 vueRouter 的过程中 通过 createMatcher 创建路由匹配对象(路由映射表),并且根据 mode 来采取不同的路由方式。

3.创建路由匹配对象

src/create-matcher.js

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
    // 创建路由映射表
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
    
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  // 路由匹配 找到对应的路由
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    //...
  }

  return {
    match,
    addRoutes
  }
}

createMatcher 函数的作用就是创建路由映射表,然后通过闭包的方式让 addRoutesmatch函数能够使用路由映射表的几个对象,最后返回一个 Matcher 对象。

在createMatcher中通过使用 createRouteMap() 根据用户配置的路由规则来创建对应的路由映射表,返回对应的 pathList, pathMap, nameMap  

createRouteMap 构造函数 主要用于创建映射表,根据用户的路由配置规则创建对应的路由映射表

src/create-route-map.js

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>;
  pathMap: Dictionary<RouteRecord>;
  nameMap: Dictionary<RouteRecord>;
} {
  // 创建映射表
  const pathList: Array<string> = oldPathList || []
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  // 遍历路由配置,为每个配置添加路由记录
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })
  // 确保通配符在最后
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }
  return {
    pathList,
    pathMap,
    nameMap
  }
}
// 添加路由记录
function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  // 获得路由配置下的属性
  const { path, name } = route
  const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
  // 格式化 url,替换 / 
  const normalizedPath = normalizePath(
    path,
    parent,
    pathToRegexpOptions.strict
  )
  // 生成记录对象
  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    instances: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props: route.props == null
      ? {}
      : route.components
        ? route.props
        : { default: route.props }
  }

  if (route.children) {
    // 递归路由配置的 children 属性,添加路由记录
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
  // 如果路由有别名的话
  // 给别名也添加路由记录
  if (route.alias !== undefined) {
    const aliases = Array.isArray(route.alias)
      ? route.alias
      : [route.alias]

    aliases.forEach(alias => {
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      )
    })
  }
  // 更新映射表
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }
  // 命名路由添加记录
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        `Duplicate named routes definition: ` +
        `{ name: "${name}", path: "${record.path}" }`
      )
    }
  }
}

4.路由初始化 init

当根组件调用 beforeCreate 钩子函数时,会执行插件安装阶段注入的 beforeCreate 函数

beforeCreate () {
  // 在option上面存在router则代表是根组件 
  if (isDef(this.$options.router)) {
    this._routerRoot = this
    this._router = this.$options.router
    // 执行_router实例的 init 方法   在 VueRouter 构造函数中的 init()
    this._router.init(this)
     // 为 vue 实例定义数据劫持   让 _router 的变化能及时响应页面的更新
    Vue.util.defineReactive(this, '_route', this._router.history.current)
  } else {
     // 非根组件则直接从父组件中获取
    this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
  }
  // 通过 registerInstance(this, this)这个方法来实现对router-view的挂载操作:主要用于注册及销毁实例
  registerInstance(this, this)
},

在根组件中进行挂载,非根组件从父级中获取,保证全局只有一个 路由实例 初始化时执行,保证页面再刷新时也会进行渲染

init() --  vueRouter 构造函数中的路由初始化

src/index.js

init(app: any /* Vue component instance */) {
    // 将当前vm实例保存在app中,保存组件实例
    this.apps.push(app)
    // 如果根组件已经有了就返回
    if (this.app) {
      return
    }
    /* this.app保存当前vm实例 */
    this.app = app
    // 赋值路由模式
    const history = this.history
    // 判断路由模式,以哈希模式为例
    if (history instanceof HTML5History) {
      // 路由跳转
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      // 添加 hashchange 监听
      const setupHashListener = () => {
        history.setupListeners()
      }
      // 路由跳转
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }
    // 该回调会在 transitionTo 中调用
    // 对组件的 _route 属性进行赋值,触发组件渲染
    history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })
  }

init() 核心就是进行路由的跳转,改变 URL 然后渲染对应的组件。 路由初始化:

  1. 在Vue调用init进行初始化时会调用beforeCreate钩子函数
  2. init方法中调用了transationTo 路由跳转
  3. 在transationTo方法中又调用了confirmTransation 确认跳转路由,最终在这里执行了runQueue方法,
  4. runQueue 会把队列 queue 中的所有函数调用执行,其中就包括 路由守卫钩子函数 的执行

5.路由跳转

transitionTo

src/history/base.js

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  // 获取匹配的路由信息
  const route = this.router.match(location, this.current)
  // 确认切换路由
  this.confirmTransition(route, () => {
    // 以下为切换路由成功或失败的回调
    // 更新路由信息,对组件的 _route 属性进行赋值,触发组件渲染
    // 调用 afterHooks 中的钩子函数
    this.updateRoute(route)
    // 添加 hashchange 监听
    onComplete && onComplete(route)
    
    // 更新 URL
    this.ensureURL()
    // 只执行一次 ready 回调
    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) })
    }
  })
}


 updateRoute (route: Route) {
    // 更新当前路由信息  对组件的 _route 属性进行赋值,触发组件渲染
    const prev = this.current
    this.current = route
    this.cb && this.cb(route)
    // 路由跳转完成 调用 afterHooks 中的钩子函数
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }

在路由跳转前要先匹配路由信息,在确认切换路由后更新路由信息,触发组件的渲染,最后更新 url

Matcher 中的 match() 在路由配置中匹配到相应的路由则创建对应的路由信息

src/create-matcher.js

function match (
  raw: RawLocation,
  currentRoute?: Route,
  redirectedFrom?: Location
): Route {
  // 序列化 url
  // 比如对于该 url 来说 /abc?foo=bar&baz=qux#hello
  // 会序列化路径为 /abc
  // 哈希为 #hello
  // 参数为 foo: 'bar', baz: 'qux'
  const location = normalizeLocation(raw, currentRoute, false, router)
  const { name } = location
  // 如果是命名路由,就判断记录中是否有该命名路由配置
  if (name) {
    const record = nameMap[name]
    // 没找到表示没有匹配的路由
    if (!record) return _createRoute(null, location)
    const paramNames = record.regex.keys
      .filter(key => !key.optional)
      .map(key => key.name)
    // 参数处理
    if (typeof location.params !== 'object') {
      location.params = {}
    }
    if (currentRoute && typeof currentRoute.params === 'object') {
      for (const key in currentRoute.params) {
        if (!(key in location.params) && paramNames.indexOf(key) > -1) {
          location.params[key] = currentRoute.params[key]
        }
      }
    }
    if (record) {
      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      return _createRoute(record, location, redirectedFrom)
    }
  } else if (location.path) {
    // 非命名路由处理
    location.params = {}
    for (let i = 0; i < pathList.length; i++) {
     // 查找记录
      const path = pathList[i]
      const record = pathMap[path]
      // 如果匹配路由,则创建路由
      if (matchRoute(record.regex, location.path, location.params)) {
        return _createRoute(record, location, redirectedFrom)
      }
    }
  }
  // 没有匹配的路由 返回空的路由
  return _createRoute(null, location)
}


通过matcher的match方法(有name匹配name,没有就匹配path,然后返回,默认重新生成一条路由返回) 解析用户的路由配置并按照route类型返回,然后路由切换就按照这个route来。

根据匹配的条件创建路由 _createRoute()

src/create-matcher.js

function _createRoute (
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
    // 根据条件创建不同的路由
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    return createRoute(record, location, redirectedFrom, router)
  }

  return {
    match,
    addRoute,
    getRoutes,
    addRoutes
  }
}

createRoute ()

src/util/route.js

export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}
  try {
    // 深拷贝
    query = clone(query)
  } catch (e) {}
  // 创建路由对象
  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) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  // 通过Object.freeze定义的只读对象 route
  return Object.freeze(route)
}


// 获得包含当前路由的所有嵌套路径片段的路由记录
// 包含从根路由到当前路由的匹配记录,从上至下
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record)
    record = record.parent
  }
  return res
}

6. 确认跳转

至此匹配路由已经完成,我们回到 transitionTo 函数中,接下来执行 confirmTransition

confirmTransition(route: Route, onComplete: Function, onAbort?: Function) {
  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(route, current) &&
    route.matched.length === current.matched.length
  ) {
    this.ensureURL()
    return abort()
  }
  // 通过对比路由解析出可复用的组件,需要渲染的组件,失活的组件
  const { updated, deactivated, activated } = resolveQueue(
    this.current.matched,
    route.matched
  )
  
  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)
      }
  }
  // 导航守卫数组
  const queue: Array<?NavigationGuard> = [].concat(
    // 失活的组件钩子
    extractLeaveGuards(deactivated),
    // 全局 beforeEach 钩子
    this.router.beforeHooks,
    // 在当前路由改变,但是该组件被复用时调用
    extractUpdateHooks(updated),
    // 需要渲染组件 enter 守卫钩子
    activated.map(m => m.beforeEnter),
    // 解析异步路由组件
    resolveAsyncComponents(activated)
  )
  // 保存路由
  this.pending = route
  // 迭代器,用于执行 queue 中的导航守卫钩子
  const iterator = (hook: NavigationGuard, next) => {
  // 路由不相等就不跳转路由
    if (this.pending !== route) {
      return abort()
    }
    try {
    // 执行钩子
      hook(route, current, (to: any) => {
        // 只有执行了钩子函数中的 next,才会继续执行下一个钩子函数
        // 否则会暂停跳转
        // 以下逻辑是在判断 next() 中的传参
        if (to === false || isError(to)) {
          // next(false) 
          this.ensureURL(true)
          abort(to)
        } else if (
          typeof to === 'string' ||
          (typeof to === 'object' &&
            (typeof to.path === 'string' || typeof to.name === 'string'))
        ) {
        // next('/') 或者 next({ path: '/' }) -> 重定向
          abort()
          if (typeof to === 'object' && to.replace) {
            this.replace(to)
          } else {
            this.push(to)
          }
        } else {
        // 这里执行 next
        // 通过 runQueue 中的 step(index+1) 执行 next()
          next(to)
        }
      })
    } catch (e) {
      abort(e)
    }
  }
  // 经典的同步执行异步函数
  runQueue(queue, iterator, () => {
    const postEnterCbs = []
    const isValid = () => this.current === route
    // 当所有异步组件加载完成后,会执行这里的回调,也就是 runQueue 中的 cb()
    // 接下来执行 需要渲染组件中的 beforeRouteEnter 导航守卫钩子
    const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
    // beforeResolve 解析路由钩子
    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()
          })
        })
      }
    })
  })
}
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => {
  // 队列中的函数都执行完毕,就执行回调函数
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
      // 执行迭代器,用户在钩子函数中执行 next() 回调
      // 回调中判断传参,没有问题就执行 next(),也就是 fn 函数中的第二个参数
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  // 取出队列中第一个钩子函数
  step(0)
}

7. 导航守卫

导航守卫在 确认路由跳转中出现

const queue: Array<?NavigationGuard> = [].concat(
    // 失活的组件钩子
  	/*
     *  找出组件中对应的钩子函数, 给每个钩子函数添加上下文对象为组件自身
     *  数组降维,并且判断是否需要翻转数组,因为某些钩子函数需要从子执行到父,
     *  获得钩子函数数组
     */ 
    extractLeaveGuards(deactivated),
    // 全局 beforeEach 钩子, 将函数 push 进 beforeHooks 中。
    this.router.beforeHooks,
    // 在当前路由改变,但是该组件被复用时调用
    extractUpdateHooks(updated),
    // 需要渲染组件 beforeEnter 守卫钩子
    activated.map(m => m.beforeEnter),
    // 解析异步路由组件
    resolveAsyncComponents(activated)
)

  1. 先执行失活组件 deactivated 的钩子函数 ,找出对应组件中的钩子函数

function extractLeaveGuards(deactivated: Array<RouteRecord>): Array<?Function> {
// 传入需要执行的钩子函数名  失活组件触发 beforeRouteLeave 
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}


function extractGuards(
  records: Array<RouteRecord>,
  name: string,
  bind: Function,
  reverse?: boolean
): Array<?Function> {
  const guards = flatMapComponents(records, (def, instance, match, key) => {
   // 找出组件中对应的钩子函数
    const guard = extractGuard(def, name)
    if (guard) {
    // 给每个钩子函数添加上下文对象为组件自身
      return Array.isArray(guard)
        ? guard.map(guard => bind(guard, instance, match, key))
        : bind(guard, instance, match, key)
    }
  })
  // 数组降维,并且判断是否需要翻转数组
  // 因为某些钩子函数需要从子执行到父
  return flatten(reverse ? guards.reverse() : guards)
}
export function flatMapComponents (
  matched: Array<RouteRecord>,
  fn: Function
): Array<?Function> {
// 数组降维
  return flatten(matched.map(m => {
  // 将组件中的对象传入回调函数中,获得钩子函数数组
    return Object.keys(m.components).map(key => fn(
      m.components[key],
      m.instances[key],
      m, key
    ))
  }))
}
  1. 执行全局 beforeEach 钩子函数, 将函数 push 进 beforeHooks 中。
beforeEach(fn: Function): Function {
    return registerHook(this.beforeHooks, fn)
}
function registerHook(list: Array<any>, fn: Function): Function {
  list.push(fn)
  return () => {
    const i = list.indexOf(fn)
    if (i > -1) list.splice(i, 1)
  }
}
  1. 执行 beforeRouteUpdate 钩子函数 与 deactivated 实现类似
  2. 执行 beforeEnter 独享路由钩子
  3. 解析异步组件
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
  return (to, from, next) => {
    let hasAsync = false
    let pending = 0
    let error = null
    // 扁平化数组 获取 组件中的钩子函数数组
    flatMapComponents(matched, (def, _, match, key) => {
    // 判断是否是异步组件
      if (typeof def === 'function' && def.cid === undefined) {
        // 异步组件
        hasAsync = true
        pending++
        // 成功回调
        // once 函数确保异步组件只加载一次
        const resolve = once(resolvedDef => {
          if (isESModule(resolvedDef)) {
            resolvedDef = resolvedDef.default
          }
          // 判断是否是构造函数
          // 不是的话通过 Vue 来生成组件构造函数
          def.resolved = typeof resolvedDef === 'function'
            ? resolvedDef
            : _Vue.extend(resolvedDef)
        // 赋值组件
        // 如果组件全部解析完毕,继续下一步
          match.components[key] = resolvedDef
          pending--
          if (pending <= 0) {
            next()
          }
        })
        // 失败回调
        const reject = once(reason => {
          const msg = `Failed to resolve async component ${key}: ${reason}`
          process.env.NODE_ENV !== 'production' && warn(false, msg)
          if (!error) {
            error = isError(reason)
              ? reason
              : new Error(msg)
            next(error)
          }
        })
        let res
        try {
        // 执行异步组件函数
          res = def(resolve, reject)
        } catch (e) {
          reject(e)
        }
        if (res) {
        // 下载完成执行回调
          if (typeof res.then === 'function') {
            res.then(resolve, reject)
          } else {
            const comp = res.component
            if (comp && typeof comp.then === 'function') {
              comp.then(resolve, reject)
            }
          }
        }
      }
    })
    // 不是异步组件直接下一步
    if (!hasAsync) next()
  }
}

异步组件解析后会执行 runQueue 中的回调函数

  // 经典的同步执行异步函数
  runQueue(queue, iterator, () => {
    const postEnterCbs = [] // 存放beforeRouteEnter 中的回调函数
    const isValid = () => this.current === route
    // 当所有异步组件加载完成后,会执行这里的回调,也就是 runQueue 中的 cb()
    // 接下来执行 需要渲染组件中的 beforeRouteEnter 导航守卫钩子
    const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
    // beforeResolve 导航守卫钩子
    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()
          })
        })
      }
    })
  })
  1. 执行 beforeRouterEnter ,因为在 beforeRouterEnter 在路由确认之前组件还未渲染,所以此时无法访问到组件的 this 。

但是该钩子函数在路由确认执行,是唯一一个支持在 next 回调中获取 this 对象的函数。


// beforeRouteEnter 钩子函数
function extractEnterGuards (
  activated: Array<RouteRecord>,
  cbs: Array<Function>,
  isValid: () => boolean
): Array<?Function> {
  return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
    return bindEnterGuard(guard, match, key, cbs, isValid)
  })
}

function bindEnterGuard (
  guard: NavigationGuard,
  match: RouteRecord,
  key: string,
  cbs: Array<Function>,
  isValid: () => boolean
): NavigationGuard {
  return function routeEnterGuard (to, from, next) {
    return guard(to, from, cb => {
      next(cb)
      if (typeof cb === 'function') {
        // 判断 cb 是否是函数
        // 是的话就 push 进 postEnterCbs
        cbs.push(() => {
          // #750
          // if a router-view is wrapped with an out-in transition,
          // the instance may not have been registered at this time.
          // we will need to poll for registration until current route
          // is no longer valid.
           // 循环直到拿到组件实例
          poll(cb, match.instances, key, isValid)
        })
      }
    })
  }
}

// 该函数是为了解决 issus #750
// 当 router-view 外面包裹了 mode 为 out-in 的 transition 组件 
// 会在组件初次导航到时获得不到组件实例对象
function poll (
  cb: any, // somehow flow cannot infer this is a function
  instances: Object,
  key: string,
  isValid: () => boolean
) {
  if (instances[key]) {
    cb(instances[key])
  } else if (isValid()) {
    // setTimeout 16ms 作用和 nextTick 基本相同
    setTimeout(() => {
      poll(cb, instances, key, isValid)
    }, 16)
  }
}
  1. 执行 beforeResolve 导航守卫钩子,如果注册了全局 beforeResolve 钩子就会在这里执行。
  2. 导航确认完成后 updateRoute 切换路由,更新路由信息后 调用 afterEach 导航守卫钩子
updateRoute (route: Route) {
  // 更新当前路由信息  对组件的 _route 属性进行赋值,触发组件渲染
  const prev = this.current
  this.current = route
  this.cb && this.cb(route)  // 实际执行 init传入的回调, app._route = route 对组件的 _route 属性进行赋值
  // 路由跳转完成 调用 afterHooks 中的钩子函数
  this.router.afterHooks.forEach(hook => {
    hook && hook(route, prev)
  })
}

this.cb 是怎么来的呢? 其实 this.cb 是通过 History.listen 实现的,在VueRouter 的初始化 init 过程中对 this.cb 进行了赋值

//  History 类中 的listen 方法对this.cb 进行赋值
listen (cb: Function) {
  this.cb = cb
}

//  init 中执行了 history.listen,将回调函数赋值给 this.cb
init (app: any /* Vue component instance */) {
  this.apps.push(app)
  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}
  1. 触发组件的渲染

当app._router 发生变化时触发 vue 的响应式调用render() 将路由相应的组件渲染到中

app._route = route  

hash 模式的实现

hash模式的原理是监听浏览器url中hash值的变化,并切换对应的组件

class HashHistory extends History  {
   constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }
  // 监听 hash 的变化
  setupListeners () {
    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
    
    if (supportsScroll) {
      setupScroll()
    }
    
    window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      // 传入当前的 hash 并触发跳转
      this.transitionTo(getHash(), route => {
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    })
  }   
}

// 如果浏览器没有 # 则自动补充 /#/
function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}


export default HashHistory

如果手动刷新页面的话,是不会触发hashchange事件的,也就是找不出组件来,那咋办呢?刷新页面肯定会使路由重新初始化,咱们只需要在初始化函数init 上执行一次原地跳转就行。

router-view 组件渲染

组件渲染的关键在于 router-view ,将路由变化时匹配到的组件进行渲染。 routerView是一个函数式组件,函数式组件没有data,没有组件实例。 因此使用了父组件中的$createElement函数,用以渲染组件,并且在组件渲染的各个时期注册了hook 如果被 keep-alive 包裹则直接使用缓存的 vnode 通过 depth 实现路由嵌套, 循环向上级访问,直到访问到根组件,得到路由的 depth 深度

export default {
  name: 'RouterView',
  /* 
    https://cn.vuejs.org/v2/api/#functional
    使组件无状态 (没有 data ) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使他们更容易渲染。
  */
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    /* 标记位,标记是route-view组件 */
    data.routerView = true

    /* 直接使用父组件的createElement函数  因此router-view渲染的组件可以解析命名槽*/
    const h = parent.$createElement
    /* props的name,默认'default' */
    const name = props.name
    /* option中的VueRouter对象 */
    const route = parent.$route
    /* 在parent上建立一个缓存对象 */
    const cache = parent._routerViewCache || (parent._routerViewCache = {})


    /* 记录组件深度 用于实现路由嵌套 */
    let depth = 0
    /* 标记是否是待用(非alive状态)) */
    let inactive = false
    /* _routerRoot中中存放了根组件的势力,这边循环向上级访问,直到访问到根组件,得到depth深度 */
    // 用 depth 帮助找到对应的 RouterRecord
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        // 遇到其他的 router-view 组件则路由深度+1 
        depth++
      }
      /* 如果_inactive为true,代表是在keep-alive中且是待用(非alive状态) */
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    /* 存放route-view组件的深度 */
    data.routerViewDepth = depth

    /* 如果inactive为true说明在keep-alive组件中,直接从缓存中取 */
    if (inactive) {
      return h(cache[name], data, children)
    }

    // depth 帮助 route.matched 找到对应的路由记录
    const matched = route.matched[depth]

    /* 如果没有匹配到的路由,则渲染一个空节点 */
    if (!matched) {
      cache[name] = null
      return h()
    }

    /* 从成功匹配到的路由中取出组件 */
    const component = cache[name] = matched.components[name]

    // attach instance registration hook
    // this will be called in the instance's injected lifecycle hooks
    /* 注册实例的registration钩子,这个函数将在实例被注入的加入到组件的生命钩子(beforeCreate与destroyed)中被调用 */
    data.registerRouteInstance = (vm, val) => {  
      /* 第二个值不存在的时候为注销 */
      // val could be undefined for unregistration
      /* 获取组件实例 */
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        /* 这里有两种情况,一种是val存在,则用val替换当前组件实例,另一种则是val不存在,则直接将val(这个时候其实是一个undefined)赋给instances */
        matched.instances[name] = val
      }
    }

    // also register instance in prepatch hook
    // in case the same component instance is reused across different routes
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }

    // resolve props
    let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
    if (propsToPass) {
      // clone to prevent mutation
      propsToPass = data.props = extend({}, propsToPass)
      // pass non-declared props as attrs
      const attrs = data.attrs = data.attrs || {}
      for (const key in propsToPass) {
        if (!component.props || !(key in component.props)) {
          attrs[key] = propsToPass[key]
          delete propsToPass[key]
        }
      }
    }

    return h(component, data, children)
  }
}

嵌套路由的实现

routerView的render函数通过定义一个depth参数,来判断当前嵌套的路由是位于matched函数层级,然后取出对应的record对象,渲染器对应的组件。

router-link 组件

router-link 的本质是 a 标签,在标签上绑定了click事件,然后执行对应的VueRouter实例的push()实现的

export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,  // 当点击时会调用router.replace()而不是router.push(),这样导航后不会留下history记录
    activeClass: String,
    exactActiveClass: String,
    event: {
      type: eventTypes,
      default: 'click'   // 默认为 click 事件
    }
  },
  render (h: Function) {
    // 获取 $router 实例
    const router = this.$router
    // 获取当前路由对象
    const current = this.$route

    // 要跳转的地址
    const { location, route, href } = router.resolve(this.to, current, this.append)
    const classes = {}
    const globalActiveClass = router.options.linkActiveClass
    const globalExactActiveClass = router.options.linkExactActiveClass
    // Support global empty active class
    const activeClassFallback = globalActiveClass == null
            ? 'router-link-active'
            : globalActiveClass
    const exactActiveClassFallback = globalExactActiveClass == null
            ? 'router-link-exact-active'
            : globalExactActiveClass
    const activeClass = this.activeClass == null
            ? activeClassFallback
            : this.activeClass
    const exactActiveClass = this.exactActiveClass == null
            ? exactActiveClassFallback
            : this.exactActiveClass
    const compareTarget = location.path
      ? createRoute(null, location, null, router)
      : route

    classes[exactActiveClass] = isSameRoute(current, compareTarget)
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget)

    const handler = e => {
      //  绑定点击事件
      //  若设置了 replace 属性则使用 router.replace 切换路由
      //  否则使用 router.push 更新路由
      if (guardEvent(e)) {
        if (this.replace) {
          //  router.replace()  导航后不会留下history记录
          router.replace(location)
        } else {
          router.push(location)
        }
      }
    }

    const on = { click: guardEvent }  // <router-link> 组件默认都支持的click事件 
    if (Array.isArray(this.event)) {
      this.event.forEach(e => { on[e] = handler })
    } else {
      on[this.event] = handler
    }

    const data: any = {
      class: classes     
    }

    if (this.tag === 'a') {   // 如果是 a 标签会绑定监听事件
      data.on = on  // 监听自身
      data.attrs = { href }
    } else {
      // find the first <a> child and apply listener and href
      const a = findAnchor(this.$slots.default)    // 如果不是 a标签则会 找到第一个 a 标签
      if (a) {                                     
        // in case the <a> is a static node        // 找到第一个 a 标签
        a.isStatic = false
        const extend = _Vue.util.extend
        const aData = a.data = extend({}, a.data)
        aData.on = on
        const aAttrs = a.data.attrs = extend({}, a.data.attrs)
        aAttrs.href = href
      } else {
        // doesn't have <a> child, apply listener to self
        data.on = on      // 如果没找到 a 标签就监听自身  
      }
    }

    //最后调用$createElement去创建该Vnode
    return h(this.tag, data, this.$slots.default)  
  }
}

// 阻止浏览器的默认事件,所有的事件都是通过 VueRouter 内置代码实现的
function guardEvent (e) {
  // don't redirect with control keys
  if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
  // don't redirect when preventDefault called
  if (e.defaultPrevented) return
  // don't redirect on right click
  if (e.button !== undefined && e.button !== 0) return
  // don't redirect if `target="_blank"`
  if (e.currentTarget && e.currentTarget.getAttribute) {
    const target = e.currentTarget.getAttribute('target')
    if (/\b_blank\b/i.test(target)) return
  }
  // this may be a Weex event which doesn't have this method
  if (e.preventDefault) {
    e.preventDefault()
  }
  return true
}

何时触发视图更新

在混入 beforeCreate 时 对 _route 作了响应式处理,即访问vm._route时会先向dep收集依赖

beforeCreate () {
      // 判断组件是否存在 router 对象,该对象只在根组件上有
      if (isDef(this.$options.router)) {
        // 根路由设置为自己
        this._routerRoot = this
        //  this.$options.router就是挂在根组件上的 VueRouter 实例
        this._router = this.$options.router
        // 执行VueRouter实例上的init方法,初始化路由
        this._router.init(this)
        // 很重要,为 _route 做了响应式处理
        //   即访问vm._route时会先向dep收集依赖, 而修改 _router 会触发组件渲染
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 用于 router-view 层级判断
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },

//  访问vm._route时会先向dep收集依赖
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

访问 $router 时触发依赖收集

  • 在组件中使用 this.$router
  • router-link 组件内部

何时触发 dep.notify 呢? 路由导航实际执行的history.push方法 会触发 tansitionTo

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

在确认路由后执行回调时会通过 updateRoute 触发 this.$route 的修改

updateRoute (route: Route) {
  // 更新当前路由信息  对组件的 _route 属性进行赋值,触发组件渲染
  const prev = this.current
  this.current = route
  this.cb && this.cb(route)
  this.router.afterHooks.forEach(hook => {
    hook && hook(route, prev)
  })
}

其中 this.cb 在路由初始化过程中 通过history.listen 保存的

//  VueRouter 路由初始化时设置的 listen 回调
history.listen(route => {
  this.apps.forEach((app) => {
    //  $router 的更新 ==>> app._route=route则触发了set,即触发dep.notify向watcher派发更新
    app._route = route
  })
})


// history 类中 cb的取值
listen (cb: Function) {
  this.cb = cb
}

当组件重新渲染, vue 通过 router-view 渲染到指定位置 综上所述 路由触发组件更新依旧是沿用的vue组件的响应式核心, 在执行transitionTo 前手动触发依赖收集, 在路由transitionTo 过程中手动触发更新派发以达到watcher的重新update; 而之所以路由能正确的显示对应的组件,则得益于路由映射表中保存的路由树形关系

$router.push 切换路由的过程

vue-router 通过 vue.mixin 方法注入 beforeCreate 钩子,该混合在 beforeCreate 钩子中通过 Vue.util.defineReactive() 定义了响应式的 _route 。所谓响应式属性,即当 _route 值改变时,会自动调用 Vue 实例的 render() 方法,更新视图。 vm.render()是根据当前的_route 的 path,nam 等属性,来将路由对应的组件渲染到 router-view 中

  1. $router.push() //显式调用方法
  2. HashHistory.push() //根据hash模式调用, 设置hash并添加到浏览器历史记录(window.location.hash= XXX)
  3. History.transitionTo() // ==>> const route = this.router.match(location, this.current) 找到当前路由对应的组件
  4. History.confirmTransition() // 确认路由,在确认页面跳转后 触发路由守卫,并执行相应回调
  5. History.updateRoute() //更新路由
  6. {app._route= route} // 路由的更改派发更新 触发页面的更新
  7. vm.render() // 在 中进行 render 更新视图
  8. window.location.hash = route.fullpath (浏览器地址栏显示新的路由的path)

History.replace()

在 hash 模式下

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    replaceHash(route.fullPath)
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}


function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}

通过 window.location.replace 替换当前路由,这样不会将新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由。

history模式下

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    replaceState(cleanPath(this.base + route.fullPath))
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

监听地址栏

在地址栏修改 url 时 vueRouter 会发生什么变化

当路由采用 hash 模式时,监听了浏览器 hashChange 事件,在路由发生变化后调用 replaceHash()

  //  监听 hash 的变化
  setupListeners () {
    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      setupScroll()
    }
    
    window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      // 传入当前的 hash 并触发跳转
      this.transitionTo(getHash(), route => {
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    })
  }

在路由初始化的时候会添加事件 setupHashListener 来监听 hashchange 或 popstate;当路由变化时,会触发对应的 push 或 replace 方法,然后调用 transitionTo 方法里面的 updateRoute 方法来更新 _route,从而触发 router-view 的变化。 所以在浏览器地址栏中直接输入路由相当于代码调用了replace()方法,将路由替换成输入的 url。

在 history 模式下的路由监听是在构造函数中执行的,对 HTML5History 的 popstate 事件进行监听

window.addEventListener('popstate', e => {
  const current = this.current
  const location = getLocation(this.base)
  if (this.current === START && location === initLocation) {
    return
  }

  this.transitionTo(location, route => {
    if (supportsScroll) {
      handleScroll(router, route, current, true)
    }
  })
})

小结

页面渲染

1、Vue.use(Router) 注册 2、注册时调用 install 方法混入生命周期,定义 router 和 route 属性,注册 router-view 和 router-link 组件 3、生成 router 实例,根据配置数组(传入的routes)生成路由配置记录表,根据不同模式生成监控路由变化的History对象 4、生成 vue 实例,将 router 实例挂载到 vue 实例上面,挂载的时候 router 会执行最开始混入的生命周期函数 5、初始化结束,显示默认页面

路由点击更新

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

地址变化路由更新

1、HashHistory 和 HTML5History 会分别监控 hashchange 和 popstate 来对路由变化作对用的处理 2、HashHistory 和 HTML5History 捕获到变化后会对应执行 push 或 replace 方法,从而调用 transitionTo 3、然后更新 _route 触发 router-view 的变化

路由相关问题

1. vue-router响应路由参数的变化

  • 通过 watch 监听 route 对象
// 监听当前路由发生变化的时候执行
watch: {
  $route(to, from){
    console.log(to.path)
    // 对路由变化做出响应
  }
}
  • 组件中的 beforeRouteUpdate 路由守卫

在组件被复用的情况下,在同一组件中路由动态传参的变化 如: 动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,

beforeRouteUpdate(to, from, next){
  // to do somethings
}

2. keep-alive 缓存后获取数据

  • beforeRouteEnter

在每次组件渲染时执行 beforeRouterEnter

beforeRouteEnter(to, from, next){
    next(vm=>{
        console.log(vm)
        // 每次进入路由执行
        vm.getData()  // 获取数据
    })
},
  • actived

在 keep-alive 组件被激活时都会执行 actived 钩子

服务器端渲染期间 avtived 不被调用 
activated(){
	this.getData() // 获取数据
},

总结

当时在写这篇文的时候就是想着尽量能把各个知识点都串联上,建立完善的知识体系

这不写着写着就成了长文😂, 一旦开始就无法停下,那就硬着头皮继续吧

不过这篇长文真的是有够长的,哈哈哈哈,能坚持看到这里的同学我都感到佩服😘

如果觉得还有哪里缺失的点可以及时告诉我哦

那么今天就先到这啦😜

参考

  1. VueRouter 源码深度解析
  2. 7张图,从零实现一个简易版Vue-Router,太通俗易懂了!
  3. Vue-Router面试题汇总