VueRouter部分功能 手把手带你写

70 阅读15分钟

1. vue.use方法源码

在这一步,我们先来看看Vue.use()方法是干嘛用的,这对于手写VueRouter至关重要

路径:node_modules/vue/src/core/global-api/use.ts

import type { GlobalAPI } from 'types/global-api'
import { toArray, isFunction } from '../util/index'export function initUse(Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | any) {
    const installedPlugins =
      this._installedPlugins || (this._installedPlugins = [])
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
​
    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (isFunction(plugin.install)) {
      plugin.install.apply(plugin, args)
    } else if (isFunction(plugin)) {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

1. Vue.use干嘛用的?

一般是用来注册 第三方的库用的,里面可以接受函数、对象。如果接受函数就直接调用,如果接收的是对象,就会去里面找install方法,再去调用他

2. 判断插件是否注册

​
……上面也暂时删除
export function initUse(Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | any) {
    const installedPlugins =
      this._installedPlugins || (this._installedPlugins = [])
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
  ……中间部分暂时删去
    installedPlugins.push(plugin)
    return this
  }
}
  1. use方法,接受参数plugin,值可以是函数也可以是任意类型。
  2. 里面的installedPlugins是一个数组,是库的集合
  3. 判断要注册的这个库,是否已经存在了,利用indexOf方法进行判断,如果已经存在了,就直接return this,this在Vue.use方法里面,所以this指向的是Vue实例
  4. 如果这个库不存在,就插入到installedPlugins这个数组里面去

3. use如何处理传递进来的参数

import type { GlobalAPI } from 'types/global-api'
import { toArray, isFunction } from '../util/index'export function initUse(Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | any) {
    const installedPlugins =
      this._installedPlugins || (this._installedPlugins = [])
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
​
    // additional parameters
+    const args = toArray(arguments, 1)
+    args.unshift(this)
+    if (isFunction(plugin.install)) {
+      plugin.install.apply(plugin, args)
+    } else if (isFunction(plugin)) {
+      plugin.apply(null, args)
+    }
    installedPlugins.push(plugin)
    return this
  }
}
  1. 把传递进来的参数转化为数组,去掉第一个参数
  2. 往开头又插入一个参数,就是this,就是Vue实例。这一点很重要,因为创建VueRouter类的时候,第一个参数就是Vue实例,我们可以挂载$router对象,而Vue实例就是这里传递进来的。
  3. 判断plugin.install是不是函数,如果是,就利用apply方法来进行调用,this指向plugin
  4. 如果不是函数,就直接调用plugin

2. 小tips

看源码,一定要抓住核心,不然就会迷失

vue-router文件

/src/index.js文件

里面有hash和html5,表示两种路由模式

base.js表示公共语法

abstract.js后续才会用到

3. VueRouter类实现

第一,用vue/cli脚手架创建一个项目

vue create router-demo

第二,修改里面的router/index.js里面的一句代码

// 1. 引入vue
import Vue from 'vue'
// 2. 引入 vue-router
// import Router from 'vue-router'
+import Router from '../my-router/index'
// 3. Vue注册路由实例
Vue.use(Router)

因此,也需要创建和router文件夹并列的文件夹 my-router/index.js

里面写我们写下来的代码

第三步,需要如下文件,如图,依次创建

image-20220819161631194

第四步,在写代码时,可以时不时去瞄一下,官方的vue-router实例,里面的src文件夹下的代码是怎么写的,看不懂也没关系的啦,不用死扣

image-20220819161640369

第五步,my-router/index.js文件,创建一个VueRouter类

// 1. 类
class VueRouter {
    constructor(options) {
        // 2. 接受options.routes 里面是路由的规则
      
    }
    // 3. init方法 传入Vue 实例 传入路由变量的实例 后面能够进行方法的调用
    init(Vue) {
        console.log(Vue);
    }
}

上面的options是干嘛用的呢?

我们在router/index.js里面,new Vue实例时,就会传入一个对象, 就给了上面的options

