![07a01bb50f68027987ba51cbc5463b65.png](https://i-blog.csdnimg.cn/blog_migrate/e30b943e00bc99e61383548986b5969f.jpeg)
![915757baf7f6d14810364affc124a5ef.png](https://i-blog.csdnimg.cn/blog_migrate/28126f62b954886ebc7e8107108b9a7f.jpeg)
![2b34e97a7549d169ff5045f4177b7dc1.png](https://i-blog.csdnimg.cn/blog_migrate/431d003836fc3ac3b54229c38607bca3.jpeg)
![40b9f74acc65997e679cdf974cd743e8.png](https://i-blog.csdnimg.cn/blog_migrate/26d79e3438b6ac92340bd7258281d3a2.jpeg)
照例还是读官方文档,边读边写问题。
![d6407caababf0fb6d4139abe199ae83d.png](https://i-blog.csdnimg.cn/blog_migrate/642854f4f9279b12a2ede399bfeccfd9.jpeg)
准备
必须先介绍前端路由的基本知识,有了深厚的内功之后可以驾驭任何外公,不,外功,举证参见张无忌。
![ef52efad7e3945acce04b4d52e6fa52e.png](https://i-blog.csdnimg.cn/blog_migrate/e98a43938cd7dbd62cce0c1d5a743d2e.png)
首先看看如果前端实现路由,要满足哪些前提条件,以下四点是我总结的
- 路由体现在URL上
- 路由能携带参数
- 路由的改变不会引起页面刷新
- 能让浏览器历史里记录下路由变化
于是只剩hash
和HTML5的state
API可选了。
hash
是URL上带#
的部分,本意是用来做页面的锚点的,在锚点之间的跳转是不会触发页面刷新的,只在浏览器侧发生变化。监听hash
的改变有hashchange
事件。
state
API目前主流浏览器都支持了(不要跟我再提什么IE9,来气)。一句话描述就是,history
提供了pushState
和replaceState
两个API能改变URL,但不会触发页面刷新。监听state
的改变没有事件,但是对于变化后浏览器的后退行为,有popstate
事件。
至于两者的优劣,网上比较的太多了,我就不掺和了。除了这些以外,我们要做的实验是这两者是否会相互影响,测试代码如下
<!DOCTYPE html>
<html>
<head>
<title>Route</title>
</head>
<body>
<a href="#foo">foo</a>
<a href="#bar">bar</a>
<button id="btn1">Change Normal State</button>
<button id="btn2">Change Hash State</button>
<div id="foo" style="border: 1px solid red; height: 800px">foo</div>
<div id="bar" style="border: 1px solid blue; height: 800px">bar</div>
<script>
window.addEventListener("hashchange", e => {
console.log("hash改变了");
});
window.addEventListener("popstate", e => {
console.log("popstate被触发了");
});
document.getElementById("btn1").addEventListener("click", e => {
history.pushState("test", "test", "test");
});
document.getElementById("btn2").addEventListener("click", e => {
history.pushState("test", "test", "#test");
});
</script>
</body>
</html>
首先测试两个超链接,发现无论是点击超链接,还是之后再点击浏览器的返回,都会触发hashchange
和popstate
事件。
接下来测试按钮的行为,其中一个按钮将URL变为/test
,另一个按钮将URL变为/#test
。测试后发现,点击按钮,即便是修改hash
的行为,也不会触发hashchange
事件,在点击浏览器返回时,都会触发popstate
事件。
总结一下,popstate
是好兄弟,hashchange
触发的时候,它也会跟着一起上。
想起了一个重口味的梗,小便和大便哪个更讲义气?
结构
│__create-matcher.js
│__create-route-map.js
│__index.js
│__install.js
│
├─components
│______link.js
│______view.js
│
├─history
│______abstract.js
│______base.js
│______errors.js
│______hash.js
│______html5.js
│
└─util
│______async.js
│______dom.js
│______location.js
│______misc.js
│______params.js
│______path.js
│______push-state.js
│______query.js
│______resolve-components.js
│______route.js
│______scroll.js
│______state-key.js
│______warn.js
源码使用Flow
开发,rollup
来做构建。
对象声明
Vue
生态圈的各框架初始化过程大致相似。
export default class VueRouter {
//...
}
VueRouter.install = install
VueRouter.version = '__VERSION__'
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
除了暴露出VueRouter
之外,在它之上还加了install
方法,用作Vue
的插件。另外还判断了浏览器和全局Vue
的存在,装载插件,就是为了在全局引用的时候不需要再调用Vue.use(VueRouter)
。
install
install.js
就光暴露了install
方法,在装载插件的时候执行,因此它是第二层的初始化,主要针对的不是Vue-Router
对象本身而是针对整个Vue
组件树,之前创建Vue Router
对象的时候还没有感知Vue
,但此时就跟Vue
挂钩了。
这里局部的变量终于起名为了_Vue
,让我舒服了很多,之前读Vuex
的时候,相同作用的这个变量取名为Vue
,着实让我困惑了一把。
跟Vuex
一样,它也是定义了一个mixin
。
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this) // 后文会提到这个方法
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
跟Vuex
里另一点相似的是_routerRoot
——在根部,这个变量指向Vue
实例自己,在其他的Vue
实例上,这个变量指向根Vue
实例,并且也是通过父组件的该变量一层层传下来的。
registerInstance
看着有点意思,在beforeCreate
和destroyed
里调用的时候只差一个参数,而这一个参数的差别肯定引起的效果是相反的。
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
初看有点不明所以,这时就要借助于debugger手段了。多读了几遍之后发现,原来坑就坑在这个i
最后相当于是vm.$options._parentVnode.data.registerRouteInstance
,但是又没给中间变量取别的名字。
通过全局搜索发现,registerRouteInstance
这个方法定义在了<router-view>
组件里,也就是说只有<router-view>
的下一层组件实例的beforeCreate
方法,才会进到if
里,调用registerRouteInstance
方法,这个方法的作用如其名字,在全局注册实例,而如果callVal
为undefined
的话,实际效果就是unregister
取消注册了。
接着就是在原型上定义两个变量,这样让每一个Vue
组件都能访问到$router
和$route
,并且是指向同一个对象实例的。
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
然后把<router-view>
和<router-link>
两个组件定义在全局,在哪里都能直接用它们。
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
最后是自定义选项合并策略,可以参考https://cn.vuejs.org/v2/guide/mixins.html#自定义选项合并策略)
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
初始化
构造函数是第一层的初始化,主要针对VueRouter
对象本身。
兵马未动粮草先行,先定义实例变量。
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
接下来先设置好默认的模式,Vue Router
一共支持三种模式。
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
有点绕,我觉得还不如堆砌if else
呢。实际逻辑是
- 如果运行环境不是浏览器,那就设置为
abstract
- 如果是浏览器运行环境,默认为
hash
- 但即便你设置了
history
,我也要先检查一下你浏览器是不是支持history
的BOM API,要是不支持,那么对不起,仍然滚回hash
去
有了模式之后,就用简单工厂模式来创建对象了
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
看着好像很简单的样子,但那是代码给你的错觉,还有一部分的初始化工作在init
方法里。这个init
方法在什么时候会被调用?其实就在刚才看到的mixin
代码里。
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
}
在Vue
组件树根实例上(严格的说是Vue Router
挂载的那个Vue
实例上,因为可以把Vue Router
挂载在全量组件树的下面某一层上)执行这个init
方法。
看一下init
方法,抠掉不重要的代码。
init (app: any /* Vue component instance */) {
this.apps.push(app)
// main app previously initialized
// return as we don't need to set up new history listener
if (this.app) {
return
}
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
传入的app
就是Vue Router
对象实例所挂载的Vue
实例,这个很好理解,但居然用了一个数组this.apps
来缓存起来,想想除非是一个Vue Router
对象实例被挂到了多个Vue
根组件上,那么才会产生在Vue Router
对象实例上用一个数组来缓存所有的Vue
根组件,暂时想象不出这种业务场景,绝大多数情况应该只会有一个Vue
路由根组件。
下一步就是跳转了,因为有可能用户打开的并不是一个根路径,所以要根据实际URL来跳转到对应的路由去,也即是渲染对应的组件,同时这也是路由的意义——按照URL可以直接打开对应的页面。
最后就是监听变化,和之前beforeCreate
里的代码再呼应起来。刚才我们漏掉了一句
Vue.util.defineReactive(this, '_route', this._router.history.current)
在Vue
路由根组件的_route
属性上定义了一个响应,当这个属性发生变化的时候,会引起Vue
的重新渲染,一会我们会做实验看到当_route
发生变化时,会重新进入<router-view>
和<router-link>
的render
渲染函数。调用history
对象监听到变化后,就会去修改这个_route
对象。