# vue-router原理

# 前言

  1. vue作为最受欢迎的前端框架之一,vue-router又是官方推出的路由管理库
  2. SPA思想
  3. 面试
  4. 源码预览 (opens new window)
  5. 本文基于vue-router@3.x版本

# 应用配置

一般会这么配置

shell安装
npm install (or yarn add) vue-router


router.js

import VueRouter from 'vue-router'
import Vue from 'vue'

Vue.use(VueRouter)
const router = new VueRouter({
    routes: ....,
    base,
    mode: 'hash | history',
    ...others
})


export default router;


main.js

import router from 'router.js';

new Vue({
    router,
    ...
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

页面使用路由跳转

image.png

# 思考几个点

上面这几段代码值得思考的有几个点

  1. router.js中单独引入vue,并通过vue.use()挂载到全局,和main.js中引入的vue,会是同一个实例吗?
  2. vue.use()这中间会涉及到什么
  3. vue-router暴露了router-link、router-view这两个全局组件
  4. 另外vue原型挂载了$route、$router这个实例、并附带了一些方法。如push、replace等等
  5. 另外vue-router有三种mode模式。history、hash、abstract这中间到底干了什么

下面带着这些点逐步探究

# 不同文件间引入vue、这中间有影响吗?(main.js中引入vue、和router.js中都从vue包中引入,最后实例this会有冲突吗?)

不会

可以这么理解,单文件作用域都是隔离的,也就是说这中间vue,只会引入一次,符合tree-shaking,最后都会合并同一个实例上面

# vue.use()执行机制

vue.use注册一个组件,会调用这个组件实例上的install方法、并将vue以形参传入install方法

# 首先挂载vue-router


import { install } from './install';
import { createMatcher } from './create-matcher';
import { HashHistory } from './history/hash';
import { HTML5History } from './history/html5';
import { AbstractHistory } from './history/abstract';
const inBrowser = typeof window !== 'undefined';

export default class VueRouter {
  constructor(options) {
    // 路由配置
    this.options = options;
    // 创建路由matcher对象,传入routes路由配置列表及VueRouter实例,主要负责url匹配
    this.matcher = createMatcher(options.routes);

    let mode = options.mode || 'hash';

    // 支持所有 JavaScript 运行环境,非浏览器环境强制使用abstract模式,主要用于SSR
    if (!inBrowser) {
      mode = 'abstract';
    }

    this.mode = mode;

    // 根据不同mode,实例化不同history实例
    switch (mode) {
      case 'history':
        console.log('history');
        this.history = new HTML5History(this);
        break;
      case 'hash':
        console.log('hash');
        this.history = new HashHistory(this);
        break;
      case 'abstract':
        console.log('abstract');
        this.history = new AbstractHistory(this);
        break;
      default:
        if (process.env.NODE_ENV !== 'production') {
          throw new Error(`[vue-router] invalid mode: ${mode}`);
        }
    }
  }

  init(app) {
    // 绑定destroyed hook,避免内存泄露
    app.$once('hook:destroyed', () => {
      this.app = null;

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

    // 存在即不需要重复监听路由
    if (this.app) return;

    this.app = app;

    // 跳转当前路由path匹配渲染 用于页面初始化
    this.history.transitionTo(
      // 获取当前页面 path
      this.history.getCurrentLocation(),
      () => {
        // 启动监听
        this.history.setupListeners();
      }
    );

    // 传入赋值回调,为_route赋值,进而触发router-view的重新渲染,当前路由对象改变时调用
    this.history.listen((route) => {
      app._route = route;
    });
  }

  // 匹配路由
  match(location) {
    return this.matcher.match(location);
  }

  // 获取所有活跃的路由记录列表
  getRoutes() {
    return this.matcher.getRoutes();
  }

  // 动态添加路由(添加一条新路由规则)
  addRoute(parentOrRoute, route) {
    this.matcher.addRoute(parentOrRoute, route);
  }

  // 动态添加路由(参数必须是一个符合 routes 选项要求的数组)
  addRoutes(routes) {
    this.matcher.addRoutes(routes);
  }

  // 导航到新url,向 history栈添加一条新访问记录
  push(location) {
    this.history.push(location);
  }

  // 在 history 记录中向前或者后退多少步
  go(n) {
    this.history.go(n);
  }

  // 导航到新url,替换 history 栈中当前记录
  replace(location, onComplete) {
    this.history.replace(location, onComplete);
  }

  // 导航回退一步
  back() {
    this.history.go(-1);
  }
}
VueRouter.install = install;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

install.js部分代码

Vue.mixin({
    // Vue创建前钩子,此生命周期$options已挂载完成
    beforeCreate() {
      // 通过判断组件实例this.$options有无router属性来判断是否为根实例
      // 只有根实例初始化时我们挂载了VueRouter实例router(main.js中New Vue({router})时)
      if (this.$options.router) {
        this._routerRoot = this;
        // 在 Vue 根实例添加 _router 属性( VueRouter 实例)
        this._router = this.$options.router;
        // 调用VueRouter实例初始化方法
        // _router即VueRouter实,此处this即Vue根实例
        this._router.init(this);
        // 把 ($route <=> _route) 处理为响应式的
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      } else {
        // 为每个组件实例定义_routerRoot,回溯查找_routerRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      }
    }
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  1. vue-router将实例以递归的形式挂载到根实例上,这样就避免了创建了多个实例对象,减少性能消耗
  2. 调用Vue.util.defineReactive响应式代理this._route达到监听页面变化
  3. this._router.init(this);其实调用的是router原型上init方法

# 不同路由模式监听机制

  1. 不同与传统页面渲染机制,vuejs以单页面应用的形式渲染(single page application)
  2. 单页面渲染的优势,首页资源加载比较多,但是用户跳转路由页面是不刷新的,这主要归功于html5提供的hash模式和history模式
  3. 通过监听hash或者history变化响应页面更新

image.png

base.js image.png

hash.js image.png

html5.js image.png

暴露的几个常用的api

image.png

# router-link、router-view

以全局组件形式挂载

image.png

router-link

image.png

# $router、 $route

image.png

# 最后

vue-router源码很多,源码阅读、手写源码等只是你学习这其中的思想,更多的是思考为什么要这么写 欢迎大家留言交流

最后更新时间: 5/21/2023, 10:34:40 PM