Vue-Router 核心原理

395 阅读13分钟

本系列主要讲述 Vue 里面的一些核心源码的实现原理,这章节主要讲述 Vue-Router 内部是如何实现和工作的。

未命名文件 (3).png 上图为 Vue-Router 核心的全部知识点,上到下、右到左逐步深入剖析 Vue-Router 内部的核心原理。

路由

路由:通过互联的网络把信息从源地址传输到目的的地址的活动。路由发生在 OSI 网络参考模型中的第三层即网络层。路由引导分组传送,经过一些中间的节点后,到它们最后的目的地。作成硬件的话,则称之为路由器。路由通常根据路由表 --- 一个存储到各个目的地的最佳路径的表 --- 来引导分组传送。

上面的定义很官方,我们可以从中抽取出一些重点来:

  • 路由是一种活动,负责将信息从源地址传输到目的地址
  • 要完成这样一个活动需要一个很重要的东西 路由表---源地址和目的地址的映射表。

前端路由

在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI(组件) 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新,无需页面的刷新。
简单点来说就是:输入 URL -> JavaScript解析地址 -> 找到对应的地址页面 -> 执行页面产生的 JavaScript -> 看到页面

后端路由

用户输入一个 URL,浏览器传递给服务端,服务端匹配映射表,找到对应的处理程序,返回对应的资源(页面或者其他)。
简单点来说就是:输入 URL -> 请求发送到服务器 -> 服务器解析请求的路径 -> 拿到对应的页面信息 -> 返回给客户端

前端路由的两个核心

如何做到当 URL 发生变化的时候却不会引起页面的刷新?如何检测到了 URL 地址发生了改变?下面通过前端路由的两个核心 hashhistory 分别来实现。

hash 实现

hash 是 URL 中的 (#)号以及后面的部分,通常作锚点在页面内进行导航,改变 URL 中的 hash 却不会引起页面的刷新。 可以通过 hashchange 事件来监听 URL 地址的变化,改变 URL 的方式只有一下几种方式:

  • 通过浏览器的前进后退改变 URL。
  • 通过 <a> 标签来改变 URL。
  • 通过 window.loaction 改变 URL。 可以通过 loaction.hash 来获取到 hash 值。

history 实现

history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新

history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:

  • 通过浏览器前进后退改变 URL 时会触发 popstate 事件
  • 通过 pushState/replaceState 或<a>标签改变 URL 不会触发 popstate 事件。
  • 可以拦截 pushState/replaceState 的调用和<a>标签的点击事件来检测 URL 变化
  • 通过 JavaScript 调用 history 的 back,go,forward 方法可以触发该事件

可以通过 loaction.pathname 拿到 history 值

分析 VueRouter 内部的本质

// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/home'
import Index from '@/components/index'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/index',
      name: 'index',
      component: Index
    }
  ]
})

从 router/index.js中可以看出,Router 其实就是一个 class 类,向外抛出一个 Router 实例,然后再将 Router 实例写入 new Vue({})配置中。所以我们可以初步的确定 Rouetr 是一个 class 类

import install from './install'
class MyVueRouter {
    constructor(){
    
    }
}
MyVueRouter.install = install

我们还是用了 Vue.use()方法来注册 Router 插件

Vue.use 方法

import { toArray } from '../util/index'

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)
    // 将vue实例作为第一个参数传入到args数组中,保证第一个参数是vue 其余是传入的参数
    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
  }
}

Vue.use()方法执行的原则:
如果传入的参数是一个对象,并且有 install 方法,就会执行参数中的 install 方法。
如果传入的参数是一个函数,则会执行参数本身。
Vue.use() 方法的作用:

  • 插件只能被注册依次,保证插件列表中不能有重复的插件。
  • 插件的类型,可以是 install 方法,也可以是一个包含 install 方法的对象。

install 方法

export let Vue
export default function install (_Vue) {
  // 获取第一个实例参数vue
  Vue = _Vue
  // 给全部的组件添加一个 router属性
  Vue.mixin({
    beforeCreate () { // 深度优先
      // 第一种写法
      if (this.$options.router) { // 如果是根组件
        this._root = this // 将当前实例挂载到根组件上
        // 父组件挂载 router
        this._router = this.$options.router
        // 初始化路由init this根实例
        this._router.init(this)
      } else {
        this._root = this.$parent && this.$parent._root
        // 子组件也要挂载上 router
        this._router = this._root._router
      }
      // 第二种写法可以写在原型链上
    }
  })
}

