• vue-router源码分析(上)


    简介

    路由的概念相信大部分同学并不陌生,我们在用 Vue 开发过实际项目的时候都会用到 Vue-Router 这个官方插件来帮我们解决路由的问题。它的作用就是根据不同的路径映射到不同的视图。本文不再讲述路由的基础使用和API,不清楚的同学可以自行查阅官方文档vue-router3 对应 vue2vue-router4 对应 vue3。今天我们从源码出发以vue-router 3.5.3源码为例,一起来分析下Vue-Router的具体实现。

    由于篇幅原因,vue-router源码分析分上、中、下三篇文章讲解。

    vue-router源码分析(上)

    vue-router源码分析(中)

    vue-router源码分析(下)

    路由

    既然我们在分析路由,我们首先来说说什么是路由,什么后端路由、什么是前端路由。

    路由就是根据不同的 url 地址展示不同的内容或页面,早期路由的概念是在后端出现的,通过服务器端渲染后返回页面,随着页面越来越复杂,服务器端压力越来越大。后来ajax异步刷新的出现使得前端也可以对url进行管理,此时,前端路由就出现了。

    我们先来说说后端路由

    后端路由

    后端路由又可称之为服务器端路由,因为对于服务器来说,当接收到客户端发来的HTTP请求,就会根据所请求的URL,来找到相应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。

    对于最简单的静态资源服务器,可以认为,所有URL的映射函数就是一个文件读取操作。 对于动态资源,映射函数可能是一个数据库读取操作,也可能是进行一些数据的处理,等等。

    然后根据这些读取的数据,在服务器端就使用相应的模板来对页面进行渲染后,再返回渲染完毕的HTML页面。早期的jsp就是这种模式。

    前端路由

    刚刚也介绍了,在前后端没有分离的时候,服务端都是直接将整个 HTML 返回,用户每次一个很小的操作都会引起页面的整个刷新(再加上之前的网速还很慢,所以用户体验可想而知)。

    在90年代末的时候,微软首先实现了 ajax(Asynchronous JavaScript And XML) 这个技术,这样用户每次的操作就可以不用刷新整个页面了,用户体验就大大提升了。

    虽然数据能异步获取不用每个点击都去请求整个网页,但是页面之间的跳转还是会加载整个网页,体验不是特别好,还有没有更好的方法呢?

    至此异步交互体验的更高级版本 SPA单页应用 就出现了。单页应用不仅仅是在页面交互是无刷新的,连页面跳转都是无刷新的。既然页面的跳转是无刷新的,也就是不再向后端请求返回 HTML页面。

    页面跳转都不从后端获取新的HTML页面,那应该怎么做呢?所以就有了现在的前端路由。

    可以理解为,前端路由就是将之前服务端根据 url 的不同返回不同的页面的任务交给前端来做。在这个过程中,js会实时检测url的变化,从而改变显示的内容。

    前端路由优点是用户体验好,用户操作或页面跳转不会刷新页面,并且能快速展现给用户。缺点是首屏加载慢,因为需要js动态渲染展示内容。而且由于内容是js动态渲染的所以不利于SEO

    如何实现前端路由

    想实现一个路由到底需要些什么功能呢?

    路由映射表

    要实现路由,需要一个很重要的东西-路由映射表。

    • 服务端做路由页面跳转时,映射表的反映的是url页面的关系
    • 前端的路由映射表反映的是url组件的关系

    其实这个东西就是我们常写的routes数组。

    const routes = [
      {
        path: "/home",
        name: "Home",
        component: Home,
      },
      ...
    ] 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    有了映射表,我们就知道url和组件的映射关系。

    匹配器

    但是,映射表维护的只是一个关系,并不能帮我们完成,访问/home,返回Home组件这样一个流程,所以我们还需要一个匹配器,来帮我们完成从url组件的匹配工作。

    历史记录栈

    是不是有了路由映射表匹配器就可以实现前端路由了呢?其实是不够的,我们还需要一个存储记录访问过的url,以满足我们前进后退的需求。

    历史记录栈浏览器平台,已经原生支持,无需实现,直接调用接口即可(除了abstract模式,后面笔者会说)。

    总结

    前端路由的具体流程如下:

    1. 当我们访问某个url时,如/home,匹配器会拿着/home去路由映射表中去查找对应的组件,并将组件返回渲染,同时将访问记录推入历史栈中。

    2. 当我们通过前进/后退去访问某个url时,会先从历史栈中找到对应url,然后匹配器拿url去找组件,并返回渲染;只不过,这是通过前进/后退实现访问的,所以不需要再推入历史栈了。

    有了这三个概念并大概清楚了前端路由流程就能更好的理解后面的知识了。

    源码调试分析方法

    笔者先来讲讲调试源码的方法。首先我们创建好vue项目并安装好vue-router,然后在node_modules里面找到vue-router包,源码在src里面。我们直接改源码会不会生效呢?答案是不会的,因为我们引入的文件并不是源码,而是打包后的文件,文件在dist目录下,是vue-router.ems.js

    我们看代码的时候就看src目录下的源码,需要修改源码或者加debuger的时候就复制方法名到vue-router.ems.js中找,找到后修改该文件对应方法即可。

    源码目录结构

    首先我们来看看目录结构如下

    image.png

    • dist目录是打包后的文件,这里我们不分析。

    • components目录是存放内置组件router-linkrouter-view

    • history是存实现各种路由模式的地方

    • util中存放的是一些辅助函数

    • index.jsvue-router的入口文件,也是vue-router类定义的地方

    • install.js是安装逻辑所在文件

    • typests类型定义文件

    vue-router 流程探索

    首先我们来看我们在使用vue-router的流程是怎样的。

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    // 1. 安装插件
    Vue.use(VueRouter)
    
    // 2. 定义路由组件
    const Home = { template: '<div>home</div>' }
    const About = { template: '<div>about</div>' }
    
    // 3. 实例化vue-router
    const router = new VueRouter({
      mode: 'history',
      routes: [
        { path: '/home', name: "Home", component: Home },
        { path: '/about', name: "About", component: About },
      ]
    })
    
    // 4. 实例化Vue并传递router
    new Vue({
      router, // 注入router实例
      template: `
        <div id="app">
          <ul>
            <li><router-link to="/home">home</router-link></li>
            <li><router-link to="/about">about</router-link></li>
           </ul>
           
          <router-view class="view"></router-view>
        </div>
      `
    }).$mount('#app') 
    
    • 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

    大概可以分为四步,首先是使用Vue.use()安装我们的插件,然后定义好路由组件,再然后是使用new VueRouter()并传递routes实例化我们的路由,最后使用new Vue()并传递router实例化我们的Vue

    这里总结起来就三个核心流程:安装、实例化、初始化。

    我们先来说说安装

    安装

    我们知道 Vue 提供了 Vue.use 的全局 API 来注册插件,我们使用插件的时候都会使用Vue.use来注册安装我们的插件,所以我们先来分析一下它的实现原理,定义在 vue/src/core/global-api/use.js 中(这是vue中的源码)。

    分析Vue.use方法

    export function initUse (Vue: GlobalAPI) {
      Vue.use = function (plugin: Function | Object) {
        const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
        if (installedPlugins.indexOf(plugin) > -1) {
          return this
        }
    
        const args = toArray(arguments, 1)
        args.unshift(this)
        if (typeof plugin.install === 'function') {
          plugin.install.apply(plugin, args)
        } else if (typeof plugin === 'function') {
          plugin.apply(null, args)
        }
        installedPlugins.push(plugin)
        return this
      }
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    Vue.use 接受一个 plugin 参数,并且维护了一个_installedPlugins数组,它存储所有注册过的 plugin;接着又会判断 plugin 有没有定义 install 方法,如果有的话则调用该方法,并且该方法执行的第一个参数是 Vue;最后把 plugin 存到 installedPlugins 中。

    可以看到 Vue 提供的插件注册机制很简单,每个插件都需要实现一个静态的 install 方法,当我们执行 Vue.use 注册插件的时候,就会执行这个 install 方法,并且在这个 install 方法的第一个参数我们可以拿到 Vue 对象,这样的好处就是作为插件的编写方不需要再额外去import Vue 了。

    分析Vue-Router.install方法

    Vue-Router 的入口文件是 src/index.js,其中定义了 VueRouter 类,也实现了 install 的静态方法。

    // index.js
    
    import { install } from './install' // 导入安装方法
    
    ...
    
    VueRouter.install = install
    
    ...
    
    // 浏览器环境,自动安装
    if (inBrowser && window.Vue) {
      window.Vue.use(VueRouter)
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我们可以看到,开头导入了install方法,并将其做为静态方法直接挂载到VueRouter上,这样在Vue.use(VueRouter)时,install方法就会被调用。

    如果在浏览器环境,并且通过script标签的形式引入Vue时(会在window上挂载Vue全局变量),会尝试自动使用Vue.use()方法来安装VueRouter

    我们接下来看看install.js

    // install.js
    
    import View from './components/view'
    import Link from './components/link'
    
    export let _Vue
    
    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({
        beforeCreate () {
          if (isDef(this.$options.router)) {
            this._routerRoot = this
            this._router = this.$options.router
            this._router.init(this)
            // 将 _route 变成响应式
            Vue.util.defineReactive(this, '_route', this._router.history.current)
          } else {
            this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
          }
          registerInstance(this, this)
        },
        destroyed () {
          registerInstance(this)
        }
      })
    
      Object.defineProperty(Vue.prototype, '$router', {
        get () { return this._routerRoot._router }
      })
    
      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
    } 
    
    • 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

    主要做了如下几件事情:

    避免重复安装

    为了确保 install 逻辑只执行一次,用了 install.installed 变量做已安装的标志位。

    传递Vue引用减少打包体积

    用一个全局的 _Vue 来接收参数 Vue,因为作为 Vue 的插件对 Vue 对象是有依赖的,但又不能去单独去 import Vue,因为那样会增加包体积,所以就通过这种方式拿到 Vue 对象。

    注册全局混入

    Vue-Router 安装最重要的一步就是利用 Vue.mixin,在beforeCreatedestroyed生命周期函数中注入路由逻辑。

    Vue.mixin我们知道就是全局 mixin,所以也就相当于每个组件的beforeCreatedestroyed生命周期函数中都会有这些代码,并在每个组件中都会运行。

    Vue.mixin({
      beforeCreate () {
        if (isDef(this.$options.router)) {
          this._routerRoot = this
          this._router = this.$options.router
          this._router.init(this)
          Vue.util.defineReactive(this, '_route', this._router.history.current)
        } else {
          this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
        }
        registerInstance(this, this)
      },
      destroyed () {
        registerInstance(this)
      }
    }) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这两个钩子中,this是指向当时正在调用钩子的vue实例

    这两个钩子中的逻辑,在安装流程中是不会被执行的,只有在组件实例化时执行到钩子时才会被调用

    先看混入的 beforeCreate 钩子函数

    它先判断了this.$options.router是否存在,我们在new Vue({router})时,router才会被保存到到Vue根实例$options上,而其它Vue实例$options上是没有router的,所以if中的语句只在this === new Vue({router})时,才会被执行,由于Vue根实例只有一个,所以这个逻辑只会被执行一次。

    对于根 Vue 实例而言,执行该钩子函数时定义了 this._routerRoot 表示它自身;this._router 表示 VueRouter 的实例 router,它是在 new Vue 的时候传入的;

    另外执行了 this._router.init() 方法初始化 router,这个逻辑之后介绍。

    然后用 defineReactive 方法把 this._route 变成响应式对象,保证_route变化时,router-view会重新渲染,这个我们后面在router-view组件中会细讲。

    我们再看下else中具体干了啥

    主要是为每个组件定义_routerRoot,对于子组件而言,由于组件是树状结构,在遍历组件树的过程中,它们在执行该钩子函数的时候 this._routerRoot 始终指向的离它最近的传入了 router 对象作为配置而实例化的父实例(也就是永远等于根实例)。

    所以我们可以得到,在每个vue组件都有 this._routerRoot === vue根实例this._router === router对象

    对于 beforeCreatedestroyed 钩子函数,它们都会执行 registerInstance 方法,这个方法的作用我们也是之后会介绍。

    添加$route、$router属性

    接着给 Vue 原型上定义了 $router$route 2 个属性的 get 方法,这就是为什么我们可以在组件实例上可以访问 this.$router 以及 this.$route

    Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
    })
    
    Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
    }) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们可以看到,$router其实返回的是this._routerRoot._router,也就是vue根实例上的router,因此我们可以通过this.$router来使用router的各种方法。

    $route其实返回的是this._routerRoot._route,其实就是this._router.history.current,也就是目前的路由对象,这个后面会细说。

    注册全局组件

    通过 Vue.component 方法定义了全局的 <router-link><router-view> 2 个组件,这也是为什么我们在写模板的时候可以直接使用这两个标签,它们的作用我想就不用笔者再说了吧。

    钩子函数的合并策略

    最后设置路由组件的beforeRouteEnterbeforeRouteLeavebeforeRouteUpdate守卫的合并策略。

    总结

    那么到此为止,我们分析了 Vue-Router 的安装过程,Vue 编写插件的时候通常要提供静态的 install 方法,我们通过 Vue.use(plugin) 时候,就是在执行 install 方法。Vue-Routerinstall 方法会给每一个组件注入 beforeCreatedestoryed 钩子函数,在beforeCreate 做一些私有属性定义和路由初始化工作。并注册了两个全局组件,然后设置了钩子函数合并策略。

    后面我们再来分析一下 VueRouter 对象的实现和它的初始化工作。

  • 相关阅读:
    等保合规是什么意思?怎么做?
    校园网页设计成品 学校班级网页制作模板 dreamweaver网页作业 简单网页课程成品 大学生静态HTML网页源码
    旋转框/微调按钮的基类--QAbstractSpinBox 类
    Linux内存管理(四):内存架构和内存模型简述
    软件工程毕业设计课题(71)微信小程序毕业设计PHP校园浴室预约小程序系统设计与实现
    ECCV 2022最新研究成果:全球首个text-sketch-image数据集FS-COCO
    leetcode算法每天一题010: 正则表达式,判断pattern和string是否匹配(动态规划)
    “元宇宙”最权威的解释来了!全国科技名词委研讨会达成共识
    AtomicInteger——Java中的多线程原子计数器
    intellij debug模式提示 : Method breakpoints may dramatically slow down debugging
  • 原文地址:https://blog.csdn.net/pfourfire/article/details/125446377