image-20220819103834442

第六步,我们给VueRouter类添加

// 1. VueRouter类
class VueRouter {
    constructor(options) {
        // 2. 接受options.routes 里面是路由的规则 我们把routes这个数字里面的路由规则给到this._routes私有变量
+       this._routes = options.routes || []
    }
    // 3. init方法 传入Vue 实例 传入路由变量的实例
    init(Vue) {
        console.log(Vue);
    }
}

4. install方法的使用

第一步,在my-router/install.js文件里面创建一个全局变量,导出一个install方法。

导出一个_Vue变量,赋值为Vue,Vue是Vue.use(VueRouter)时,会调用install方法,然后传递进来的 Vue实例

// 4.导出Vue 让在其他文件也可以使用Vue实例
export let _Vue
​
// 5. 导出install方法
export default function install(Vue) {
    // 5.1 获取Vue的实例
    _Vue = Vue
}

第二步,我们调用Vue.mixin({})方法,里面使用beforeCreate方法

// 4.导出Vue 让在其他文件也可以使用Vue实例
export let _Vue
​
// 5. 导出install方法
export default function install(Vue) {
    // 5.1 获取Vue的实例
    _Vue = Vue
    // 5.2 进行vue实例的混入   
+    _Vue.mixin({
        // 混入后 所有的vue实例的beforeCreate()方法都会执行如下代码
+        beforeCreate() {
            
​
        }
    })
​
}

第三步,增加逻辑判断,是不是new Vue实例呢?如果是的话 就能够获取到$router属性

我们做了几件事情,

  1. 挂载this,
  2. router赋值给一个私有属性 this._router
  3. 通过私有属性 this._router 进行调用init方法【还记得他吗,上一节,我们创建VueRouter类时,使用的】。因为this.$options.router接受到的是VueRouter的实例,实例自然有init方法
// 4.导出Vue 让在其他文件也可以使用Vue实例
export let _Vue
​
// 5. 导出install方法
export default function install(Vue) {
    // 5.1 获取Vue的实例
    _Vue = Vue
    // 5.2 进行vue实例的混入   
    _Vue.mixin({
        // 混入后 所有的vue实例的beforeCreate()方法都会执行如下代码
        beforeCreate() {
+        // 5.3 判断是否为Vue的实例
        // 如果条件成立 是Vue的实例 【这里的Vue实例指的是 new Vue()】
+        if (this.$options.router) {
            // 源码发现,Vue的实例会挂载到当前的私有属性_routerRoot属性上 方便后面的组件可以使用
+             this._routerRoot = this
             // 挂载路由实例到_router私有属性上 
             // 方便后面的组件可以使用
+             this._router = this.$options.router
             // 调用router实例的 init方法 router是new VueRouter()上,因此会有init方法
+             this._router.init(this)
        }
​
        }
    })
​
}

这里所指的new Vue传递Vue实例,指的是什么?是main.js文件里面的这个

image-20220819104611533

我们去查看VueRouter源码里面的install.js文件,发现很多相似之处

image-20220819104916716

第六步,如果不是new Vue 怎么办呢

就获取父亲【APP.VUE】的_routerRoot 也就是this,this._routerRoot = this.$parent && this.$parent._routerRoot 这句代码就是把父组件App.vue的this值给到当前的组件实例的_routerRoot

 _Vue.mixin({
        // 混入后 所有的vue实例的beforeCreate()方法都会执行如下代码
        beforeCreate() {
            // 5.3 判断是否为Vue的实例
            // 如果条件成立 是Vue的实例 【这里的Vue实例指的是 new Vue()】
            if (this.$options.router) {
                // 源码发现,Vue的实例会挂载到当前的私有属性_routerRoot属性上 方便后面的组件可以使用
                this._routerRoot = this
                // 挂载路由实例到_router私有属性上 
                // 方便后面的组件可以使用
                this._router = this.$options.router
                // 调用router实例的 init方法 router是new VueRouter()上,因此会有init方法
                this._router.init(this)
            } else {
                // 这句代码是什么意思?
                // 5.4 否则,就是其他对应的组件 
                // this.$parent表示父组件App.vue this.$parent._routerRoot 访问App.vue组件的_routerRoot的私有属性 这个App.vue组件的创建是new Vue()而来的 他的_routerRoot属性 有this 就是Vue实例,每个组件就能够使用this了
+                this._routerRoot = this.$parent && this.$parent._routerRoot
            }
        }
    })