createMatcher 方法

在这里我们需要将MyVueRouter中传入的 routes 数据进行扁平化,也就是说将其转化为

  ['/','/about','/about/a','/about/b']
  {'/': '记录','/about': '记录','/about/a': '记录',}
import install from './install'
import createMatcher from './create-matcher'
class MyVueRouter {
    constructor(options){
        // 创建匹配器 这个方法返回两个方法 match和addRouter

        // match 负责匹配路径 {'/': '记录','/about': '记录','/about/a': '记录',}
        // addRoutes 动态添加路由配置
        this.matcher = createMatcher(options.routes || [])
    }
}
MyVueRouter.install = install
import createRouterMap from './createRouterMap'
export default function createMatcher (routes) {
  // routes 用户当前传入的配置
  // 核心 扁平化用户传入的routes 创建路由映射表
  //   ['/','/about','/about/a','/about/b']
  //   {'/': '记录','/about': '记录','/about/a': '记录',}
  // 这个方法的作用就是返回一个pathList 数组和 pathMap路由映射表
  const {pathList, pathMap} = createRouterMap(routes) // 初始化配置
  // 动态添加路由信息方法
  function addRouter (routes) { // 添加新的配置
    createRouterMap(routes, pathList, pathMap)
  }
  // 用来匹配的方法
  function match () {}
  return {
    match,
    addRouter
  }
}

更方便我们进行数据的处理,当用户访问 URL 的时候,我们可以直接拿到对应的组件进行展示。
createMatcher 方法主要用来 匹配映射表 动态添加路由扁平化数据。对外返回 match 匹配方法、addRouter 动态添加路由方法

createRouterMap 方法

export default function createRouterMap (routes, oldPathList, oldPathMap) {
  // 将用换传入的routes进行格式化
  const pathList = oldPathList || []
  const pathMap = oldPathMap || {}
  // 循环遍历
  routes.forEach(route => {
    // 存放每一个routes的记录
    addRouteRecord(route, pathList, pathMap)
  })
  return {
    pathList,
    pathMap
  }
}
function addRouteRecord (route, pathList, pathMap, parent) {
  const path = parent ? `${parent.path}/${route.path}` : route.path
  // 判断映射表中是否存在路径
  const record = { // 记录
    path,
    component: route.component,
    parent
  }
  if (!pathMap[path]) {
    // 将路径添加到pathList数组和映射表中
    pathList.push(path)
    pathMap[path] = record
  }
  // 递归遍历是否有children属性
  if (route.children) {
    route.children.forEach(child => {
      addRouteRecord(child, pathList, pathMap, record)
    })
  }
}

扁平化后的数据展示:

image.png createRouteMap 方法作用:

  • 通过上面的 createMatcher 方法可以知道这里的 createRouteMap 其实有两个作用,第一就是初始化配置,第二就是可以动态添加路由信息。所以该方法有三个参数。
  • 当初始化配置的时候,此时的 pathListpathMap 均取默认值为空。
  • 需要循环每一个 routes 信息,addRouteRecord 函数主要用来记录并且记录在 pathListpathMap 中。
  • 判断存放的 path 是否已经存在了 pathMap 中,如果没有,则需要将 path 存储在 pathListpathMap 中。
  • 判断是否存在children多层嵌套路由,如果存在,则需要递归 addRouteRecord 函数,这里需要注意:需要将 parent 传入,才能知道是属于哪个父组件的子组件,并将其正确的拼接存放到 pathListpathMap
  • 最后将 pathListpathMap 返回

为什么要在beforeCreate进行混入?因为尽可能的减少对组件其他属性的影响,所以尽早混入最好。
父子组件生命周期的执行顺序:
父beforeCreate -> 父create -> 父beforeMounte -> 子beforeCreate -> 子created -> 子beforeMounte -> 子mounted -> 父mounted

HashHistory类、History类

根据不同的模式来创建不同的路由对象,主要有 hash模式 和 history 模式,这里就只讲解hash模式。
源码中需要声明三个类,基类、HashHistory类、H5History类。基类主要存放的是共同的方法。本章节只讨论hash模式,所以这里需要声明两个类,一个是基类 History,另外一个是 HashHistory 类。当使用相同的方法的时候只需要继承基类就可以了。

export default class History {
  constructor (router) { // router = new MyRouter
    this.router = router
  }
}

HashHistory类 直接继承上面的基类 History类。

