前言
如今大前端的趋势下,你停下学习的脚步了吗?Vue3.0都Beta了,但是还是感觉有些知识点云里雾里的,小编研究了一下Vue-Router
源码整理和总结了一些东西,看尤大大怎么设计的。希望能够对你们有所帮助,如果喜欢的话,可以帮忙点个赞👉。
阅读本文之前,小编有三句话要说:
1.下面因为源码可能会变,所以没有贴源码,源码可以根据文章链接去github上下载
2.本文的基本思路是根据源码的index.js
文件走的
3.如果喜欢的本文的话,关注小编公众号:[小丑的小屋]。
安装
npm install vue-router
使用方法见官网
正文
1. install.js源码
源码地址:https://github.com/vuejs/vue-router/blob/dev/src/install.js
1.1源码解析
首先在解析之前不得不说尤大大的细节做的是真好👍,第一行代码首先做了防止VueRouter
的重复注册。
export function install (Vue) {
if (install.installed && _Vue === Vue) return
install.installed = true
_Vue = Vue
}
接着使用了Vue.mixin
混入的方法注册组件,使用了beforeCreate
和destoryed
两个钩子。
Vue.mixin({
beforeCreate () { //生命周期创建之前,一般情况是给组件增加一些特定的属性的时候使用这个钩子,在业务逻辑中基本上使用不到
if (isDef(this.$options.router)) { //isDef判断是否存在
this._routerRoot = this //this是根Vue实例
this._router = this.$options.router //把根实例上的router属性挂载到_router
this._router.init(this) //调用init初始化路由的方法
//defineReactive数据劫持,一旦`this._router.history.current`值发生变化,更新_route
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this //向上它的父亲一直向上找解决根组件嵌套问题
}
registerInstance(this, this) //注册实例
},
destroyed () {
registerInstance(this) //销毁实例
}
})
beforeCreate
这个钩子代表生命周期创建之前,一般情况下是给组件增加一些特定的属性的时候才会使用的,在业务逻辑中基本上是使用不到的。在beforeCreate
钩子中做了很重要的一步,判断根Vue实例上是否配置了router
,也就是我们经常用main.js
中的路由的注册。
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router, //😚就是这个地方😍
render: h => h(App),
}).$mount('#app')
如果没有配置会向他的父级查找,保证每一个节点上都有_routerRoot
属性,解决根组件的嵌套
问题,如果没有this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
这一行代码,我们子组件上没有__routerRoot
属性。
Vue.util.defineReactive(this, '_route', this._router.history.current)
defineReactive
这个方法是Vue中的核心方法之一,即响应式原理。一旦this._router.history.current
值发生变化,更新_route
。那么如果页面的路由改变是怎么改变_route
的呢?在index.js
的init
方法里:
history.listen(route => { //发布订阅模式每个 router 对象可能和多个 vue 实例对象(这里叫作 app)关联,每次路由改变会通知所有的实例对象。
this.apps.forEach(app => {
app._route = route
})
})
registerInstance(this, this)
这个函数怎么理解呢?我认为就是router-view
的注册函数,_parentVnode
是实例的虚拟父级节点,需要找到父级节点中的router-view
。首先会去判断是否存在父子关系节点,根据节点的层级在route
的matched
的属性上找到对应的数据之后,如果组件的路径component
或者路由route.matched
没有匹配渲染会render
一个h()
,那么data
上面就不会添加registerRouteInstance
注册路由的函数;
const matched = route.matched[depth]
const component = matched && matched.components[name]
// render empty node if no matched route or no config component
if (!matched || !component) {
cache[name] = null
return h()
}
registerInstance(this, this)
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
registerInstance
这个方法在beforeCreate
和destroyed
的时候都被调用了一次,如果val
值是undefined
那么这个路由实例就会被注销,即matched.instances[name] = undefined
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration val可能没有定义被注销
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
这两个方法是利用Object.defineProperty
的get
方法给vue
原型上添加$router
和$route
属性,这样就和上面提到的保证每一个节点上都有_routerRoot属性相呼应,如果没有_routerRoot
,这里的添加属性会报错。
//vue原型上添加$router属性
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
//vue原型上添加$route属性
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
这里有个面试题:$route
和$router
的区别:
$route
是一个对象
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}
$router
就是VueRouter
的实例
注册RouterView
和RouterLink
组件。
Vue.component('RouterView', View) //router-view组件
Vue.component('RouterLink', Link) //router-link组件
view
和link
两个组件都是函数组件
1.2总结
在install.js
中主要做了如下几件事:
1、绑定父子节点路由的关系
2、路由导航改变响应式的原理
3、将组件的实例和路由的规则绑定到一起
4、注册全局的$route
和$router
方法
5、注册router-link
和router-view
组件
2. view.js源码
源码地址:https://github.com/vuejs/vue-router/blob/dev/src/components/view.js
2.1源码解析
函数组件中主要包含了props
和render
两部分。
props
中配置项name
默认是default
与之对应的就是路由的命名视图部分
props: {
name: {
type: String,
default: 'default'
}
},
render
部分对应两个参数_
,{props, children, parent, data}
,其中_
对应的是createElement
方法,
{props, children, parent, data}
对应的是context
,即:
props
提供所有 prop 的对象children
:VNode 子节点的数组parent
:对父组件的引用data
:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
通过当前路由地址所属的层级,找到在matched
的位置,进行对应的渲染,如果的找不到不进行渲染。如果是父节点找到keepAlive
的状态,之前加载过的直接使用直接的缓存,如果没有渲染一个空页面。
2.2总结
在view.js
中主要是做了如下几件事:
1、一直向父级查找,找到当前路由所属的层级,找到对应的router-view
进行渲染。
2、判断keepAlive
的状态决定如何渲染。
3.link.js源码
源码地址:https://github.com/vuejs/vue-router/blob/dev/src/components/link.js
3.1 源码解析
与router-view
一样router-link
也是一个函数组件,其中tag
默认会被渲染成一个a
标签.
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
exact: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
ariaCurrentValue: {
type: String,
default: 'page'
},
event: {
type: eventTypes,
default: 'click'
}
},
通过这些参数的配置调用render()方法中的h(this.tag, data, this.$slots.default)
渲染vnode
,即<tag data >{this.$slots.default}</tag>
4.create-matcher.js源码
源码地址:https://github.com/vuejs/vue-router/blob/dev/src/create-matcher.js
4.1源码解析
export function createMatcher (
routes: Array<RouteConfig>, //router中的routes
router: VueRouter //router的配置
):
createMatcher
方法利用createRouteMap
这个方法去格式化路由,而createRouteMap
这个方法最终返回3个参数pathList
,pathMap
,nameMap
,同时通过遍历和递归调用addRouteRecord
方法对一系列的属性(包括name
,path
,children
,props
,路径正则
,匹配规则是否开启大小写
等)进行判断和格式化之后返回需要的数据格式。
pathList: Array<string>, //列表
pathMap: Dictionary<RouteRecord>, //字典
nameMap: Dictionary<RouteRecord> //字典
拿到这些数据之后,返回了两个方法
addRoutes
和match
。
4.2 总结
1.create-matcher.js
主要的作用是拿到处理好的数据格式之后,导出两个核心方法
2.create-route-map.js
主要的作用是处理数据的格式。
5.路由模式源码
源码地址:https://github.com/vuejs/vue-router/tree/dev/src/history
5.1源码解析
源码的结构是这样的:
首先定义了History
类,HashHistory
、HTML5History
、AbstractHistory
都是继承History
。
1、hash
对应的是HashHistory
,这个类里面主要的核心方法是setupListeners
通过判断浏览器或者手机是否支持supportsPushState
即window.history.pushState
属性。如果不懂pushState
可以阅读我的一篇文章<一文带你真正了解histroy>。如果支持监听popstate
事件,如果不支持监听hashchange
事件,在你采用浏览器前进后退时或者触发go()
等事件来触发popstate
。在监听之后采用发布订阅模式有一个事件移除机制,很细节哦。如果不支持supportsPushState
使用window.location.hash
或者window.location.replace||assgin
。最后通过调用base.js
中的基础类中的transitionTo
方法通过this.router.match
匹配到路由之后,通知路由的更新.
history.listen(route => { //发布订阅模式
this.apps.forEach(app => {
app._route = route //$route的改变
})
})
2、history
对应的是HTML5History
,这个类里面主要的核心方法是setupListeners
监听了popstate
事件。
3、abstract
对应的是AbstractHistory
,这个类主要的核心声明了一个列表,判断列表里有没有这个路由或者下标,然后直接通知路由的更新。
5.2总结
路由模式
主要做了如下几件事:
1、通过对路由模式的不同监听不同的事件,hash
监听popstate
和hashchange
事件;history
监听popstate
事件
2、通用transitionTo
方法去更新路由信息。
补充知识:判断数据类型的四种方法:
typeof
instanceof
constructor
Object.prototype.toString.call
结尾
上面内容是通过index.js
文件的思路串行下来的,当然还有很多细节需要补充,后续的文章会更精彩,有错误的地方欢迎指出,欢迎点评🤭,更多干货欢迎关注公众号:小丑的小屋。
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 小丑同学 原文链接:https://juejin.im/post/6864156289267597326