如果报错

image-20220817150239142

这里察觉到Vue.use(Router)里面的Router是undefined

说明 install方法出了问题:

  1. 检查install方法的逻辑
  2. 其实最开始就该检查install方法是否引入成功 不然也不会是undefiend的了

5. 组件创建

第一步,components/下面创建两个文件,link.js和view.js分别用来渲染 router-link和router-view组件

第二步,link.js里面这样写

导出渲染一个a标签,感到陌生的是,domProps这个写法,这里还是有点不太清楚,!待查,注意书写是一个{domProps: {再一个对象}}

// 9. 这里创建的是一个a标签 domProps是属性名,第三个坑表示内容
// 默认导出 
// props传参 + render渲染组件
export default {
    props: {
        to: {
            type: String,
            required: true
        }
    },
    render(h) {
        return h('a', { domProps: {href: '#' + this.to}}, [this.$slots.default])
    }
}
// 10. 我们修改router/index.js里面引入的Router类
// 启动项目即可
// 此时点击 标签 发现组件内容不能够切换

第三步,view.js里面这样写

默认导出一个对象,里面用render函数渲染一个div,内容是router-view

// 8. 这里创建的是router-view组件 渲染为一个div 内容我们先填充为router-view
export default {
    render(h) {
        return h('div', 'router-view')
    }
}

第四步,install.js里面引入注册

// 导入link和view 进行组件的注册
+import Link from './components/link'
+import View from './components/view'// 4.导出Vue 让在其他文件也可以使用Vue实例
export let _Vue
​
// 5. 导出install方法
export default function install(Vue) {
    // 5.1 获取Vue的实例
    _Vue = Vue
    // 5.2 进行vue实例的混入   
    _Vue.mixin({
        // 混入后 所有的vue实例的beforeCreate()方法都会执行如下代码
        beforeCreate() {
            // 5.3 判断是否为Vue的实例
            // 如果条件成立 是Vue的实例 【这里的Vue实例指的是 new Vue()】
            if (this.$options.router) {
                // 源码发现,Vue的实例会挂载到当前的私有属性_routerRoot属性上 方便后面的组件可以使用
                this._routerRoot = this
                // 挂载路由实例到_router私有属性上 
                // 方便后面的组件可以使用
                this._router = this.$options.router
                // 调用router实例的 init方法 router是new VueRouter()上,因此会有init方法
                this._router.init(this)
            } else {
                // 这句代码是什么意思?
                // 5.4 否则,就是其他对应的组件 
                // this.$parent表示父组件App.vue this.$parent._routerRoot 访问App.vue组件的_routerRoot的私有属性 这个App.vue组件的创建是new Vue()而来的 他的_routerRoot属性 有this
                this._routerRoot = this.$parent && this.$parent._routerRoot
            }
​
        }
    })
    // 注册组件
+    Vue.component('RouterView', View)
+    Vue.component('RouterLink', Link)
}
​
​

我们把install.js文件引入到index.js 挂载VueRouter类上面去

import install from './install'
import createMatcher from './create-matcher'
// 1. VueRouter类
class VueRouter {
    constructor(options) {
        // 2. 接受options.routes 里面是路由的规则
        this._routes = options.routes || []
    }
    // 3. init方法 传入Vue 实例 传入路由变量的实例
    init(Vue) {
        console.log(Vue);
    }
}
// 6. 把install方法 挂载到VueRouter实例上面去
+VueRouter.install = install
+export default VueRouter