import History from './base'
export default class HashHistory extends History {
  constructor (router) {
    super(router)
  }
}

上面创建完两个类之后,我们开始根据不同的模式来创建不同的路由对象,this.mode 表示当前模式是 hash 还是 history,默认是 hash 模式new HashHistory 并且需要将路由实例对象作为参数传入,方便我们后续获取路由 router 信息。

import install from './install'
import createMatcher from './create-matcher'
import HashHistory from './history/hash'
class MyVueRouter {
    constructor(options){
        // 创建匹配器 这个方法返回两个方法 match和addRouter

        // match 负责匹配路径 {'/': '记录','/about': '记录','/about/a': '记录',}
        // addRoutes 动态添加路由配置
        this.matcher = createMatcher(options.routes || [])
        // 根据不同的路由模式创建不同的路由对象
        this.mode = options.mode || 'hash'
        this.history = new HashHistory(this) // this 表示当前路由实例
    }
    init(app) { // app表示根实例 
        // 初始化
    }
}
MyVueRouter.install = install

经过上面创建好类之后,前期的准备工作就已经做好了,我们现在开始要初始化 init 这个核心的方法。那么我们应该如何初始化呢?就好比如当前路径为 /index/a ,我们应该要先根据当前路径,找到到指定的组件中,才可以进行对应的组件展示。所以就需要添加一个 transitionTo 方法,但是问题来了,我们应该添加到基类 History 上还是添加到 HashHistory 上呢?我们不妨想一下,这个 transitionTo 是每一个模式都要进行跳转的,所以要将 transitionTo 方法添加到基类 History 中,因为HashHistory继承了基类,所以也会拿到 transitionTo 方法。

transitionTo 方法

这个方法的作用的就是:过渡,根据当前的路径过渡到某个路径去,并且进行路由信息映射表的匹配。

    基类History
export default class History {
  constructor (router) {
    this.router = router
  }
  transitionTo (location) {
    // loaction 跳转的路由地址
  }
}
//     MyVueRouter
import install from './install'
import createMatcher from './create-matcher'
import HashHistory from './history/hash'
class MyVueRouter {
    constructor(options){
        // 创建匹配器 这个方法返回两个方法 match和addRouter

        // match 负责匹配路径 {'/': '记录','/about': '记录','/about/a': '记录',}
        // addRoutes 动态添加路由配置
        this.matcher = createMatcher(options.routes || [])
        // 根据不同的路由模式创建不同的路由对象
        this.mode = options.mode || 'hash'
        this.history = new HashHistory(this) // this 表示当前路由实例
    }
    init(app) { // app表示根实例 
        // 初始化
        const history = this.history
        history.transitionTo(history.getCurrentLocation())
    }
}
MyVueRouter.install = install
// HashHistory类
import History from './base'
function getHash () {
  return window.location.hash.slice(1)
}
export default class HashHistory extends History {
  constructor (router) {
    super(router)
  }
  getCurrentLocation () {
    return getHash()
  }
}

经过上面的分析,我们已经拿到了当前路由的hash值,但是,我们还需要进行进一步的监听后续路由地址的变化,所以还需要在 transitionTo方法 中添加第二个参数,跳转之后继续监听路由信息变化的回调函数。

//     基类History
export default class History {
  constructor (router) {
    this.router = router
  }
  // 跳转的核心逻辑 location代表跳转的目的地址 onComplete代表当前跳转成功之后执行的方法
  transitionTo (location, onComplete) {
    // loaction 跳转的路由地址
    // 如果存在回调就自动执行回调函数
    onComplete && onComplete()
  }
}
// MyVueRouter
import install from './install'
import createMatcher from './create-matcher'
import HashHistory from './history/hash'
class MyVueRouter {
    constructor(options){
        // 创建匹配器 这个方法返回两个方法 match和addRouter

        // match 负责匹配路径 {'/': '记录','/about': '记录','/about/a': '记录',}
        // addRoutes 动态添加路由配置
        this.matcher = createMatcher(options.routes || [])
        // 根据不同的路由模式创建不同的路由对象
        this.mode = options.mode || 'hash'
        this.history = new HashHistory(this) // this 表示当前路由实例
    }
    init(app) { // app表示根实例 
        // 初始化
        const history = this.history
        const setRouterLister = () => {
          return history.setRouterListener()
        }
        history.transitionTo(history.getCurrentLocation(), setRouterLister())
    }
}
MyVueRouter.install = install
// HashHistory类
import History from './base'
function getHash () {
  return window.location.hash.slice(1)
}
export default class HashHistory extends History {
  constructor (router) {
    super(router)
  }
  getCurrentLocation () {
    return getHash()
  }
  // 新增代码
  setRouterListener () {
    window.addEventListener('hashchange', () => {
      // 进行路由跳转 当路由地址发生变化的时候再进行路由的跳转
      this.transitionTo(getHash())
    })
  }
}

