Vue Router源码实现

Vuevue-router

# Vue-Router源码结构

  • src
    • components(组件)
      • link.js(RouterLink组件)
      • view.js(RouterView组件)
    • history(历史管理,VueRouter支持三种模式的历史管理)
      • abstract.js(class,abstract模式,服务端渲染支持的模式)
      • base.js(History类,三种历史管理的基类)
      • errors.js
      • hash.js(class,hash模式)
      • html5.js(class,history模式)
    • util(通用函数)
    • index.js(VueRouter类)
    • install.js(实现Vue插件)
    • create-matcher.js(创建匹配器)
    • create-route-map.js(解析路由)

# Vue.use()

官网解释 Vue-use (opens new window)

Vue.use(plugin) 如果是一个对象,必须提供install方法 如果是函数,会被作为install方法,直接调用

该方法在嗲用new Vue()之前被调用,当install方法被一个插件多次调用,只会被安装一次。 Vue.use(MyPlugin, { someOption: true }) 第二个参数是可选参数,是插件的参数

Vue.use(VueRouter) Vue.use是用来注册插件的,它会调用传入对象的install方法

src\core\global-api\use.js —— 源码位置

export function initUse (Vue: GlobalAPI) {
  // Vue.use(VueRouter, options)
  // 给Vue挂载了一个方法,传入参数
  Vue.use = function (plugin: Function | Object) {
    // installedPlugins已经安装的插件数组
    // this是Vue,因为是Vue.use
    // 获取Vue的已安装插件的数组,如果没有初始化为空数组
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 判断当前插件在不在数组中,如果已经注册过了就直接返回Vue,这样可以链式编程
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    // 把数组中的第一个元素(plugin)去除,获取剩余的全部参数
    const args = toArray(arguments, 1)
    // 把this(Vue)插入第一个元素的位置
    // install方法的第一个参数是this
    args.unshift(this)
    // 判断插件如果是对象就调用其install方法并把参数传递过去
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)  // plugin.install(args[0], args[1])
    // 判断插件如果是函数就调用自己并把参数传递过去  
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    // 将plugin加到已经安装的插件数组中
    installedPlugins.push(plugin)
    return this
  }
}
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

# VueRouter源码实现

# 搭建项目结构

  1. src/my-vue-router里面按照上面的结构先搭建文件
  2. 在index.js中先实现VueRouter类的基本结构
export default class VueRouter {
  constructor (options) {
    // 记录所有的路由规则
    this._routes = options.routes || []
  }

  init (app) {
    // app是Vue的实例
  }
}
1
2
3
4
5
6
7
8
9
10

# install方法

  • 注册 VueRouter 插件,并给 Vue 根实例,以及每一个子组件对象设置 _routerRoot ,让子组件可以获取到根实例,以及根实例中存储的 _router 对象。
// 这里因为传入了Vue的构造函数,为了别的模块方便使用,就记录到一个变量中,这样其他模块就不用每次都import Vue,这样这个模块就不用依赖Vue框架
export let _Vue = null
// install方法会传入一个参数:Vue的构造函数
export default function install (Vue) {
  // 判断插件是否注册过,可以参考源码
  if (install.installed && _Vue === Vue) return
  install.installed = true
  // 将传入的Vue构造函数赋值给变量供其他模块使用
  _Vue = Vue
  
  // _Vue.mixin方法是让所有的组件和实例都有beforeCreate钩子函数
  // Vue.prototype.xx 这种方式也可以挂载,这种方式是真的给所有的实例添加成员
  // 而下面的mixin方法是给根实例以及所有的组件添加 router 属性,vuex 也是通过这种方式挂载的
  _Vue.mixin({
    beforeCreate () {
      // 给所有 Vue 实例,增加 router 的属性
      // new Vue({
      //   router,
      //   render: h => h(App)
      // }).$mount('#app')
      // 参考这个根实例,因为我们会给根实例添加router,所以以这个作为根实例的判断依据
      if (this.$options.router) {
        this._router = this.$options.router
        // 为了让子组件也可以找到router方法,就设置一个根节点传递this(Vue)
        this._routerRoot = this
        // 初始化 router 独享
        this._router.init(this)

      // this.$parent 和 this.$children都是vue初始化的时候就有的属性
      // 子组件,无法正常获取,那就通过其父组件找到根实例,就找到根实例上面的_router了
      } else { 
        this._routerRoot = this.$parent && this.$parent._routerRoot
      }
    }
  })
}
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
  • 在index.js中给VueRouter添加install方法