6. createMatcher方法的使用

第一步,在index.js里面引入文件,使用方法。

这个方法用来“解析routes选项,匹配路由规则”

import install from './install'
+import createMatcher from './create-matcher'
// 1. VueRouter类
class VueRouter {
    constructor(options) {
        // 2. 接受options.routes 里面是路由的规则
        this._routes = options.routes || []
        // 11. 添加对createMatch方法的调用
+        this.matcher = createMatcher(this._routes)
    }
    // 3. init方法 传入Vue 实例 传入路由变量的实例
    init(Vue) {
        console.log(Vue);
    }
}
// 6. 把install方法 挂载到VueRouter实例上面去
VueRouter.install = install
export default VueRouter

第二步,在create-matcher.js文件里面写如下方法

  1. 引入createRouteMap方法
  2. 从该方法里面 按需导入 pathList pathMap这两个方法
  3. 里面声明match 和 addRoutes方法 => match用来匹配路由规则,addRoutes用来动态添加路由规则
// 12. 导入createMap方法
import createRouteMap from './create-route-map'
export default function createMatcher(routes) {
    // debugger
    // 从 createRouteMap 里面导出 pathList pathMap的方法
    const {pathList, pathMap} = createRouteMap(routes)
    // match方法
    function match() {}
    // addRoutes方法 动态添加路由
    function addRoutes() {
        createRouteMap(routes, pathList, pathMap)   
    }
    // 返回
    return {
        match,
        addRoutes
    }
}

第三步,我们可以去查看 官方的 create-matcher.js文件里面的方法的使用

也有match 和 addRoute,同时addRoutes里面也用了createRouteMap方法

image-20220819130926227

第四步,我们去create-route-map.js文件里面声明这个方法

  1. 参数

routes: 路由规则,是一个数字,里面记录了path, component,children

oldPathList: 数组, [/home, /home/list] 大概是这样

oldPathMap: 是一个对象,{‘/home‘: {}}

image-20220819131549348

  1. 内部声明变量,接受形式参数
  2. 解析routes,遍历,再调用addRouteRecord这个方法。逻辑是,如果动态添加路由调用 addRoute => createRouteMap => addRouteRecord【这个方法用来添加路由记录,我们下面还要添加】
  3. 返回pathList和pathMap 我们可以打印这看看。
export default function createRouteMap(routes, oldPathList, oldPathMap) {
    // debugger
    // 声明初始值 pathList 和 pathMap
    const pathList = oldPathList || []
    const pathMap = oldPathMap || {} 
   
​
    // 遍历 routes 调用addRouteRecord()方法
    // 对路由进行解析 还要考虑 children的情况
    routes.forEach((route) => {
        addRouteRecord(route, pathList, pathMap)
    })
    console.log(pathList);
    console.log(pathMap);
​
    return {
        pathList,
        pathMap
    }
}

第五步,创建 addRouteRecord 方法,用来添加路由规则

四个参数,前面三个和之前的类似。

第四个,表示,当前的组件是否有父亲的路由记录?有的要也要进行设置,没有就是undefined

function addRouteRecord(route, pathList, pathMap, parentRecord) {
    
}

判断是否有父亲,有的话 path的值会不同

 
 function addRouteRecord(route, pathList, pathMap, parentRecord) {
+    const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path 
}

进行record记录的拼凑

 
 function addRouteRecord(route, pathList, pathMap, parentRecord) {
    const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path 
​
+    const record = {
        path,
        component: route.components,
        // 记录了父子的关系
        parent: parentRecord
    }
}

判断pathMap里面是否已经有这个记录了,如果有了,就跳过

如果没有,就要加入到pathMap里面去

判断route里面是否有children属性,如果有的话,就要递归遍历,添加记录,

// 如果已经有了record 相同的path 就直接跳过
+   if (!pathMap[path]) {
        pathList.push(path)
        pathMap[path]  = record
    }
​
    // 判断路由中是否有子路由