这里我们使用前面所说到的监听 hash 值的变化的函数 hashchange,当路由地址 hash 值发生变化的时候再调用 transitionTo方法 来进行路由的跳转。

通过 transitionTo 方法,将拿到的路由信息去路由映射表中进行匹配,并且给当前路由一个默认值,后续路由变化会更改这个默认值,所以这个路径不能够写死,需要用一个方法进行包裹来实时的更新信息。

// History类
export function createRoute (record, location) {
  // 这个方法返回两个信息 一个是路由地址 一个匹配到的路由包含父组件
  const res = []
  if (record) { // {path: /about/a,component:xxx,parent}
    while (record) {
      // 这里就是在不停的查找路由中的父组件路由
      res.unshift(record)
      record = record.parent
    }
  }
  return {
    ...location,
    matched: res
  }
}
export default class History {
  constructor (router) {
    this.router = router
    // 默认路由的当前路径 当地址改变的时候会更改路由信息
    this.current = createRoute(null, {
      path: '/',
    })
  }
transitionTo (location, onComplete) {
    // loaction 跳转的路由地址
    // 进行路由匹配 去路由映射表中找到匹配的路由
    // {/:record,/index:record....}
    const route = this.router.match(location)
    console.log(route)
    // 如果传入第二个参数 有就执行这个回调函数
    onComplete && onComplete()
  }
}

在基类 History 中调用了 router 上的 match 方法,但是 MyVueRouter 中并没有这个方法,我们可以定义一个 match 方法,前面我们在 MyVouRouter 中定义了 this.matcher 方法返回的 match 函数,这里我们就刚好的对接上了之前所写的代码。

// MyVueRouter
...
// 进行路由匹配
match (location) {
    return this.matcher.match(location)
  }
...

这时我们又返回到了 createMatcher函数进行路由信息的匹配

// createMatcher
import { createRoute } from './history/base'
// 用来匹配的方法
  function match (location) {
    // 需要找到对应的记录 并且要根据记录产生一个匹配数组
    const path = {
      path: location
    }
    const record = pathMap[location]
    if (record) { // 找到了记录
      return createRoute(record, path)
    }
    // 没有找到记录
    return createRoute(null, path)
  }

image.png

完善 transitionTo 方法

当我们从映射表中匹配到了路由信息之后,我们需要更新 this.current

// History
...
constructor (router) {
    this.router = router
    // 默认路由的当前路径 当地址改变的时候会更改路由信息
    this.current = createRoute(null, {
      path: '/'
    })
  }
// 跳转的核心逻辑 location代表跳转的目的地址 onComplete代表当前跳转成功之后执行的方法
  transitionTo (location, onComplete) {
    // loaction 跳转的路由地址
    // 进行路由匹配 去路由映射表中找到匹配的路由
    // {/:record,/index:record....}
    const route = this.router.match(location)
    console.log(route)
    // /index/a => {path:'/index/a',matched:[/index,/index/a]}
    // 当路由地址发生变化的时候更新current 避免用户点击同一个路由重新渲染
    if (this.current.path === location && route.matched.length === this.current.matched.length) {
      // 路由信息相同不进行跳转
      return
    }
    // 更新
    this.updateRoute(route)
    // 如果传入第二个参数 有就执行这个回调函数
    onComplete && onComplete()
  }
  updateRoute (route) {
    this.current = route
  }
...

我们已经完成了 this.current 的更新,但是这是一个普通的数据,这个属性根本不会影响视图的更新,我们需要的是路径发生了变化,视图更新,这才是 Vue 的核心。所以我们需要将 this.current 设置为响应式。

响应式的实现