// 导入install模块
import install from './install'
export default class VueRouter {
  constructor (options) {
    this._routes = options.routes || []
  }

  init (app) {
  }
}

// 将install方法挂载到插件中
VueRouter.install = install
1
2
3
4
5
6
7
8
9
10
11
12
13

# router-link、router-view

  • 此时创建这两个组件的目的是为了测试
  • router-link(创建跳转链接)
// 配置组件的选项,要注册成一个全局的组件
export default {
  // 组件名称
  name: 'RouterLink',
  // 参数 
  props: {
    // to:跳转的路由地址是什么
    to: {
      type: String,
      // 参数必须
      required: true
    }
  },
  // 里面的内容是变化的所以要用slot,默认插槽
  // template: '<a :href="{{ '#' + this.to }}"><slot name="default"></slot></a>'
  render (h) {
    // 属性选项attrs
    return h('a', { attrs: { href: '#' + this.to } }, [this.$slots.default])
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • router-view(创建模板渲染地址)
export default {
  name: 'RouterView',
  render (h) {
    // h() h函数如果没有传参数,那就输出一个空的注释 <!---->
    return h()
  }
}
1
2
3
4
5
6
7
  • 在install中将两个组件注册到Vue上
// 引入两个模块
import Link from './components/link'
import View from './components/view'
export let _Vue = null
export default function install (Vue) {
  ...
  _Vue.mixin({
    beforeCreate () {
      ...
    }
  })

  // 在全局注册的两个组件
  // 两个参数:第一个是组件名称,第二个是组件选项
  _Vue.component(Link.name, Link)
  _Vue.component(View.name, View)
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这个时候就可以替换vueRouter的代码可以简单运行了。

# createMatcher 和 createRouteMap

# createMatcher

  • 创建并返回一个匹配器,包含 match 方法和 addRoutes 方法
    • match 根据路由地址匹配相应的路由规则对象
    • addRoutes 动态添加路由规则

# createRouteMap

  • 把所有的路由规则解析成路由表

    • pathList 是一个数组,存储所有的路由地址
    • pathMap 路由表,路由地址 -> record 一个记录(path、component、parent)
  • 遍历所有的路由规则,生成路由表

  • 如果有子路由的话,递归添加子路由到路由表

  1. 先创建createMatcher的结构
import createRouteMap from './create-route-map'
// 导出方法createMatcher,因为要解析路由规则如果把routes传进来
export default function createMatcher (routes) {
  // 调用createRouteMap方法,创建路由规则,将参数传入
  // 解析完成之后返回一个路由表,一个对象:
  // pathList就是路由地址的数组,pathMap是个对象,里面记录了path的相关信息
  const { pathList, pathMap } = createRouteMap(routes)
  
  // 测试打印
  console.log(pathList) // ['/','/music','/music/pop','.music/rock','/about']
  console.log(pathMap) // {/:{...},...}
  
  // 匹配函数,根据path路径匹配路由信息的对象
  function match (path) {
    //TODO
  }
  
  // 添加新的路由规则,参数是新的路由规则,要添加到路由表中
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap)
  }
  // 返回两个方法
  return {
    match,
    addRoutes
  }
}
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
  1. 在create-route-map.js中创建createRouteMap函数
// 形式大概要转成这个样子 routes { path, component, ..... }
// createRouteMap方法,接收一个参数,路由规则
export default function createRouteMap (routes, pathList, pathMap) {
  // 定义pathList,存储所有的路由地址
  pathList = pathList || []
  // 定义pathMap,路由表,路径和组件的相关信息
  pathMap = pathMap || {}

  // 遍历所有的路由规则 routes,因为存在子路由可能需要递归处理,方便处理里面新起一个函数
  routes.forEach(route => {
    addRouteRecord(route, pathList, pathMap)
  })

  return {
    pathList,
    pathMap
  }
}

// 返回一个对象,对象里面记录了路由和组件的映射关系
/**
 * 
 * @param {*} route 路由
 * @param {*} pathList 路由列表
 * @param {*} pathMap 路由表
 * @param {*} parentRecord 如果这里有值就是子路由,对应起父路由,如果没有值就是第一层父路由
 */
function addRouteRecord (route, pathList, pathMap, parentRecord) {
  // 将path取出
  const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path
  // 记录信息变量,并将父路由的信息记录下来
  const record = {
    path: path,
    component: route.component,
    parentRecord: parentRecord
    // ...
  }

  // 判断当前路径,是否已经存储在路由表中了
  if (!pathMap[path]) {
    // 没有记录就开始记录,在数组中加一份
    pathList.push(path)
    // 将record信息存在path对应的值中
    pathMap[path] = record
  }

  // 判断当前的route是否有子路由,如果有children就代表是子路由
  if (route.children) {
    route.children.forEach(childRoute => {
      // 第四个参数如果不写,和其他路由没有区别,没有办法区分子路由和父路由,所以这里要将父路由的参数也加进去
      addRouteRecord(childRoute, pathList, pathMap, route)
    })
  }
}
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
  1. 在index.js中引入
import install from './install'
// 导入createMatcher模块
import createMatcher from './create-matcher'

export default class VueRouter {
  constructor (options) {
    this._routes = options.routes || []
    // 将路由规则进行映射
    this.matcher = createMatcher(this._routes)
  }

  init (app) {
  }
}

VueRouter.install = install
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

测试可以看到现在输出的样子:

image

目前这样还不是很方便,如何让他更好的找到子路由对应的组件和父路由对应的组件

# createMatcher -- match

根据路由地址,匹配一个路由数据对象 route { matched, path }

在createMatcher的match方法中进行修改

// 具体的createRoute函数在util/route里面实现,这里导入
import createRoute from './util/route'
...
// 匹配函数,根据path路径匹配路由信息的对象
function match (path) {
    const record = pathMap[path]
    if (record) {
      // 创建路由数据对象
      // route ==> { matched, path }  matched ==> [record1, record2]
      return createRoute(record, path)
    }
    return createRoute(null, path)
}
// 测试子路由地址
console.log(match('./music/pop'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# util/route.js -- createRoute

createRoute 根据路由地址,创建 route 路由规则对象

  • route --> { matched: [ musicRecord ], path: '/music' }
  • 如果是子路由的话,找到他的所有父路由对应的 record 插入到数组的第一项中
  • matched 数组中 -> [musicRecord, popRecord]

在util/route.js里面写createRoute函数

// createRoute方法定义两个参数:
// record是对应的信息,path是路由
export default function createRoute (record, path) {
  // 创建路由数据对象
  // route ==> { matched, path }  matched ==> [record1, record2]
  const matched = []
  // 进行循环,判断record是否有值
  while (record) {
    // 第一个record是子路由,matched里面第一个是父路由,所以不能用push方法,要用unshift方法
    matched.unshift(record)
    // 如果有父路由就赋值给record
    record = record.parentRecord
  }

  return {
    matched,
    path
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

再进行测试输出:

image

# History历史管理

  • hash 模式
  • html 5 模式
  • abstract 模式(暂不实现)

面向对象的思想,这三种模式都有一些共同的特性,所以抽象出来一个父类History

# History 父类

  • 属性
    • router()
    • current(记录当前路径对应的路由规则对象 {path:'/', matched: []})
  • 方法
    • transitionTo(path, onComplete):跳转到指定的路径,根据当前路径获取匹配的路由规则对象 route,然后更新视图
import createRoute from '../util/route'
export default class History {
  constructor (router) {
    this.router = router
    // 记录当前路径对应的 route 对象 { matched, path }
    // 一开始初始化null,path是/
    this.current = createRoute(null, '/')
  }
  
  // 跳转操作,传入一个path路由,要对当前路径进行赋值操作
  // 初始化的时候要传入一个onComplete回调函数,里面负责去注册路由改变的事件,onhashchange
  transitionTo (path, onComplete) {
    this.current = this.router.matcher.match(path)
    // 有的话就注册,只注册一次
    onComplete && onComplete()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# HashHistory

  • 继承 History
  • 确保首次访问地址为 #/
  • getCurrentLocation() 获取当前的路由地址(# 后面的部分)
  • setUpListener() 监听路由地址改变的事件
import History from './base'
// 继承父类
export default class HashHistory extends History {
  constructor (router) {
    // 调用父类的构造函数
    super(router)
    // 保证首次访问的时候 #/
    ensureSlash()
  }
  // 获取当前的路径,获取hash,把前面的#去掉
  getCurrentLocation () {
    return window.location.hash.slice(1)
  }
  // 监听路由变化之后跳转到当前路径
  setUpListener () {
    window.addEventListener('hashchange', () => {
      this.transitionTo(this.getCurrentLocation())
    })
  }
}
// 如果初始的时候hash没有值就加一个/,地址变成了 localhost:8080/#/
function ensureSlash () {
  if (window.location.hash) {
    return
  }
  window.location.hash = '/'
}

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
  • 在VueRouter 构造函数中初始化 history,根据创建 VueRouter 传来的 mode 决定使用哪个 History 对象
  • 我们在首次加载页面的时候要初始化,init里面对一开始打开页面监听事件的方式进行了注册。
...
// 引用历史管理
import HashHistory from './history/hash'
import HTML5History from './history/html5'

export default class VueRouter {
  constructor (options) {
    this._routes = options.routes || []
    this.matcher = createMatcher(this._routes)
    // 添加一个mode属性,判断参数中有没有mode,么有默认是hash
    const mode = this.mode = options.mode || 'hash'
    // 通过mode的值加载对应的历史管理
    switch (mode) {
      case 'hash':
        this.history = new HashHistory(this)
        break
      case 'history':
        this.history = new HTML5History(this)
        break
      default:
        throw new Error('mode error')
    }
  }

  init (app) {
    // app 是 Vue 的实例
    const history = this.history
    // 这个setUpListener里面使用对象调用方法的方式将this从window指向history
    const setUpListener = () => {
      history.setUpListener()
    }
    // 一开始就打开当前页面地址
    // transitionTo是父类方法,getCurrentLocation是子类方法
    history.transitionTo(
      history.getCurrentLocation(),
      // 这里原本是 history.setUpListener() 这样使用,但是因为在base中直接调用this指向window,为了解决这个问题使用了另一种
      // 所以在上面定义了一个setUpListener
      setUpListener
    )
  }
}

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

# 给 router 对象设置响应式的 _route 属性

上面的代码写完,跳转的时候路由是变化了,current也变化了,但是视图没有发生变化,因为current不是响应式数据,而且current是History的属性,并不是Vue的。

  1. 在install.js中将 _router 属性变成响应式数据
...
export default function install (Vue) {
  ...
  _Vue.mixin({
    beforeCreate () {
      ...
      if (this.$options.router) {
        ...
        this._router.init(this)
        // 给 router 对象设置响应式的 _route 属性
        // this是vue的实例,给vue的实例定义了一个_route,_route是私有属性,所以是下划线开头,外部访问用$toute
        // 这个_route的值是this._router.history.current,如果currnet改变就要重新给_route赋值,_route是响应式的,所以视图也会跟着变。
        // Vue.util这个属性官方说轻易不要用,出问题自己承担责任
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        ...
      }
    }
  })
  ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. 上一步注册了一个_route的响应式属性,将current的值赋值。下一步要在current改变的时候,重新赋值给_route才能确保视图更新,所以在base.js中修改History
import createRoute from '../util/route'
export default class History {
  constructor (router) {
    ...
    this.current = createRoute(null, '/')
    // 初始化cb属性,这个属性的作用是在this.current变化之后将改变的值传出去
    this.cb = null
  }

  // 定义一个listen函数,给cb赋值
  // 在 transitionTo 中调用,触发回调,给 _route 赋值
  listen (cb) {
    this.cb = cb
  }

  transitionTo (path, onComplete) {
    this.current = this.router.matcher.match(path)
    // 确保cb有值并且将this.current传入
    // 调用 listen 中设置的回调,并且把 最新的 current 传递给 cb
    // cb 中把当前的 current 赋值给 app._route 响应式数据发生变化,更新视图
    this.cb && this.cb(this.current)
    onComplete && onComplete()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. 在index.js中调用listen
...

export default class VueRouter {
  constructor (options) {
    ...
  }

  init (app) {
    // app 是 Vue 的实例
    const history = this.history

    // 因为history.transitionTo要依赖这个listen方法,所以这个在前面调用
    // 接收History类中的this.current传给Vue的实例的_route属性,更新视图
    history.listen(current => {
      app._route = current
    })

    const setUpListener = () => {
      history.setUpListener()
    }
    
    history.transitionTo(
      history.getCurrentLocation(),
      setUpListener
    )
  }
}

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

这样,初始化的时候this.current就传给了_route(下划线的是内部属性,外部属性前面加$),当this.current变化的时候调用cb也就是listen的回调函数重新将current赋值给_route,加载组件也就跟着变化了。

# $route/$router的处理

install.js 中

...
export default function install (Vue) {
  ...
  _Vue.mixin({
    ...
  })
  _Vue.component(Link.name, Link)
  _Vue.component(View.name, View)

  // 给Vue的实例注册$router和$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
8
9
10
11
12
13
14
15
16
17
18
19

# router-view--渲染当前组件

  • 获取当前组件的 $route 路由规则对象
  • 找到里面的 matched 匹配的 record (里面有 component)
  • 如果是 /music 的话,matched 匹配到一个,直接渲染对应的组件
  • 如果是 /music/pop 的话,matched 匹配到两个 record(第一个是父组件,第二个是子组件)

# 一级路由的加载

export default {
  name: 'RouterView',
  render (h) {
    // 先实现一级路由的加载
    const route = this.$route
    // 获取到里面matched对应的组件
    const record = route.matched[0]
    if (record) {
      return h(record.component)
    }
    return h()
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

完成这一步,那首页和about的组件加载就没有问题了。

# 实现子路由的加载

export default {
  name: 'RouterView',
  render (h) {
    const route = this.$route
    // 默认数组访问的深度为0 route.matched[0]
    let depth = 0
    // 下面判断用
    this.routerView = true
    // 父组件
    let parent = this.$parent
    // 如果是子路由,那么组件里面存在父子组件关系,一层一层找,总能找到最顶层的router-view
    // 如果有父组件parent说明当前是子组件,每一个组件的routerView都是true,如果是true就说明是router-view,深度就++
    // 直到找到最顶层的router-view停止
    while (parent) {
      if (parent.routerView) {
        depth++
      }
      parent = parent.$parent
    }
    // /music/pop
    // matched ---> [musicRecord, popRecord]
    // 获取到里面matched对应的组件,这里把数组的个数对应组件深度
    const record = route.matched[depth]
    if (record) {
      return h(record.component)
    }
    return h()
  }
}

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

这样子组件也很好的展示出来了。

# 模拟导航守卫(钩子函数)

  • router 中增加属性记录所有的钩子函数
  1. 在index.js中增加钩子函数
...
export default class VueRouter {
  constructor (options) {
    ...
    this.matcher = createMatcher(this._routes)
    // 增加一个属性,记录所有 beforeEach 注册的钩子函数
    this.beforeHooks = []
    ...
  }

  // 定义 beforeEach 方法
  beforeEach (fn) {
    this.beforeHooks.push(fn)
  }

  ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 在base.js中,在跳转之前执行钩子函数
...
export default class History {
  constructor (router) {
    ...
  }

  // 跳转操作
  transitionTo (path, onComplete) {
    // 将current存在一个变量里
    const current = this.router.matcher.match(path)
    // 在页面跳转之前(重新渲染之前),将this.current先传出去
    // 执行钩子函数,传入 to 和 from
    this.router.beforeHooks.forEach(hook => {
      hook(current, this.current)
    })
    // 改变当前的current
    this.current = current
    ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  1. 在router/index.js初始化VueRouter的时候定义钩子函数
// 路由对象
const router = new VueRouter({
  routes
})

// 定义钩子函数
router.beforeEach((to, from) => {
  console.log('beforeEach', to, from)
})
1
2
3
4
5
6
7
8
9

在跳转之前,可以看到控制台有属性记录输出

更新时间: 2022-01-11 10:46