我们知道vue-router有两种mode:一种是history,格式是已/开头的。如:/a,/b;一种是hash模式,格式是已#/开头的。如:#/a,#/b;这两种主要都是依据window的history属性来实现的。那么我们回顾一下history属性;
概念:history表示当前窗口的浏览历史。
相关api:
(1)history.length // 历史记录的长度
(2) history.go(0) // 0 表示刷新 正数表示前进步数 负数表示后退步数
(3)history.state // history堆栈最上层的状态值
(4)history.back() // 移动到上一个网址
(5)history.forward() // 移动到下一个网址
(6)history.pushState() //方法用于在历史记录中增加一条记录(state,title,url)
注意:
(1)添加历史记录,导航栏会显示url,但是不会跳转到对应的页面,甚至不会检查是否存在,只是成为浏览历史中的最新记录,总之,pushState()方法不会触发页面刷新,只是导致History对象发生变化,地址栏会有反应。
(2)对于hash路由,也不会触发hashchange事件,但是锚点变了,回创建记录
(3) pushState要想插入一个跨域网址,会导致报错。防止恶意代码让用户以为他们是在另一个网站上,因为这个方法不会导致页面跳转。
(7)history.replaceState用来改变histoty当前的记录 也就是替换掉栈顶的记录
(8)popstate:表示每当同一个文档的浏览历史(即history对象)出现变化的时候会触发,但是排除(pushState,replaceState增加的history记录)只有用户点击浏览器倒退,前进按钮才会触发。
简单来讲,对于vue-router当mode为hash的时候,就是监听hashchange方法获取到当前的路径名,然后渲染当前路径对应的组件显示到页面上面,而对于mode:history的时候,就是监听popstate方法同样也是获取到当前的路径名,然后渲染当前路径对应的组件显示到页面上面。
基本原来我们知道了,我们现在来看源码是怎么实现的。
在使用vue插件的时候,
(1)首先会默认调用插件的install方法,并且传递两个参数一个是全局Vue构造函数一个是options一些配置属性。vue使用插件是在new Vue之前执行的。所以我们先看Vue插件做了什么,首先会使用Vue.mixin混入,给每一个跟实例以及子实例增加beforeCreate方法,以及声明两个全局组件router-link以及router-view和两个原型属性 r o u t e 和 route和 route和router。这就是install所干的事情。
总结vue-router的组成部分是由四部分组成,及声明两个全局组件router-link以及router-view和两个原型属性 r o u t e 和 route和 route和router。
(2)然后开始Vue执行的入口文件是main.js,在main.js中先声明Vue的实例,在Vue实例中注入router实例,所以我们先分析router实例的源码。
import { createMatcher } from './create-matcher';
import BrowserHistory from './history/history'
import HashHistory from './history/hash'
import install from './install'
class VueRouter{
constructor(options){
// console.log(options)
// 根据用户的配置 和 当前请求的路径渲染对应的组件
// 创建匹配器 可以用于后序的匹配操作
// 用户没有传递配置 就默认传入一个空数组
// 1.match 通过路径来匹配组件
// 2.addRoutes 动态添加匹配规则
// 给实例增加matcher属性
this.matcher = createMatcher(options.routes || [])
// 我需要根据不同的路径进行切换
// hash h5api
options.mode = options.mode || 'hash'; // 默认没有传入就是hash模式
switch(options.mode){
case 'hash':
this.history = new HashHistory(this)
break;
case 'history':
this.history = new BrowserHistory(this);
break;
}
this.beforeHooks = [];
}
match(location){
return this.matcher.match(location)
}
push(to){
// console.log('跳转',to)
this.history.push(to)
// this.history.transitionTo(to); // 跳转路由
}
init(app){
// 初始化操作
// 监听hash值变化 默认跳转到对应的路径中
const history = this.history;
// 跳转到对应路径
const setUpHashListener = ()=>{
history.setUpListener() // 监听路由变化 hashchange
}
// 默认会调用一次路由跳转到对应的位置,在transitionTo方法中如果传递了回调函数那么会执行这个回调函数,这个成功的回调函数中会定义一些事件监听,监听hash的变化
history.transitionTo(
history.getCurrentLocation() ,// 获取当前的位置路径
setUpHashListener
)
// 每次路径发生变化都会调用此方法
history.listen((route)=>{
app._route = route;
})
// setUpListener 方法hash里面去 transitionTo 放到base中公共方法 getCurrentLocation 放到自己家里 window.locaion.hash /path
}
beforeEach(fn){
this.beforeHooks.push(fn)
}
}
VueRouter.install = install;
export default VueRouter;
在router实例中,首先创建匹配器,然后根据mode模式创建对应的history实例,history实例上面会挂载当前router实例,以及声明current属性,这个属性是初始化匹配的path和matched数组(匹配的组件数组),在HashHistory上面会调用ensureSlash,这个方法是确保用户输入的是以#/格式的url。
(3)初始化router实例以后,vue实例开始进行初始化操作,在初始化状态之前会调用beforeCreate声明周期,在install中我们看到了在全局上面混入了beforeCreate声明周期, 我们来看源码:
beforeCreate(){
// 将父亲传入的router实例共享给所有的子组件
if(this.$options.router){
// Vue构造函数的实例 跟实例的$options
// console.log('父亲',this.$ options.name)
this._routerRoot = this; // 给当前跟组件一个属性 代表的是他自己
this._router = this.$options.router;
// 初始化方法
this._router.init(this); // 这里的this就是跟实例
// console.log('父组件this',this)
// 如何获取到current属性 当然路由要渲染的组件
// 这个this属性是跟实例
// 如何获取到current属性 将current属性定义在_route上
Vue.util.defineReactive(this,'_route',this._router.history.current)
// 当current 发生变化 更新_route 属性
// 如果current中的path或者是matched的其他属性发生变化 也是响应式的
}else{
// 子组件 继承Vue的构造函数Sub的实例
// console.log('子组件this',this)
// console.log('儿子')
// 组件渲染是 一层一层的渲染
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
// 无论是父组件还是子组件 都可以通过 this._routerRoot._router 获取共同的实例;
}
在这个生命周期上面首先区分是子实例还是跟实例,如果是跟实例的话,我们将在跟实例上面挂载_routerRoot属性就是跟实例,以及增加_router属性这个属性就是当前的router实例,然后调用router实例的init方法。
(4)我们来看router实例的init方法,她会调用history实例的transitionTo方法并且传入两个函数history.getCurrentLocation() ,setUpHashListener
我们主要看这是三个函数。
const setUpHashListener = ()=>{
history.setUpListener() // 监听路由变化 hashchange
}
// 默认会调用一次路由跳转到对应的位置,在transitionTo方法中如果传递了回调函数那么会执行这个回调函数,这个成功的回调函数中会定义一些事件监听,监听hash的变化
history.transitionTo(
history.getCurrentLocation() ,// 获取当前的位置路径
setUpHashListener
)
//
transitionTo(location, onComplete) {
// 跳转时会条用此方法 from to
// 路径变化了 视图还要刷新 响应式数据原理
let route = this.router.match(location); // {'/'.matched:[]}
if (
location == this.current.path &&
route.matched.length == this.current.matched.length
) {
// 防止重复跳转
return;
}
// 在更新之前先调用注册好的声明周期 导航守卫 再去执行下面的逻辑
let queue = [].concat(this.router.beforeHooks); // 拿到了注册的方法
const iterator = (hook,next)=>{
hook(this.current,route,()=>{next()})
}
runQuene(queue,iterator,()=>{
this.updateRoute(route)
// 根据路径加载不同的组件 this.router.matcher(location) 组件
onComplete && onComplete();
})
}
在history的transitionTo方法中,我们先根据路径获取匹配的路径以及所有的组件,然后判断这次匹配的路径和组件个数和上次的比较如果不同那么说明路径变化了,需要重新渲染组件, 然后调用onComplete方法,这个方法是history上面的setUpListener方法
setUpListener(){
window.addEventListener('hashchange',()=>{
//hash值变化了 拿到最新的hash值 进行跳转
this.transitionTo(getHash()) // hash变化再次进行跳转
})
}
这个方法中监听hash值的变化,如果hash值变化了,那么重新调用transitionTo方法并将当前的hash值传递进去。
总之这块代码的意思就是在beforeCreate方法中,默认先根据路径匹配当前的路由信息,在增加事件监听对hash变化的监听,如果当前的路由有变化继续更改当前的路由信息,并将当前的路由信息传递给history对象的current属性上面。
router的init方法中还要调用listen方法,listen方法就是将这个回调函数赋值给history对象的一个cb属性,在transitionTo方法中如果路由发生变化会调用updateRoute方法,这个方法就是更改将当前匹配的route赋值给current属性,并且调用传入的cb方法
history.listen((route)=>{app._route = route;})
这个cb是将当前的vue跟实例上面_route属性赋值给当前切换的route对象。
(5)好的,在beforeCreate中调用init方法完成以后,会在Vue实例上面赋值_route属性,这个属性值等于实例上面的_router属性的history对象上面的current属性值,我们前面说了,这个属性值等于的就是当前是匹配的信息包括匹配的path和匹配的组件数组。
(6)beforeCreate声明周期以及状态初始化完成以后开始进行,生成虚拟dom,将虚拟dom转变成真实的dom,也就是真实的渲染了,我们就可以看看我们前面声明的两个组件了最重要的是routerview组件的渲染,我们来看源码
export default {
name:'routerView',
functional:true,
render(h,{parent,data}){
// 调用render方法 说明一定是一个router-view组件
// 获取当前渲染的记录
let route = parent.$route; // this.current
console.log("data",data,parent)
let depth = 0;
// $vnode 获取的是当前节点的虚拟dom对象,给当前虚拟节点的data属性上面增加routerView属性标志为当前是routerView组件
data.routerView = true;
// App.vue中渲染组件的时候,默认会调用render函数,父亲中没有data.routerView属性
// 然后我们要找到当前是router-view组件的第几层,因为组件是从最外层的组件开始渲染的,所以找里层是第几层需要向上找它所以的父组件 看看有几个routerView,有几个routerView那么他就是第几层
while(parent){
// router-view的父标签
// $vnode代表的是占位符 组件标签名的虚拟节点 组件内部渲染的虚拟节点
// console.log('$vnode',parent.$vnode)
//
if(parent.$vnode && parent.$vnode.data.routerView){
depth ++;
}
parent = parent.$parent;
}
let record = route.matched[depth]
// console.log('current111',current)
// 第一层 router-view渲染的是第一个record 第二个 router-view渲染的是第二个record
if(!record){
return h(); // 空的虚拟节点 empty-vnode 注释节点
}
return h(record.component,data)
}
}
我们要根据当前的匹配信息渲染对应的组件,我们知道组件是从外层向里层开始渲染的,当遇到router-view组件的时候给当前虚拟节点的data属性上面增加routerView属性标志为当前是routerView组件,我们要找到当前是第几层的话就要从当前节点向上遍历遍历其所有的父节点,如果使用depth来记录父节点中有多少个routerView组件,然后渲染当前的组件。
(7)好的,从router的初始化以及根据路由将对应的组件渲染页面上已经完成,那么我们看当点击view-link,页面是如何变化的,看源码
export default {
name:'routerLink',
props:{
to:{
type:String,
required:true
},
tag:{
type:String,
default:'a'
}
},
methods:{
handler(to){
this.$router.push(to)
}
},
render(){
let {tag,to} = this;
// jsx 语法 绑定事件
return <tag onClick={this.handler.bind(this,to)}>{this.$slots.default}</tag>
}
}
在view-link中,我们点击组件调用handler方法,这个方法会调用 r o u t e r 属 性 的 p u s h 方 法 , 前 面 说 了 这 个 router属性的push方法,前面说了这个 router属性的push方法,前面说了这个router属性就是router实例,我们看push方法如何实现的
push(to){
// console.log('跳转',to)
this.history.push(to)
// this.history.transitionTo(to); // 跳转路由
}
push(location){
//
this.transitionTo(location,()=>{ // 去更新hash值,hash值变化后虽然会再次跳转 但是不会重新更新current属性
window.location.hash = location
})
}
push方法很简单,它是调用history上面的push方法,调用前面说的transitionTo方法根据路径改变current值,改变vue实例上面的_route方,然后改变地址栏上面的hash值,从而达到页面重新渲染的效果。
我们知道router路由有一些钩子函数,其中全局的钩子函数就是beforeEach,这个全局钩子是在你切换路由之前执行的方法。
router.beforeEach((from,to,next)=>{
console.log(1)
setTimeout(()=>{
next()
},1000)
})
router.beforeEach((from,to,next)=>{
console.log(2)
setTimeout(()=>{
next()
},1000)
})
这个方法是如何实现的呢我们来看源码
beforeEach(fn){
this.beforeHooks.push(fn)
}
给router实例上面增加beforeHooks属性将传递进去的所有回调函数,放到这个数组里面,我们知道这个方法是用在切换路由之前执行的,所以我们要看transitionTo方法中当确认切换路由了先不更改current以及_route值。看源码关键部分。
let queue = [].concat(this.router.beforeHooks); // 拿到了注册的方法
const iterator = (hook,next)=>{
hook(this.current,route,()=>{next()})
}
runQuene(queue,iterator,()=>{
this.updateRoute(route)
// 根据路径加载不同的组件 this.router.matcher(location) 组件
onComplete && onComplete();
})
function runQuene(queue,iterator,cb){
// 异步迭代
function step(index){
if(index>=queue.length){
return cb()
}
let hook = queue[index]
iterator(hook,()=>step(index+1))
}
step(0)
}
我们在updateRoute方法外面套了runQuene方法,
(1)看这个方法runQuene这会方法声明step方法,
(2)然后调用step方法传递0参数,在step方法中会先声明hook变量这个变量是beforeHooks数组的第一个值,
(3)然后调用iterator方法,这个方法传递两个参数一个是hook一个是()=>step(index+1)一个箭头函数,】
(4)我们再看iterator方法,这个方法会调用hook方法,然后将之前匹配的路由和现在匹配的路由以及一个箭头函数,
(5)再hook方法中会执行传入的()=>step(index+1)方法,又会调用step方法,直到遍历完成beforeHooks数组。
好了,这么说有点绕来绕去的,总结就是调用beforeHooks数组然后将之前的匹配的路由和现在匹配的路由和下一个step方法传递进去直到,beforeHooks数组执行完成。
我们分析完成了我们看打印的结果吧