+    if (route.children) {
        route.children.forEach((childRoute) => {
            addRouteRecord(childRoute, pathList, pathMap, record)
        })
    }

也许看到这个你就理解了

image-20220819132019494

小结:

  1. 先是创建了VueRouter实例 index.js里面

先是创建了VueRouter实例,然后声明init方法,并且用_routes属性接受传递进来的routes参数。

image-20220819132113201

  1. 全局混入,让每个组件都可以使用Vue的实例

    install.js文件里面,我们进行了全局混入,让每个组件都可以使用Vue的实例this【this._routerRoot = this】。

    我们还调用了init方法。

  2. 渲染组件

    我们在my-router/view.js和link.js里面创建了a标签和div组件,

    引入到了install.js文件,并利用了Vue.component组件进行注册。

    再把install.js文件引入到index.js文件,挂载到Vue实例上去

  3. 我们在index.js里面调用了一个createMatcher方法,要去声明。

    这个方法从createRoute方法里面解构出pathList和pathMap两个变量。

    并且声明,match和addRoutes方法,返回他们

  4. 解析路由规则,添加路由记录我们在createRouteMap方法里面,遍历每个routes选项,为每个路由选项添加记录record

    如果有children,还要递归遍历,进行添加,

7. 我们测试一下

第一步,确保你在,项目根目录的router/index.js里面引入了刚刚自己写的my-router/index.js文件

image-20220819133254692

第二步,在项目的views文件夹下面,创建三个组件,并且router/index.js里面进行路由的配置 routes

const routes = [
  {
    path: '/about',
    components: About,
    children: [{
      path: 'users',
      components: Users
    }]
  },
  {
    path: '/',
    components: Home,
  },
]

第三,App.vue里面设置好router-link组件和router-view组件

<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </nav>
    <router-view />
  </div>
</template><style lang="less">
</style>

第四,打开页面

点击a链接,发现地址栏的信息能够改变,只是组件内容不会切换

image-20220819133530668

8. match方法的实现

第一,根据path => 从pathMap中找到对应的路由记录信息。pathMap里面是path和组件相关的信息

const record = pathMap[path]

第二,判断是否有record信息,如果有的话,就调用 createRoute方法,传入record信息和path信息

第三,在 my-router/util/route.js 文件里面定义这个方法 createRoute。

  • 方法返回 match数组 和 path路径。
  • 通过遍历判断record是否有parent,有的话unshift到match数组里,match数组记录了[parentRecord, childRecord]信息,与路由组件渲染内容一一对应
export default function createRoute(record, path) {
    // [parentRecord, childRecord] 这是match数组存储记录的顺序,先parent 再child => 组件的渲染的顺序是UI有
    // 路由记录信息的数组
    const match = []
    // 路由记录放到数组,可能有parent,也放到数组 注意是unshift
    while (record) {
        match.unshift(record)
        record = record.parent
    }
    // 返回path和match数组结合的对象
    return {
        path,
        match
    }
}

第四,match方法里面,如果有record路由记录,就return createRoute(record, path)的结果,如果没有,就返回createRoute(null, path)的结果

export default function createMatcher(routes) {
​
    // 从 createRouteMap 里面导出 pathList pathMap的方法
    const {pathList, pathMap} = createRouteMap(routes)
​
    // 疑问:这个方法什么时候调用呢 ?
    
    
+    function match(path) {
         // 根据路径 从 pathMap里面找到对应的路由信息 
+        const record = pathMap[path]
+        if (record) {
        // 根据对应的路由信息,调用 createRoute方法 最终生成 数组【有可能包含了父路由和子路由的record信息】
        // 这个方法 就让我们根据path => 直接获取到包含整个 路由记录的数组
        // 后续利用这个数组里面的记录的信息的内容,能够渲染出组件内容
+            return createRoute(record, path)
+        }
+        return createRoute(null, path)
+    }
    // addRoutes方法 动态添加路由
    function addRoutes(routes) {
        createRouteMap(routes, pathList, pathMap)   
    }
    // 返回
    return {
        match,
        addRoutes
    }
}