// install.js
export let Vue
export default function install (_Vue) {
  // 获取第一个实例参数vue
  Vue = _Vue
  // 给全部的组件添加一个 router属性
  Vue.mixin({
    beforeCreate () { // 深度优先
      // 第一种写法
      if (this.$options.router) { // 如果是根组件
        this._root = this // 将当前实例挂载到根组件上
        // 父组件挂载 router
        this._router = this.$options.router
        // 初始化路由init this根实例
        this._router.init(this)
        // 响应式数据变化,只要_route变化 就会更新视图
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._root = this.$parent && this.$parent._root
        // 子组件也要挂载上 router
        this._router = this._root._router
      }
      // 第二种写法可以写在原型链上
    }
  })
  Object.defineProperty(Vue.prototype, '$route', { // 通过原型链可以拿到$route
    get () {
      // current 里面存放着 path matched
      return this._root._route
    }
  })
  Object.defineProperty(Vue.prototype, '$router', {
    get () {
      return this._root._router
    }
  })
}

这里的 $router$route 就相当于 this._root._router、``this._root._route,并且将它们挂载到 Vue 原型上,方便每一个组件访问。 我们将_route设置成了响应式,需要在this.current更新的同时也需要将_route进行更新。

// MyVueRouter
...
init (app) { // app是根实例 初始化路由信息
    // 如何初始化 先根据当前路径 显示对应的组件
    const history = this.history
    const setRouterLister = () => {
      return history.setRouterListener()
    }
    // transitionTo 跳转到哪里
    history.transitionTo(history.getCurrentRouter(), setRouterLister()) // 跳转之后继续监听路由信息的变化
    // 因为设置响应式的是_route 所以要时刻监听_route的变化 当current发生变化的时候在触发_route 相当于一个发布订阅模式
    history.listener(route => {
      app._route = route // 视图更新
    })
  }
  ...

完善 History 类

export function createRoute (record, location) {
  // 这个方法返回两个信息 一个是路由地址 一个匹配到的路由包含父组件
  const res = []
  if (record) { // {path: /about/a,component:xxx,parent}
    while (record) {
      // 这里就是在不停的查找路由中的父组件路由
      res.unshift(record)
      record = record.parent
    }
  }
  return {
    ...location,
    matched: res
  }
}
export default class History {
  constructor (router) {
    this.router = router
    // 默认路由的当前路径 当地址改变的时候会更改路由信息
    this.current = createRoute(null, {
      path: '/'
    })
  }
  // 跳转的核心逻辑 location代表跳转的目的地址 onComplete代表当前跳转成功之后执行的方法
  transitionTo (location, onComplete) {
    // loaction 跳转的路由地址
    // 进行路由匹配 去路由映射表中找到匹配的路由
    // {/:record,/index:record....}
    const route = this.router.match(location)
    console.log(route)
    // /index/a => {path:'/index/a',matched:[/index,/index/a]}
    // 当路由地址发生变化的时候更新current
    if (this.current.path === location && route.matched.length === this.current.matched.length) {
      // 路由信息相同不进行跳转
      return
    }
    // 更新
    this.updateRoute(route)
    // 如果传入第二个参数 有就执行这个回调函数
    onComplete && onComplete()
  }
  updateRoute (route) {
    this.current = route
    this.callback && this.callback(route) // 路径变化会将最新的路径传递给listener方法
  }
  listener (callback) {
    this.callback = callback
  }
}

完善 HashHistory 类

image.png

image.png
完成上面的所有操作之后,这里还有一个小bug,就是当我们打开页面的时候,并不会自动跳转到根/下,所以找不到记录,为空。我们需要解决这个问题,在 HashHistory 类constructor 中给 path 默认加上/。

import History from './base'
function getHash () {
  return window.location.hash.slice(1)
}
function ensureSlash () {
  if (window.location.hash) {
    return
  }
  window.location.hash = '/'
}
export default class HashHistory extends History {
  constructor (router) {
    super(router)
    // path路径默认添加上 /
    ensureSlash()
  }
  getCurrentRouter () {
    return getHash()
  }
  setRouterListener () {
    window.addEventListener('hashchange', () => {
      // 进行路由跳转
      this.transitionTo(getHash())
    })
  }
}

完善 MyVueRouter

