专业编程教程与实战项目分享平台

网站首页 > 技术文章 正文

深入分析Vue-Router原理,彻底看穿前端路由

ins518 2024-09-30 21:18:57 技术文章 12 ℃ 0 评论

前言

如今大前端的趋势下,你停下学习的脚步了吗?Vue3.0都Beta了,但是还是感觉有些知识点云里雾里的,小编研究了一下 Vue-Router 源码整理和总结了一些东西,看尤大大怎么设计的。希望能够对你们有所帮助,如果喜欢的话,可以帮忙点个赞:point_right:。

阅读本文之前,小编有三句话要说:

1.下面因为源码可能会变,所以没有贴源码,源码可以根据文章链接去github上下载

2.本文的基本思路是根据源码的 index.js 文件走的

安装

npm install vue-router
复制代码

使用方法见 官网

正文

1. install.js源码

源码地址: github.com/vuejs/vue-r…

1.1源码解析

首先在解析之前不得不说尤大大的细节做的是真好:+1:,第一行代码首先做了防止 VueRouter 的重复注册。

export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue
}
复制代码

接着使用了 Vue.mixin 混入的方法注册组件,使用了 beforeCreate 和 destoryed 两个钩子。

Vue.mixin({
    beforeCreate () { //生命周期创建之前,一般情况是给组件增加一些特定的属性的时候使用这个钩子,在业务逻辑中基本上使用不到
      if (isDef(this.$options.router)) { //isDef判断是否存在
        this._routerRoot = this  //this是根Vue实例
        this._router = this.$options.router   //把根实例上的router属性挂载到_router
        this._router.init(this)  //调用init初始化路由的方法
        //defineReactive数据劫持,一旦`this._router.history.current`值发生变化,更新_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)  //销毁实例
    }
  })
复制代码

beforeCreate 这个钩子代表生命周期创建之前,一般情况下是给组件增加一些特定的属性的时候才会使用的,在业务逻辑中基本上是使用不到的。在 beforeCreate 钩子中做了很重要的一步,判断根Vue实例上是否配置了 router ,也就是我们经常用 main.js 中的路由的注册。

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

Vue.config.productionTip = false

new Vue({
  router,   //:kissing_closed_eyes:就是这个地方:heart_eyes:
  render: h => h(App),
}).$mount('#app')
复制代码

如果没有配置会向他的父级查找,保证每一个节点上都有 _routerRoot 属性,解决根组件的 嵌套 问题,如果没有 this._routerRoot = (this.$parent && this.$parent._routerRoot) || this 这一行代码,我们子组件上没有 __routerRoot 属性。

Vue.util.defineReactive(this, '_route', this._router.history.current)
复制代码

defineReactive 这个方法是Vue中的核心方法之一,即响应式原理。一旦 this._router.history.current 值发生变化,更新 _route 。那么如果页面的路由改变是怎么改变 _route 的呢?在 index.js 的 init 方法里:

history.listen(route => {   //发布订阅模式每个 router 对象可能和多个 vue 实例对象(这里叫作 app)关联,每次路由改变会通知所有的实例对象。
      this.apps.forEach(app => {
        app._route = route
      })
    })
复制代码

registerInstance(this, this) 这个函数怎么理解呢?我认为就是 router-view 的注册函数, _parentVnode 是实例的虚拟父级节点,需要找到父级节点中的 router-view 。首先会去判断是否存在父子关系节点,根据节点的层级在 route 的 matched 的属性上找到对应的数据之后,如果组件的路径 component 或者路由 route.matched 没有匹配渲染会 render 一个 h() ,那么 data 上面就不会添加 registerRouteInstance 注册路由的函数;

const matched = route.matched[depth]
const component = matched && matched.components[name]

// render empty node if no matched route or no config component
if (!matched || !component) {
  cache[name] = null
  return h()
}
复制代码
registerInstance(this, this)
复制代码
const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
复制代码

registerInstance 这个方法在 beforeCreate 和 destroyed 的时候都被调用了一次,如果 val 值是 undefined 那么这个路由实例就会被注销,即 matched.instances[name] = undefined

data.registerRouteInstance = (vm, val) => {
  // val could be undefined for unregistration  val可能没有定义被注销
  const current = matched.instances[name]
  if (
    (val && current !== vm) ||
    (!val && current === vm)
  ) {
    matched.instances[name] = val
  }
}
复制代码

这两个方法是利用 Object.defineProperty 的 get 方法给 vue 原型上添加 $router 和 $route 属性,这样就和上面提到的 保证每一个节点上都有_routerRoot属性 相呼应,如果没有 _routerRoot ,这里的添加属性会报错。

//vue原型上添加$router属性
Object.defineProperty(Vue.prototype, '$router', {
  get () { return this._routerRoot._router }
})
//vue原型上添加$route属性
Object.defineProperty(Vue.prototype, '$route', {
  get () { return this._routerRoot._route }
})
复制代码

这里有个面试题: $route 和 $router 的区别:

  • $route 是一个对象
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) : []
  }