9. 历史记录处理

在这里,我们声明父类公共部分,和hash文件的类,

在base文件里设置路由跳转的方法,

hash文件里面设置:1. 获取路由地址 2. 添加 'hashchange'事件的监听

第一步,在history文件夹下面,创建 base.js hash.js html5.js 文件

image-20220822162244481

第二步,写base.js文件

  1. 引入createRoute方法,我们依靠它, 给current变量赋值,current记录了path和match的信息
  2. 写路由跳转的方法,修改current,通过 match方法,获取到record+path再给createRoute方法【在match方法里面执行】
  3. 执行相应的回调
import createRoute  from "../util/route";
​
export default class History {
    // router赋值
    constructor(router) {
        this.router = router
        // 为什么是null 和 '/'
        // 因为是初始化的时候,路由记录还没什么信息?
        // current调用
        this.current = createRoute(null, '/')
    }
    // 调用路由跳转的方法
    transitionTo(path, onComplete) {
        // 能够返回一个路由规则对象
        this.current = this.router.matcher.match(path)
        console.log(this.current);
        onComplete && onComplete()
    }
}

第三步,hash.js文件写

  1. 类要通过 extends实现继承 super关键字把 router给到父类

  2. ensureSlash方法执行,要先声明这个方法 => 首次进入页面添加 '/'

  3. 声明getCurrentLocation方法和setUpListener方法

    • window.location.hash.slice(1)能够获取到 #后面的值 www.baidu.com/#/login 获取到 /login,而slice(1) 能够获取到 login
    • setUpListener监听 hashchange事件,调用ensureSlash,如果有hash,就会return;执行跳转,hash事件触发,就会要跳转
/* 
    getCurrentLocation: 获取当前的地址栏地址 没有参数
    setUpListener: 能够监听地址栏的改变 hashchange
    ensureSlash:确保首次进入页面能够地址加上 / 加了后 自动拼接为 /#/ 也就是说 #/是自动拼上去的
*/import History from "./base"
export default class HashHistory extends History {
    constructor(router) {
        // 这个super必须写
        super(router)
        // 确保首次访问加上 #
        ensureSlash()
    }
    // 获取当前的地址栏的地址
    getCurrentLocation() {
        // 为什么要slice(1) 因为 -> 要省略# 
        // window.location.hash -> 获取的是 #后面的内容
        return window.location.hash.slice(1)
    }
    // 只要监听setUpListener方法 window就会开始监听hashchange事件    
    // 有什么参数? 不用传递
    setUpListener() {
        window.addEventListener('hashchange', () => {
            ensureSlash()
            this.transitionTo(this.getCurrentLocation())
        })
    }
​
}
// 确保地址栏输入信息时,能够加上 / 只要你加上 / 后面会拼接为 /#/
function ensureSlash() {
    // 如果单击的是链接 肯定有地址 不用再拼
    if (window.location.hash) {
        return 
    }
    // 如果是首次进入 没有地址 就要手动拼
    return window.location.hash = '/'
}

10. 不同的路由模式的处理

第一步, my-router/index.js

  1. 声明mode值,以及 this.mode都为 options.mode或者是 hash[默认值]

  2. 通过switch进行匹配,new HashHistory 和 HTML5History实例 ,记得引入哈

  3. 获取history实例,调用history.transitioTo方法进行跳转

    • 参数1:通过history.getCurrentLocation方法获取path
    • 把setUpListener传递作为回调函数