import createMatcher from './create-matcher'
import HashHistory from './history/hash'
import install from './install'
class MyVueRouter {
  constructor (options) {
    // 创建匹配器

    // match 负责匹配路径 {'/': '记录','/about': '记录','/about/a': '记录',}
    // addRoutes 动态添加路由配置
    this.matcher = createMatcher(options.routes || [])
    // 创建路由系统
    this.mode = options.mode
    this.history = new HashHistory(this)
  }
  init (app) { // app是根实例 初始化路由信息
    // 如何初始化 先根据当前路径 显示对应的组件
    const history = this.history
    const setRouterLister = () => {
      return history.setRouterListener()
    }
    // transitionTo 跳转到哪里
    history.transitionTo(history.getCurrentRouter(), setRouterLister()) // 跳转之后继续监听路由信息的变化
    // 因为设置响应式的是_route 所以要时刻监听_route的变化 当current发生变化的时候在触发_route 相当于一个发布订阅模式
    history.listener(route => {
      app._route = route // 视图更新
    })
  }
  match (location) {
    return this.matcher.match(location)
  }
}
MyVueRouter.install = install

export default MyVueRouter

完成了上面一系列的操作之后,终于到了最后展示组件的环节,在router/index.js引入我们自己定义好的 MyVueRouter,执行发现,install 方法中缺少两个组件,分别是 router-link 和 router-view。
image.png

函数式组件

之前创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。
因为函数式组件只是函数,所以渲染开销也低很多。
组件需要的一切都是通过 context 参数传递,它是一个包括如下字段的对象:

  • props:提供所有 prop 的对象
  • children:VNode 子节点的数组
  • slots:一个函数,返回了包含所有插槽的对象
  • scopedSlots:一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
  • data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
  • parent:对父组件的引用
  • listeners:一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
  • injections: 如果使用了 inject 选项,则该对象包含了应当被注入的 property。

所以,我们需要在 install 方法中定义 router-link、router-view 全局的组件。 以函数式组件的方法创建 router-view.js

// router-view.js
export default {
  functional: true,
  render (h, {parent, data}) {
    const route = parent.$route
    const matched = route.matched
    data.routerView = true // 当前组件是一个router-view
    let depth = 0 // 索引
    while (parent) {
    // 如果存在父组件 且父组件中有router-view节点
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      parent = parent.$parent
    }
    const record = matched[depth]
    if (!record) {
      return h() // 找不到记录 渲染空
    }
    const component = record.component
    return h(component, data)
  }
}

以上我们声明了两个全局组件,但是此时的a标签还不能被点击,是因为我们没有给这个a标签添加一个 href 属性,所以我们需要给a标签添加一个 href 属性,通过观察我们发现,在router-link组件上一个 to 属性,这属性正是我们经常所用到的组件传值方式,所以我们可以使用 props 属性接收到 router-link 组件传递过来的值,然后再将 to 属性上面的值拼接到 href 中,现在我们正式的完成了将 router-link 转换成了一个a标签。

// router-link.js
export default {
  functionnal: true,
  props: {
    to: {
      type: String,
      required: true
    }
  },
  // 渲染
  render (h) {
  // 使用render的h函数渲染
    return h(
      // 标签名
      'a',
      // 标签属性
      {
        attrs: {
          href: '#' + this.to
        }
      },
      // 插槽内容
      this.$slots.default
    )
  }
}

经过上面代码的修改,我们完成了a标签内容的动态展示。this.$slots 是获取全部的插槽。this.$slots.defalut获取默认插槽,this.$slots.name 可以拿到具体的插槽内容。

完善 install 方法

import RouterView from './router-view'
import RouterLink from './router-link'
export let Vue
export default function install (_Vue) {
  // 获取第一个实例参数vue
  Vue = _Vue
  // 给全部的组件添加一个 router属性
  Vue.mixin({
    beforeCreate () { // 深度优先
      // 第一种写法
      if (this.$options.router) { // 如果是根组件
        this._root = this // 将当前实例挂载到根组件上
        // 父组件挂载 router
        this._router = this.$options.router
        // 初始化路由init this根实例
        this._router.init(this)
        // 响应式数据变化,只要_route变化 就会更新视图
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._root = this.$parent && this.$parent._root
        // 子组件也要挂载上 router
        this._router = this._root._router
      }
      // 第二种写法可以写在原型链上
    }
  })
  Object.defineProperty(Vue.prototype, '$route', { // 通过原型链可以拿到$route
    get () {
      // current 里面存放着 path matched
      return this._root._route
    }
  })
  Object.defineProperty(Vue.prototype, '$router', {
    get () {
      return this._root._router
    }
  })
  Vue.component('router-link', RouterLink)
  Vue.component('router-view', RouterView)
}

以上我们已经完成了所有关于Vue-Router核心原理的全部过程,效果如下: image.png 需要源码的话可以去gitee获取。戳我获取源码!!!