复制代码
  • $router 就是 VueRouter 的实例

注册 RouterView 和 RouterLink 组件。

Vue.component('RouterView', View)   //router-view组件
Vue.component('RouterLink', Link)   //router-link组件
复制代码

view 和 link 两个组件都是 函数组件

1.2总结

在 install.js 中主要做了如下几件事:

1、绑定父子节点路由的关系

2、路由导航改变响应式的原理

3、将组件的实例和路由的规则绑定到一起

4、注册全局的 $route 和 $router 方法

5、注册 router-link 和 router-view 组件

2. view.js源码

源码地址: github.com/vuejs/vue-r…

2.1源码解析

函数组件中主要包含了 props 和 render 两部分。

props 中配置项 name 默认是 default 与之对应的就是路由的 命名视图 部分

props: {
    name: {
      type: String,
      default: 'default'
    }
  },
复制代码

render 部分对应两个参数 _ , {props, children, parent, data} ,其中 _ 对应的是 createElement 方法, {props, children, parent, data} 对应的是 context ,即:

props
children
parent
data

通过当前路由地址所属的层级,找到在 matched 的位置,进行对应的渲染,如果的找不到不进行渲染。如果是父节点找到 keepAlive 的状态,之前加载过的直接使用直接的缓存,如果没有渲染一个空页面。

2.2总结

在 view.js 中主要是做了如下几件事:

1、一直向父级查找,找到当前路由所属的层级,找到对应的 router-view 进行渲染。

2、判断 keepAlive 的状态决定如何渲染。

3.link.js源码

源码地址: github.com/vuejs/vue-r…

3.1 源码解析

与 router-view 一样 router-link 也是一个函数组件,其中 tag 默认会被渲染成一个 a 标签.

props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    ariaCurrentValue: {
      type: String,
      default: 'page'
    },
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
复制代码

通过这些参数的配置调用render()方法中的 h(this.tag, data, this.$slots.default) 渲染 vnode ,即 <tag data >{this.$slots.default}</tag>

4.create-matcher.js源码

源码地址: github.com/vuejs/vue-r…

4.1源码解析

export function createMatcher (
  routes: Array<RouteConfig>,  //router中的routes
  router: VueRouter    //router的配置
):
复制代码

createMatcher 方法利用 createRouteMap 这个方法去格式化路由,而 createRouteMap 这个方法最终返回3个参数 pathList , pathMap , nameMap ,同时通过遍历和递归调用 addRouteRecord 方法对一系列的属性(包括 name , path , children , props , 路径正则 , 匹配规则是否开启大小写 等)进行判断和格式化之后返回需要的数据格式。

pathList: Array<string>,  //列表
  pathMap: Dictionary<RouteRecord>,  //字典
  nameMap: Dictionary<RouteRecord>  //字典
复制代码

拿到这些数据之后,返回了两个方法 addRoutes 和 match

4.2 总结

1. create-matcher.js 主要的作用是拿到处理好的数据格式之后,导出两个核心方法

2. create-route-map.js 主要的作用是处理数据的格式。

5.路由模式源码

源码地址: github.com/vuejs/vue-r…

5.1源码解析

源码的结构是这样的:

首先定义了 History 类, HashHistory 、 HTML5History 、 AbstractHistory 都是继承 History 。

1、 hash 对应的是 HashHistory ,这个类里面主要的核心方法是 setupListeners 通过判断浏览器或者手机是否支持 supportsPushState 即 window.history.pushState 属性。如果不懂 pushState 可以阅读我的一篇文章 <一文带你真正了解histroy> 。如果支持监听 popstate 事件,如果不支持监听 hashchange 事件,在你采用浏览器前进后退时或者触发 go() 等事件来触发 popstate 。在监听之后采用发布订阅模式有一个事件移除机制,很细节哦。如果不支持 supportsPushState 使用 window.location.hash 或者 window.location.replace||assgin 。最后通过调用 base.js 中的基础类中的 transitionTo 方法通过 this.router.match 匹配到路由之后,通知路由的更新.

history.listen(route => {   //发布订阅模式
      this.apps.forEach(app => {
        app._route = route  //$route的改变
      })
    })
复制代码

2、 history 对应的是 HTML5History ,这个类里面主要的核心方法是 setupListeners 监听了 popstate 事件。

3、 abstract 对应的是 AbstractHistory ,这个类主要的核心声明了一个列表,判断列表里有没有这个路由或者下标,然后直接通知路由的更新。

5.2总结

路由模式 主要做了如下几件事:

1、通过对路由模式的不同监听不同的事件, hash 监听 popstate 和 hashchange 事件; history 监听 popstate 事件

2、通用 transitionTo 方法去更新路由信息。

补充知识: 判断数据类型的四种方法: typeof instanceof constructor Object.prototype.toString.call

结尾

上面内容是通过 index.js 文件的思路串行下来的。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表