class VueRouter {
    constructor(options) {
        // 2. 接受options.routes 里面是路由的规则
        this._routes = options.routes || []
        // 11. 添加对createMatch方法的调用
        this.matcher = createMatcher(this._routes)
        // mode的修改
        // 注意mode的值来自于options
+        const mode = (this.mode = options.mode || 'hash')
+        switch(this.mode) {
+            case('hash'):
+                // 传递this就是VueRouter实例
+                this.history = new HashHistory(this)
+                break
+            case('history'):
+                this.history = new HTML5History(this)
+                break
+            default: 
+                throw new Error('mode error')
+           }
+       }
    // 3. init方法 传入Vue 实例 传入路由变量的实例
    init() {
+        const history = this.history
+        const setUpListener = () => {
+            history.setUpListener()
        }
        // 因为 new HashHistory(this) 时 会执行一次ensureSlash,修改了地址 -> 因此要进行一次路由的跳转 -> 要执行一次 transitionTo()方法
        // history.getCurrentLocation() 获取当前的地址栏输入的地址
        // setUpListener执行回调函数 注意 这里别加括号
+        history.transitionTo(
+            history.getCurrentLocation(),
+            setUpListener
+        )
    }
}

11. 测试

base.js文件里面打印即可看到结果

import createRoute  from "../util/route";
​
export default class History {
    // router赋值
    constructor(router) {
        this.router = router
        // 为什么是null 和 '/'
        // 因为是初始化的时候,路由记录还没什么信息?
        // current调用
        this.current = createRoute(null, '/')
    }
    // 调用路由跳转的方法
    transitionTo(path, onComplete) {
        // 能够返回一个路由规则对象
        this.current = this.router.matcher.match(path)
+        console.log(this.current);
        onComplete && onComplete()
    }
}

12. 定义响应式路由属性

这一步,目的是定义route属性,能够在路由跳转的时候修改route属性

第一步,在install.js文件,定义响应式属性 _route

// 导入link和view 进行组件的注册
import Link from './components/link'
import View from './components/view'// 4.导出Vue 让在其他文件也可以使用Vue实例
export let _Vue
​
// 5. 导出install方法
export default function install(Vue) {
    // 5.1 获取Vue的实例
    _Vue = Vue
    // 5.2 进行vue实例的混入   
    _Vue.mixin({
        // 混入后 所有的vue实例的beforeCreate()方法都会执行如下代码
        beforeCreate() {
            // 5.3 判断是否为Vue的实例
            // 如果条件成立 是Vue的实例 【这里的Vue实例指的是 new Vue()】
            if (this.$options.router) {
                // 源码发现,Vue的实例会挂载到当前的私有属性_routerRoot属性上 方便后面的组件可以使用
                this._routerRoot = this
                // 挂载路由实例到_router私有属性上 
                // 方便后面的组件可以使用
                this._router = this.$options.router
                // 调用router实例的 init方法 router是new VueRouter()上,因此会有init方法
                this._router.init(this)
                // 响应式属性 地址栏属性变化 对应的属性也要发生改变
+                Vue.util.defineReactive(this, "_route", this._router.history.current)
            } else {
                // 这句代码是什么意思?
                // 5.4 否则,就是其他对应的组件 
                // this.$parent表示父组件App.vue this.$parent._routerRoot 访问App.vue组件的_routerRoot的私有属性 这个App.vue组件的创建是new Vue()而来的 他的_routerRoot属性 有this
                this._routerRoot = this.$parent && this.$parent._routerRoot
            }
​
             
​
              Object.defineProperty(Vue.prototype, '$router', {
                get() {
                    return this._routerRoot._router
                }
              })
​
              Object.defineProperty(Vue.prototype, '_route', {
                get() {
                    // 这个值 app._route = route
                    return this._routerRoot._route
                }
              })
​
​
        }
    })
    // 注册组件
    Vue.component('RouterView', View)
    Vue.component('RouterLink', Link)
}
​
​

第二步,在history/base.js文件里面定义 cb属性,和listen方法

