本系列主要讲述 Vue 里面的一些核心源码的实现原理,这章节主要讲述 Vue-Router 内部是如何实现和工作的。
上图为 Vue-Router 核心的全部知识点,上到下、右到左逐步深入剖析 Vue-Router 内部的核心原理。
路由
路由:通过互联的网络把信息从源地址传输到目的的地址的活动
。路由发生在 OSI 网络参考模型中的第三层即网络层。路由引导分组传送,经过一些中间的节点后,到它们最后的目的地。作成硬件的话,则称之为路由器
。路由通常根据路由表
--- 一个存储到各个目的地的最佳路径的表 --- 来引导分组传送。
上面的定义很官方,我们可以从中抽取出一些重点来:
路由是一种活动,负责将信息从源地址传输到目的地址
要完成这样一个活动需要一个很重要的东西 路由表---源地址和目的地址的映射表。
前端路由
在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI(组件) 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新,无需页面的刷新。
简单点来说就是:输入 URL -> JavaScript解析地址 -> 找到对应的地址页面 -> 执行页面产生的 JavaScript -> 看到页面
后端路由
用户输入一个 URL,浏览器传递给服务端,服务端匹配映射表,找到对应的处理程序,返回对应的资源(页面或者其他)。
简单点来说就是:输入 URL -> 请求发送到服务器 -> 服务器解析请求的路径 -> 拿到对应的页面信息 -> 返回给客户端
前端路由的两个核心
如何做到当 URL 发生变化的时候却不会引起页面的刷新?如何检测到了 URL 地址发生了改变?下面通过前端路由的两个核心 hash
和 history
分别来实现。
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)
})
}
}
扁平化后的数据展示:
createRouteMap 方法作用:
- 通过上面的
createMatcher
方法可以知道这里的createRouteMap
其实有两个作用,第一就是初始化配置,第二就是可以动态添加路由信息。所以该方法有三个参数。 - 当初始化配置的时候,此时的
pathList
和pathMap
均取默认值为空。 - 需要循环每一个
routes
信息,addRouteRecord
函数主要用来记录并且记录在pathList
和pathMap
中。 - 判断
存放的 path 是否已经存在了 pathMap 中
,如果没有,则需要将path
存储在pathList
和pathMap
中。 - 判断
是否存在children多层嵌套路由
,如果存在,则需要递归addRouteRecord
函数,这里需要注意:
需要将parent
传入,才能知道是属于哪个父组件的子组件,并将其正确的拼接存放到pathList
和pathMap
中 - 最后将
pathList
和pathMap
返回
为什么要在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)
}
完善 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 类
完成上面的所有操作之后,这里还有一个小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。
函数式组件
之前创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 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核心原理的全部过程,效果如下:
需要源码的话可以去gitee获取。戳我获取源码!!!