VueRouter [路由机制原理]

735 阅读4分钟

前言

通过B站视频和一些童鞋的文章结合GitHub源码阅读来理解路由的实现原理

看过前章vuex状态管理的分享之后,相信对路由这块也是非常感兴趣的,同样的模式,同样的方式,我们走进GitHub之vue-router

同样直接走进 src 在这里插入图片描述

  • components:route-link 组件 和 router-view 组件 实现
  • history:关于浏览器相关,包含 hash模式 , basic模式 ,html5模式 以及非浏览器模式以及go 、push 、replace 、back 等各自的处理方法
  • util:不用多说,各种工具方法
  • create-mathcher: 这个就比较重要的了,创建路由的匹配和添加路由配置项的方法
  • create-mathcher-map:跟创建路由匹配直接相关,创建路由映射map表,并封装route对象
  • index: 入口文件,也是vueRouter构造函数实体,原型上定义了 go 、push 、replace 、back 等
  • install:初始化

老样子,Vue.use("vue-router") 会直接执行 install 初始化进行安装插件,这里就不多做解释了,不太理解的童鞋可以去前章看一下简单的介绍。

src/index.js 代码最后片段

VueRouter.install = install
VueRouter.version = '__VERSION__'

if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)  // 进行初始化和安装路由插件
}

install

src/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)
    }
  }

 // 类似 vuex 通过 vue.mixin 在生命周期创建前,将 router 挂载在vue根实例上
  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) // 销毁实例
    }
  })

 // 这里通过 Object.defineProperty 定义 '$router , $route' 的 get 来实现  
 // 而不使用 Vue.prototype.$router = this.this._routerRoot._router / .route, 
 // 是为了让其只读,不可修改
  
  Object.defineProperty(Vue.prototype, '$router', { 
    get () { return this._routerRoot._router }
  })
  Object.defineProperty(Vue.prototype, '$route', { 
    get () { return this._routerRoot._route }
  })

  // 注册全局组件 , RouterView , RouterLink,也就是我们常常使用的
  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
}

好,install.js 初始化写的很清楚,跟 vuex 非常相似,都是安装,注册相关组件,通过 mixin 在生命周期创建前将实例挂载在vue根实例上

VueRouter 核心

走进核心src/index.js vueRouter 构造函数 代码量较大,我们择取核心来看

根据mode确定模式

import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'

// constructor 里 通过 mode 来判断时哪一种模式, 默认 hash
let mode = options.mode || 'hash'

// 是否支持 history 模式 this.fallback 
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false 

 // 不支持则默认使用 hash
 if (this.fallback) { 
   mode = 'hash'
 }
 // 非浏览器操作,对应目录history里边的非浏览器模式
 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}`)
     }
 }

看到这里,可以分析出

  1. vueRouter 是根据 mode 的不同,分别调用不同的路由实现方式
  2. this.history 主要来自 history 模块 ,看上边代码引入路径 ./history/....后文有用 , 看一眼

History 模块

history 模块, 路由各种模式的实现模块,分别有 hash.js , html5.js ,sbstract.js ......

image.png

在这里,我们通过常用路由常用跳转方式 push 方法来分析整个流程

src/index.js vueRouter 构造函数 push 167 行

 push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // $flow-disable-line
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort)
    }
  }

上边有说根据 mode 的不同,调用不同的路由实现方式,这里就是直接调用对应 History 模块的路由处理方式 ,下边来具体看看到底是怎么实现的

hash.js 之 push

import { pushState, replaceState, supportsPushState } from '../util/push-state'

// 50 行
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
  this.transitionTo(
    location,
    route => {
      pushHash(route.fullPath) // 修改 hash
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    },
    onAbort
  )
}

// 切换路由 141行 对应方法 pushHash
function pushHash(path) {
 if (supportsPushState) { 
     // 是否支持 histrory 模式
     // `src/util/push-state.js` 有定义根据浏览器和版本来判断
     pushState(getUrl(path));
 } else {
     // Hash 模式实现,修改当前 url hash 值
     // 每修改一次,则会记录在 history 栈 ---  重点
     window.location.hash = path; 
 }
}

html5.js 之 push

import { pushState, replaceState, supportsPushState } from '../util/push-state'

// 44 行
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)
 }
 

htm5 - pushState

src/util/push-state.js

export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // html5 window history 对象
  const history = window.history
  try {
    if (replace) { 
      // repalce
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      history.replaceState(stateCopy, '', url)
    } else {
      // push 
      history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}
// replace 添加一个 flag 标识,继续调用 pushState
export function replaceState (url?: string) {
  pushState(url, true)
}

列举两种模式炸眼一看,大同小异呀,都是调用 this.transitionTo 方法, 然后一个调用 pushHash 一个调用 pushState , hash 是直接改写 window.loachtion.hash 值,记录在 history 栈中, html5 则是直接调用 pushState 方法 window history 对象内置方法, 不同的处理方式 。

注:其他的 go 、 replace 、ensureURL 、getCurrentLocation 等都是同样的实现方式

好,路由核心的实现原理已经大概了解了,那么路由的匹配规则在这里简单介绍一下,我也没有继续深入了哈,有兴趣的同学可以点击下方链接深入学习,我们走进 createMatcher 创建匹配器方法

src/create-matcher.js 16 行

export function createMatcher(
    routes: Array<RouteConfig>, // route 对象集合
    router: VueRouter // 当前路由实例 this 
): Matcher {
    // 创建路由映射 createRouteMap 重点,重点,重点方法
    const { pathList, pathMap, nameMap } = createRouteMap(routes);
    // 匹配规则
    function match(...){...} 
    /*
     匹配规则方法,根据路径来匹配路由,并创建路由对象 route , 也就是我们后来使用到的 this.$route 
     然后就是 create-matcher-map 创建映射关系表了,类似 {"/home":Home} 等于 { path:component}
     根据path hash路径来映射对应的组件
    */
    

bilibili 来进一步了解具体的实现方式,快速通道 vuex+vue-router

欢迎点赞,小小鼓励,大大成长

相关链接