import createRoute  from "../util/route";
​
export default class History {
    // router赋值
    constructor(router) {
        this.router = router
        // 为什么是null 和 '/'
        // 因为是初始化的时候,路由记录还没什么信息?
        // current调用
        this.current = createRoute(null, '/')
​
+        this.cb = null 
    }
+    listen(cb) {
+        // 注意 这里的cb 会被赋值为函数
        // 所以下面的transitioTo会被调用 传递参数 this.current
        this.cb = cb
    }
    // 调用路由跳转的方法
    transitionTo(path, onComplete) {
        // 能够返回一个路由规则对象
        this.current = this.router.matcher.match(path)
        // console.log(this.current);
        
        onComplete && onComplete()
    }
}

第三步,在index.js里面调用上面定义的方法listen

import install from './install'
import createMatcher from './create-matcher'
import HashHistory from './history/hash'
import HTML5History from './history/html5'
// 1. VueRouter类
class VueRouter {
    constructor(options) {
        // 2. 接受options.routes 里面是路由的规则
        this._routes = options.routes || []
        // 11. 添加对createMatch方法的调用
        this.matcher = createMatcher(this._routes)
        // mode的修改
        // 注意mode的值来自于options
        const mode = (this.mode = options.mode || 'hash')
        switch(mode) {
            case('hash'):
                // 传递this就是VueRouter实例
                this.history = new HashHistory(this)
                break
            case('history'):
                this.history = new HTML5History(this)
                break
            default: 
                throw new Error('mode error')
        }
    }
    // 3. init方法 传入Vue 实例 传入路由变量的实例
    init(app) {
        const history = this.history
        const setUpListener = () => {
            history.setUpListener()
        }
        // 因为 new HashHistory(this) 时 会执行一次ensureSlash,修改了地址 -> 因此要进行一次路由的跳转 -> 要执行一次 transitionTo()方法
        // history.getCurrentLocation() 获取当前的地址栏输入的地址
        // setUpListener执行回调函数 注意 这里别加括号
        history.transitionTo(
            history.getCurrentLocation(),
            setUpListener
        )
​
        // 在这里调用 history里面的listen方法
        // 箭头函数传递给listen
+        history.listen((route) => {
+            app._route = route
+        })
    }
}
// 6. 把install方法 挂载到VueRouter实例上面去
VueRouter.install = install
export default VueRouter
​
​
​

第四步,调用cb函数,如果地址栏信息改变,route信息也要改变

在哪里调用cb函数呢?

import createRoute  from "../util/route";
​
export default class History {
    // router赋值
    constructor(router) {
        this.router = router
        // 为什么是null 和 '/'
        // 因为是初始化的时候,路由记录还没什么信息?
        // current调用
        this.current = createRoute(null, '/')
​
        this.cb = null
    }
    listen(cb) {
        // 注意 这里的cb 会被赋值为函数
        // 所以下面的transitioTo会被调用 传递参数 this.current
        this.cb = cb
    }
    // 调用路由跳转的方法
    transitionTo(path, onComplete) {
        // 能够返回一个路由规则对象
        this.current = this.router.matcher.match(path)
        // console.log(this.current);
        // 调用cb -> 是啥意思
+        this.cb && this.cb(this.current)
        onComplete && onComplete()
    }
}

是什么意思呢?我下面截了个图。要记得 _route属性是我们之前定义的“响应式属性”挂载到vue实例上哒

this.current里面记录了路由的规则对象信息,我们赋值给route

image-20220822180718507

8/22日小结:

  1. 学习了vue2响应式和vue3响应式的简单写法

  2. 学习了发布订阅模式和观察者模式的概念

  3. 学习了vueRouter的,match方法是干嘛用的

  4. 学习了vueRouter是处理历史记录:

    • 如果第一次进入页面,会给用户的地址增加 ‘/’
    • 有方法 能够获取到当前地址栏的地址
    • 如果地址栏的信息改变,hashChange事件能够触发
    • 能够实现路由跳转
  5. 不同的路由模式处理

    • 根据mode,匹配不同的路由模式
    • 第一次进入页面 触发init方法
    • 后续改变地址栏,触发hashChange
  6. 测试 打印current变量

  7. 响应式路由:

    • 定义_route变量,并且定义cb函数,来修改cb变量

\