前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入分析Vue-Router原理,彻底看穿前端路由

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

作者头像
小丑同学
发布2020-09-21 11:00:06
2K0
发布2020-09-21 11:00:06
举报
文章被收录于专栏:小丑的小屋小丑的小屋

前言

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

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

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

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

安装

代码语言:javascript
复制
npm install vue-router

使用方法见官网[1]

正文

1. install.js 源码

源码地址:https://github.com/vuejs/vue-router/blob/dev/src/install.js

1.1 源码解析

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

代码语言:javascript
复制
export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue
}

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

代码语言:javascript
复制
 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中的路由的注册。

代码语言:javascript
复制
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,   //?就是这个地方?
  render: h => h(App),
}).$mount('#app')

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

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

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

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

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

代码语言:javascript
复制
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()
}
代码语言:javascript
复制
registerInstance(this, this)
代码语言:javascript
复制
const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

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

代码语言:javascript
复制
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.definePropertyget方法给vue原型上添加$router$route属性,这样就和上面提到的保证每一个节点上都有_routerRoot 属性相呼应,如果没有_routerRoot,这里的添加属性会报错。

代码语言:javascript
复制
//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是一个对象
代码语言:javascript
复制
 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的实例

注册RouterViewRouterLink组件。

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

viewlink两个组件都是函数组件[2]

1.2 总结

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

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

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

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

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

5、注册router-linkrouter-view组件

2. view.js 源码

源码地址:https://github.com/vuejs/vue-router/blob/dev/src/components/view.js

2.1 源码解析

函数组件中主要包含了propsrender两部分。

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

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

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

  • props提供所有 prop 的对象
  • children:VNode 子节点的数组
  • parent:对父组件的引用
  • data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件

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

2.2 总结

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

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

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

3.link.js 源码

源码地址:https://github.com/vuejs/vue-router/blob/dev/src/components/link.js

3.1 源码解析

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

代码语言:javascript
复制
 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 源码

源码地址:https://github.com/vuejs/vue-router/blob/dev/src/create-matcher.js

4.1 源码解析
代码语言:javascript
复制
export function createMatcher (
  routes: Array<RouteConfig>,  //router中的routes
  router: VueRouter    //router的配置
):

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

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

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

4.2 总结

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

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

5.路由模式源码

源码地址:https://github.com/vuejs/vue-router/tree/dev/src/history

5.1 源码解析

源码的结构是这样的:

首先定义了History类,HashHistoryHTML5HistoryAbstractHistory都是继承History

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

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

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

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

5.2 总结

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

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

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

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

结尾

上面内容是通过index.js文件的思路串行下来的,当然还有很多细节需要补充,后续的文章会更精彩,有错误的地方欢迎指出,欢迎点评。

参考资料

[1]vue-router: https://router.vuejs.org/zh/

[2]函数式组件: https://cn.vuejs.org/v2/guide/render-function.html#%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BB%84%E4%BB%B6

[3]命名视图: https://router.vuejs.org/zh/guide/essentials/named-views.html

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-08-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小丑的小屋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 安装
  • 正文
    • 1. install.js 源码
      • 2. view.js 源码
        • 3.link.js 源码
          • 4.create-matcher.js 源码
            • 5.路由模式源码
              • 参考资料
          • 结尾
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档