什么是路由
简单来说,路由就是URL到函数的映射关系。并且路由这个概念最早是出现在后端的,因为早期的网页都是服务端渲染的,比如:JSP,PHP,ASP等语言,都是直接返回渲染好的html给客户端显示。
路由分类
后端路由
在web开发早期的年代里,前端的功能远不如现在这么强大,一直是后端路由占据主导地位。无论是jsp,还是php、asp,用户能通过URL访问到的页面,大多是通过后端路由匹配之后再返回给浏览器的。浏览器在地址栏中切换不同的URL时,每次都向后台服务器发出请求,服务器响应请求,在后台拼接html文件返回给前端,并且每次切换页面时,浏览器都会刷新页面。
在后端,路由映射表中就是不同的URL地址与不同的html + css + 后端数据 之间的映射
下面这张图或许能够让你更加清楚的了解后端路由:
后端路由缺点:每次在浏览器中输入url都要发送请求到服务器,服务器解析请求路径,返回对应的页面给前端。造成服务器压力比较大。
前端路由
前端路由是后来发展到SPA(单页应用)时才出现的概念。
前端路由主要是有两种模式:
- hash模式:带有hash的前端路由,优点是兼容性高。缺点是URL带有#号不好看
- history模式: 不带hash的前端路由,优点是URL不带#号,好看。缺点是既需要浏览器支持也需要后端服务器支持。否则会出现404。
前端路由应用最广泛的例子就是当今的SPA的web项目。不管是Vue、React还是Angular的页面工程,都离不开相应配套的router工具。
整个SPA页面就只有一整套的HTMLCSS+JS,这一套HTML+CSS+JS中包含了许多个网页,当我们请求不同的URL时,客户端会从这一整套HTML+CSS+JS中找到对应的HTML+CSS+JS,将它们解析执行,渲染在页面上。
前端路由带来的最明显的好处就是,地址栏URL的跳转不会白屏了------这也得益于客户端渲染带来的好处。
vue-router就是vue框架实现前端路由的方法,接下来就开始研究vue-router实现前端路由的原理。
Vue插件注册
插件功能
- 添加全局指令、全局过滤器、全局组件
- 通过全局混入来添加一些组件选项(例如:vue-router会通过Vue.mixin给全局混入一些生命周期和销毁的生命周期,同样在原型上增加一些属性)
- 添加Vue实例方法,通过把它们添加到Vue.prototype上实现
插件注册原理
在Vue中通过Vue.use()方法注册插件。Vue编写插件的时候通常要提供静态的install方法。插件的写法有以下两种:如果插件是一个对象,必须提供install方法,如果插件是一个函数,它会被当作install方法。
js
let plugin1 = { install(_Vue,...args) {} }
let plugin2 = (_Vue,...args) => {}
Vue.use(plugin1,1,2,3)
Vue.use(plugin2,1,2,3)
通过分析use方法的源码,我们可以理解插件的注册原理。
js
Vue.use = function(plugin:Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins=[])) if(installedPlugins.indexOf(plugin) > -1) {
return this
}
const args = toArray(arguments,1) // 拿到plugin以外的参数 args.unshift(this)
if(typeof plugin.install === 'function') {
// 如果有install方法则执行,并将参数传入
plugin.install.apply(plugin,args)
} else if (typeof plugin === 'function') {
// 如果plugin本身就是一个方法,那么直接执行plugin并将参数传入
plugin.apply(null, args) }
installedPlugins.push(plugin)
return this
}
首先会判断,该插件是否已经注册过了。如果已经注册过的插件那就不执行下面的注册逻辑。如果之前没有注册过该插件,首先通过toArray(arguments,1)方法拿到传入use方法中plugin本身以外的参数,并把Vue实例到参数的最前面(args.unshift(this)),那么install接收到的第一个参数就是Vue,为什么要在install方法执行的时候传入Vue呢?这是因为不可能在插件中再去通过import Vue引入(可能会造成版本不一致),通过参数传递的这种方式就可以让插件拿到Vue,因为插件里面会用到Vue的一些方法。然后判断插件中是否存在install方法,如果有install方法则执行,并将参数传入。如果没有install方法但是plugin本身就是一个方法,那么直接执行plugin并将参数传入。并将插件存入到installedPlugins中缓存插件。这样就完成了插件的注册。
路由注册
路由注册的实现流程
上面分析了Vue注册插件的过程,现在看下对于vue-router而言插件方法是怎么定义的。vue-router的具体使用方法如下:
js
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/foo',
name: 'foo',
component:Foo,
children: {
path: '/bar',
name: 'bar'
component: Bar
}
}
]
const router = new VueRouter({ routes })
const app = new Vue({ el:'#app', render(h) { render h(App) }, router })
也就是我们在执行Vue.use(VueRouter)的时候,就会去执行VueRouter.install方法。
js
let _Vue
export function install(Vue) {
if(install.installed && _Vue === Vue)
return install.installed = true
_Vue = Vue Vue.mixin({
beforeCreate() {
if(isDef(this.$options.router)) {
this._routerRoot = this this._router = this.$options.router this._router.init(this) // 把_route变成响应式的 Vue.util.defineReactive(this,'_route', this._router.history.current)
} else {
// 非根组件
this._routerRoot=(this.$parent && this.$parent._routerRoot) || this }
// 这样每个组件都可以通过this._routerRoot访问到根的实例
registerInstance(this,this) //
},
destoryed() { registerInstance(this) } })
Object.defineProperty(Vue.prototype,'$router',
{ get() { return this._routerRoot._router } }
)
Object.defineProperty(Vue.prototype,'$route',{
get() {return this._routerRoot._route} }
)
Vue.component('RouterView',View) // 全局注册router-view组件
Vue.component('RouterLink',Link) // 全局注册router-link组件 }
第一次注册路由的时候会把installed置为true,且把传入到install方法中的Vue赋值给_Vue。如果多次执行路由的注册会直接return。最关键的是通过Vue.mixin方式混入了一个beforeCreate和destoryed的钩子函数。这是整个路由最关键的地方。Vue.mixin的作用其实就是mergeOptions,也就是把mixin方法拓展到全局的options。这样的话每一个组件都会有一个beforeCreate和destoryed钩子函数。beforeCreate首先会判断this. <math xmlns="http://www.w3.org/1998/Math/MathML"> o p t i o n s . r o u t e r 是否为 t r u e , 也就是说我们在使用路由的时候,最终我们会实例化一个 n e w V u e R o u t e r 的 r o u t e r 对象。然后在 n e w V u e 的时候会把 r o u t e r 对象传入。一旦我们把 r o u t e r 传入后,我们就可以在 t h i s . options.router是否为true,也就是说我们在使用路由的时候,最终我们会实例化一个new VueRouter的router对象。然后在new Vue的时候会把router对象传入。一旦我们把router传入后,我们就可以在this. </math>options.router是否为true,也就是说我们在使用路由的时候,最终我们会实例化一个newVueRouter的router对象。然后在newVue的时候会把router对象传入。一旦我们把router传入后,我们就可以在this.options上获取到router对象。也就是this.$options.router为true的条件成立。然后做一些私有属性定义和路由初始化工作,即执行插件上面的init方法。通过Vue.util.defineReactive方法把_route变成响应式的。最后调用registerInstance方法。这些方法具体内容后面介绍。然后会全局注册router-view组件和router-link组件。这就是为什么我们在注册了路由后就可以随意使用router-view组件和router-link组件了。router-view组件会渲染相关组件,router-link组件会渲染成一个a标签。
VueRouter对象
VueRouter是一个class,有一些静态的方法和属性。也有一些实例上的属性。当我们执行new VueRouter的时候就会去执行constructor
js
constructor(options:RouterOptions={}) {
this.app = null this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHookes = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
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
switch(mode) {
// 根据不同的mode对this.history进行实例化,对路由进行管理
case 'history':
this.history = new HTML5History(this,options.base)
break
case 'hash':
this.histroy = 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') } } }
options就是我们传入的一些配置。beforeHooks、resolveHookes、afterHooks这是跟导航守卫相关的,之后会介绍。对传入的mode(路由的模式)进行处理。浏览器环境中通常使用hash或者history路由。在非浏览器环境中使用抽象abstract路由。history路由其实是利用了html5的一个特性,并不是所有浏览器都支持,即!supportsPushState的情况下(特定浏览器、浏览器是否支持window.history且history上存在pushState方法)把mode设置降级为hash模式。根据不同的mode对this.history进行实例化(new HTML5History、new HashHistory、new AbstractHistory),对路由进行管理。
js
function supportsPushState = inBrowser & (
function() {
const ua = window.nagivator.userAgent
if((ua.indexOf('Android 2.') !== -1) || us.indexOf('Android 4.0') !== -1)
&& ua.indexOf('Mobile Safari') !== - 1 && ua.indexOf('Chrome') === -1
&& ua.indexOf('Windows Phone') === -1 ){
return false
}
return window.history && 'pushState' in window.history })()
初始化路由
路由初始化的时机是在组件的初始化阶段,执行到beforeCreate钩子函数的时候会执行router.init方法。init是class VueRouter原型上的方法,把每个组件实例传入到init方法中。保存到this.apps这个数组中。然后拿到history对象。判断是HTML5History还是HashHistory,执行不同的逻辑。
js
init(app) {
this.apps.push(app)
if(this.app) { return }
this.app = app
const history = this.history // 拿到history
if (history instanceof HTML5History || history instanceof HashHistory) {
var handleInitialScroll = function (routeOrError) {
var from = history.current;
var expectScroll = this$1$1.options.scrollBehavior;
var supportsScroll = supportsPushState && expectScroll;
if (supportsScroll && 'fullPath' in routeOrError) {
handleScroll(this$1$1, routeOrError, from, false);
}
};
var setupListeners = function (routeOrError) {
history.setupListeners();
handleInitialScroll(routeOrError);
};
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
);
}
history.listen(route => {
this.apps.forEach(
app => {
app._route = route
}
)
})
}
然后会执行history.transitionTo方法做路径切换。首先会通过执行this.router.match拿到路径route,然后执行confirmTransition。具体实现后续介绍。即路由在初始化的时候就会去调用histroy.transitionTo方法做路由过渡。
js
History.prototype.transitionTo = function transitionTo ( location, onComplete, onAbort ) {
var this$1$1 = this;
var route;
try {
route = this.router.match(location, this.current);
} catch (e) {
this.errorCbs.forEach(function (cb) { cb(e); }); // Exception should still be thrown throw e
}
var prev = this.current;
this.confirmTransition(
route,
function () {
this$1$1.updateRoute(route);
onComplete && onComplete(route);
this$1$1.ensureURL();
this$1$1.router.afterHooks.forEach(
function (hook) {
hook && hook(route, prev);
}
); // fire ready cbs once
if (!this$1$1.ready) {
this$1$1.ready = true;
this$1$1.readyCbs.forEach(function (cb) { cb(route); });
}
},
function (err) {
if (onAbort) { onAbort(err); }
if (err && !this$1$1.ready) {
if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
this$1$1.ready = true;
this$1$1.readyErrorCbs.forEach(function (cb) { cb(err); });
}
}
}
);
};
matcher
addRoutes函数
在实例化router的过程中会执行createMatcher函数(最终就是创建一个路由映射表),并将返回结果赋值给this.matcher。传入的就是我们定义的路由routes,即path到组件的映射关系,以及子路由的描述等。最终会返回一个对象{match,addRoutes}。addRoutes其实是vue-router提供的一个能力。可以动态的去添加路由配置。通常情况下我们会写死路由,但有些情况下是从后端动态下发的路由。就可以通过addRoutes函数再次执行createRouteMap,就会在原来的路由映射基础上做添加。这也是vue-router设计比较巧妙的地方。
js
function createMatcher(routes,router) {
const {pathList,pathMap,nameMap} = createRouteMap(routes)
function addRoutes(routes) {
createRouteMap(routes,pathListh,pathMap,nameMap) }
function match(raw,currentRoute,redirectedFrom){ }
reutrn { match, addRoutes }
}
首先通过createRouteMap方法创建路由映射表,定义pathList、pathMap、nameMap三个变量,优先从参数中去取。参数中没有传入的话就赋一个默认值([]或者Object.create(null))。然后对路由配置做遍历,执行addRouteRecord方法。如果路径是通配符,会调整到结尾,即优先级最低。最终返回pathList、pathMap、nameMap组成的对象。
js
function createRouteMap(routes,oldPathList?,oldPathMap?,oldNameMap?) {
const pathList = oldPathList || []
const pathMap = oldPathMap || Object.create(null)
const nameMap = oldNameMap || Object.create(null)
routes.forEach(route => {
addRouteRecord(pathList,pathMap,nameMap,route)
})
for(let i = 0; l = pathList.length;i<l;i++) {
if(pathList[i]==='*') { // 如果路径是通配符,会调整到结尾,即优先级最低 pathList.push(pathList.splice(i,1)[0]) l-- i--
}
}
return { pathList, pathMap, nameMap } }
addRouteRecord首先从路由定义中拿到path和name(路由是可以取名称的)。会限制path不能为空,同时传入的component不是string类型的,必须是一个真实的组件。pathToRegexpOptions是路径的正则匹配。通过normalizePath根据当前传入的path、parent、strict拼接成最后的path。如果是非strict,那么会把结尾的'/'替换为空。如果path[0]是'/',说明是一个绝对路径直接返回path。如果没有parent直接返回path。否则会根据parent.path和path进行拼接。
然后声明一个record变量重新描述路由,为之后的路径匹配提供比较好的依据。如果配置了子路由children,就会去遍历children。递归调用addRouteRecord创建record。如果pathList不存在record.path,就将record.path存到pathList中,并将record.path作为key,record作为value添加到pathMap中。如果传入了name,那么nameMap也会去记录这样一个record。这样就构造出完整的路由映射表。之后就能很方便的根据path或者name查到对应的record。
js
function addRouteRecord(pathList,pathMap,nameMap,route,parent?,matchAs?) {
const {path,name} = route
const pathToRegexpOptions = route.pathToRegexpOptions || {}
const normalizedPath = normalizePath( path, parent, pathToRegexpOptions.strict ) if(typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
const record = { // 对传入路由配置项重新做规范
path: normalizedPath,
regex: compileRouteRegex(normalizedPath,pathToRegexpOptions), // 正则表达式 components: route.components || {default: route.component}, // 传入的component instances: {},
name,
parent,
matchAs,
redirect: route.redirct,
beforeEnter: router.beforeEnter,
meta:route.meta || {},
props: route.props == null ? {}:route.components?route.props:{default:route.props} } if(route.children) { // 嵌套路由,递归
route.children.forEach(child => {
const childMatchAs = matchAs ? cleanPath(`${matchAs}/${child.path}`) : undefined addRouteRecord(pathList,pathMap,nameMap,child,record,childMatchAs) }) } if(!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
if(name) {
if(!nameMap[name]) {
nameMap[name] = record
}
}
}
function normalizePath(path,parent?,strict?) {
if(!strict) path = path.replace(/\/$/,'') // 把结尾的'/'去掉
if(path[0]==='/') return path
if(parent == null) return path
return cleanPath(`${parent.path}/${path}`) // 将parent.path跟path进行拼接
}
最终的pathMap结果如下:
nameMap结果如下:
match函数
作用:根据我们传入的路径raw和当前路径currentRoute计算出新的路径,并匹配到对应的路由record,然后根据新的位置和record创建新的路径并返回。
初始化时候this.current如下:
执行transitionTo时候,通过this.router.match得到路径route。比如当前路由是'/foo',那么location就是'/foo'。
js
var route = this.router.match(location, this.current);
在match函数中首先通过normalizeLocation对即将跳转的路径做处理,处理的结果location的结构如下:
此时满足没有name,但是有path的情况。然后通过pathMap[path]找到对应的record。找到的record如下:
判断location的path和record的regex是否匹配上。此时匹配上调用 _createRoute,实际就会调用createRoute函数。结果如下:
即通过match函数后拿到的route就是上面的结构。matched就是匹配的路由信息。
js
function match(raw,currentRoute?,redirectedFrom?) {
const location = normalizeLocation(raw,currentRoute,false,router)
const {name} = location
if(name) {
const record = nameMap[name]
if(!record) return _createRoute(null, location)
if(record) {
// 如果有record,对query参数做一些处理
if(currentRoute && typeof currentRoute.params === 'object') {
for(const key in currentRoute.params) {
if(!(key in location.params)) {
location.params[key] = currentRoute.params[key]
}
}
}
}
if(record) {
// 生成path,然后执行_createRoute
location.path = fillParams(record.path,location.params,`named route "${name}"`) return _createRoute(record,location,redictedFrom)
}
} elseif(location.path) {
// 没有name,但是有path的话。
loaction.params = {} // 通过path找到对应的
record for(let i = 0; i< pathListh.length;i++) {
const path = pathList[i]
const record = pathMap[path]
// 判断location的path和record的regex是否匹配上。匹配上就调用 _createRoute
if(matchRoute(record.regex,location.path,location.params)) {
return _createRoute(record,location,redirectedFrom)
}
}
}
// 都没有时
return _createRoute(null, location) }
js
function normalizeLocation(raw,current,append?,router?) {
let next = typeof raw === 'string' ? {path:raw}: raw
if(next.name || next._normalized) return next
if(!next.path && next.params && current) {
next = assign({}, next)
next._normalized = true
const params = assign(assign({},current.params),next.params)
if(current.name) {
next.name = current.name next.params = params
} elseif (current.matched.length) { }
const parsedPath = parsePath(next.path || '') // 对路径做解析
const basePath = (current && current.path) || '/'
const path = parsedPath.path ? resolvePath(parsedPath.path,basePath,append || next.append) :basePath // 算出一个新的path
const query = resolveQuery( parsedPath.query, next.query, router && router.options.parseQuery ) // 得到一个新的query
let hash = next.hash || parsedPath.hash
if(hash && hash.charAt(0) !== '#') {
hash = `#${hash}
}
return {
_normalized: true,
path,
query,
hash
}
}}
normalizeLocation的功能可以看下下面的两个测试案例
即如果输入'/abc?foo=bar&baz=qux#hello',通过normalizeLocation后返回 return {
_normalized: true,
path:'/abc',
query: {foo:'bar',baz:'qux'}
hash: '#hello'
}
如果有name就会去nameMap中获取对应的record。同样执行_createRoute生成以上结构的route。如果不存在这个record的话就执行_createRoute(null,location)。即没有与之匹配的路由,此时的结构如下:其中matched是[]。
路径切换
路径切换是整个路由中最核心的环节,因为它关系到导航守卫的管理、url的变化、视图的渲染。在路由初始化的时候会执行history.transitionTo(history.getCurrentLocation())。这个就是路径切换的方法,路径切换的入口还有就是push和replace API的执行,实际上也会去执行transitionTo方法。transitionTo是所有history基类的实现。transitionTo从api设计上功能是跳转到一个新的地址。并且有跳转成功和失败的回调。首先调用this.router.match根据跳转的路径和当前的路径算出一个新的路径。然后执行this.comfirmTransition方法进行真正的路径切换。
js
transitionTo(location,onComplete,onAbort) {
const route = this.router.match(location,this.current)
this.confirmTransition(route,
() => {
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
if(!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {cb(route)})
}
},
err =>{
if(onAbort) {
onAbort(err)
}
if(err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => {cb(err)})
}
})
}
在上面已经通过match函数拿到了相关的路由信息route。就会去执行this.confirmTransition进行路由切换。关键就是去执行this.updateRoute(route)。更新_route的值触发组件的渲染逻辑,下面会分析到。
导航守卫的执行逻辑
导航守卫可以理解为在我们执行路径切换的时候执行一些钩子函数。 完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫
- 调用全局的 beforeEach 守卫
- 在重用的组件里调用 beforeRouteUpdate 守卫
- 在路由配置里调用 beforeEnter
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter
- 调用全局的 beforeResolve 守卫
- 导航被确认。
- 调用全局的 afterEach 钩子
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
添加全局导航守卫
js
// 全局导航守卫
const router = new VueRouter({ routes })
router.beforeEach((to,from,next) => {
console.log('global before each')
next()
})
router.afterEach((to,from,next) => {
console.log('global after each')
next()
})
在路由配置中增加导航守卫
js
const routes = [
{
path: '/foo',
name: 'foo',
component: Foo,
beforeEnter(to,from,next) {
console.log('foo before enter')
next()
}
children: [
{
path: '/child',
component: Bar
}
]
},
{
name: 'bar',
path: '/bar',
component: Bar,
beforeEnter(to,from,next) {
console.log('bar before enter')
next()
}
}]
给组件添加导航守卫
js
const Foo = {
template: `<div>foo</div>`,
beforeRouteEnter(to, from, next) {
next((vm) => { console.log(vm)
})
},
beforeRouteUpdate(to, from, next) {
console.log('foo beforeRouteUpdate')
next()
},
}
导航守卫的逻辑是在confirmTransition函数中执行的,首先通过resolveQueue拿到updated、deactivated、activated三个数组。
js
var ref = resolveQueue( this.current.matched, route.matched );
var updated = ref.updated;
var deactivated = ref.deactivated;
var activated = ref.activated;
比如我从'/foo'切换到'/bar',上面三个数组的值如下:即当前激活的组件是activated,失活的组件是deactivated,updated为空。后选会根据这3个数组去执行相关的导航守卫。
接下来定义一个queue,把所有导航守卫拆到一个队列中,这个队列依次执行。每执行完一个就调用next执行队列中的下一个。直到执行完毕。
js
var queue = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(function (m) { return m.beforeEnter; }),
// async components
resolveAsyncComponents(activated)
);
调用runQueue执行队列中的任务,runQueue就是取出queue中的任务,放入到iterator去执行相应的hook。参数route、current、function(to) 分别表示导航守卫中的to、from、next。在fuction(to)中最后会执行next(to),也就是step(index + 1)。即执行下一个hook。
js
runQueue(queue, iterator, function () {
// wait until async components are resolved before
// extracting in-component enter guards
var enterGuards = extractEnterGuards(activated);
var queue = enterGuards.concat(this$1$1.router.resolveHooks);
runQueue(queue, iterator, function () {
if (this$1$1.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this$1$1.pending = null;
onComplete(route);
if (this$1$1.router.app) {
this$1$1.router.app.$nextTick(function () { handleRouteEntered(route); });
}
});
});
js
var iterator = function (hook, next) {
if (this$1$1.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
try {
hook(route, current, function (to) {
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this$1$1.ensureURL(true);
abort(createNavigationAbortedError(current, route));
} else if (isError(to)) {
this$1$1.ensureURL(true); abort(to);
} else if ( typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string')) ) {
// next('/') or next({ path: '/' }) -> redirect abort(createNavigationRedirectedError(current, route));
if (typeof to === 'object' && to.replace) {
this$1$1.replace(to);
} else {
this$1$1.push(to);
}
} else {
// confirm transition and pass on the value next(to); } });
} catch (e) { abort(e); }
js
function runQueue (queue, fn, cb) {
var step = function (index) {
if (index >= queue.length) {
cb();
} else {
if (queue[index]) {
fn(queue[index], function () { step(index + 1); });
} else { step(index + 1); }
}
};
step(0);
}
接下来看都有哪些导航守卫,以及它们执行的逻辑。
首先会执行extractLeaveGuards方法,把deactivated作为参数传入。调用失活组件中beforeRouteLeave的导航守卫。
js
function extractLeaveGuards (deactivated) {
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
function extractGuards ( records, name, bind, reverse ) {
var guards = flatMapComponents(records, function (def, instance, match, key) {
var guard = extractGuard(def, name);
if (guard) {
return Array.isArray(guard) ? guard.map(function (guard) {
return bind(guard, instance, match, key); })
: bind(guard, instance, match, key) } });
return flatten(reverse ? guards.reverse() : guards)
}
function extractGuard ( def, key ) {
if (typeof def !== 'function') { // extend now so that global mixins are applied.
def = _Vue.extend(def);
}
return def.options[key]
}
通过bind去执行组件上的beforeRouteLeave函数。
上面hook执行完后调用next()执行 this.router.beforeHooks。当我们添加全局导航守卫的时候调用router.beforeEach就会去调用registerHook,把beforeEach接收的函数push到this.beforeHooks中。
js
VueRouter.prototype.beforeEach = function beforeEach (fn) {
return registerHook(this.beforeHooks, fn)
};
function registerHook (list, fn) {
list.push(fn);
return function () {
var i = list.indexOf(fn);
if (i > -1) {
list.splice(i, 1);
}
}
}
第三步执行 extractUpdateHooks(updated),跟extractLeaveGuards(deactivated)类似。会拿到组件定义的beforeRouteUpdate函数并执行。
第四步执行activated.map(function (m) { return m.beforeEnter; }),即执行路由中定义的beforeEnter
第五步解析异步组件resolveAsyncComponents(activated)。即通过import方式引入的组件。
js
{
name: 'bar',
path: '/bar',
component: () => import('./Bar.vue'),
beforeEnter:(to,from,next) =>{
console.log('bar before enter')
next()
}
}
resolveAsyncComponents也是返回类似导航守卫的函数,如果路由配置的时候component设置为一个函数,通过import引入组件,那么def是函数且def.cid === undefined。就说明当前是异步组件。执行def加载组件,加载完成后执行resolve函数。把加载的结果重新赋值给match.components。然后执行next(),执行后续逻辑。
js
function resolveAsyncComponents (matched) {
return function (to, from, next) {
var hasAsync = false;
var pending = 0;
var error = null;
flatMapComponents(matched, function (def, _, match, key) {
if (typeof def === 'function' && def.cid === undefined) {
hasAsync = true;
pending++;
var resolve = once(function (resolvedDef) {
if (isESModule(resolvedDef)) {
resolvedDef = resolvedDef.default;
}
def.resolved = typeof resolvedDef === 'function' ? resolvedDef : _Vue.extend(resolvedDef);
match.components[key] = resolvedDef;
pending--;
if (pending <= 0) { next(); }
});
}
}
var res;
try {
res = def(resolve, reject);
} catch (e) { reject(e); }
if (res) {
if (typeof res.then === 'function') {
res.then(resolve, reject);
} else { // new syntax in Vue 2.3
var comp = res.component;
if (comp && typeof comp.then === 'function') {
comp.then(resolve, reject);
} } }
}
其他导航守卫执行过程类似。
url的变化逻辑
当点击link-router时候,触发history.push方法。
如果是hash模式,通过pushHash方法修改页面上的hash值。
js
HashHistory.prototype.push = function push (location, onComplete, onAbort) {
var this$1$1 = this;
var ref = this;
var fromRoute = ref.current;
this.transitionTo( location, function (route) {
pushHash(route.fullPath);
handleScroll(this$1$1.router, route, fromRoute, false);
onComplete && onComplete(route);
},
onAbort
);
};
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path));
} else {
window.location.hash = path;
} }
如果是history模式,就会触发HTML5Histroy.push方法,通过pushState方法调用window.history.pushState或者window.history.replaceState在历史记录里面添加一条新记录或者替换当前记录。
js
HTML5History.prototype.push = function push (location, onComplete, onAbort) {
var this$1$1 = this;
var ref = this;
var fromRoute = ref.current;
this.transitionTo(location, function (route) {
pushState(cleanPath(this$1$1.base + route.fullPath));
handleScroll(this$1$1.router, route, fromRoute, false);
onComplete && onComplete(route); },
onAbort);
};
function pushState (url, replace) {
saveScrollPosition();
var history = window.history;
try {
if (replace) {
var stateCopy = extend({}, history.state);
stateCopy.key = getStateKey();
history.replaceState(stateCopy, '', url);
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url);
}
} catch (e) { window.location[replace ? 'replace' : 'assign'](url); } }
通过上面的操作就会触发hash或者history路由变化的监听函数
路由变化监听
在init方法中会执行history.transitionTo,完成后执行setupListeners监听url的变化。
js
var setupListeners = function (routeOrError) {
history.setupListeners();
handleInitialScroll(routeOrError);
};
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
);
如果是hash模式的路由就监听hashchange事件,如果是histroy模式的路由那么就监听popstate事件。路由发生改变执行handleRoutingEvent事件处理函数,又会去执行transitionTo函数更新_route,触发视图重新渲染,渲染新的组件。
js
HashHistory.prototype.setupListeners = function setupListeners () {
var this$1$1 = this;
if (this.listeners.length > 0) { return }
var router = this.router;
var expectScroll = router.options.scrollBehavior;
var supportsScroll = supportsPushState && expectScroll;
if (supportsScroll) {
this.listeners.push(setupScroll());
}
var handleRoutingEvent = function () {
var current = this$1$1.current;
if (!ensureSlash()) { return }
this$1$1.transitionTo(getHash(), function (route) {
if (supportsScroll) {
handleScroll(this$1$1.router, route, current, true);
}
if (!supportsPushState) {
replaceHash(route.fullPath);
}
});
};
var eventType = supportsPushState ? 'popstate' : 'hashchange'; window.addEventListener( eventType, handleRoutingEvent );
this.listeners.push(function () {
window.removeEventListener(eventType, handleRoutingEvent);
});
};
组件的渲染逻辑
上面提到在每个组件渲染之前会执行beforeCreate方法,会设置响应式数据'_route'
js
Vue.util.defineReactive(this, '_route', this._router.history.current); // 数据响应式,_route变化触发视图重新更新
并通过Object.defineProperty监听 <math xmlns="http://www.w3.org/1998/Math/MathML"> r o u t e ,当获取 route,当获取 </math>route,当获取route时候触发其get函数,返回上面的响应式数据'_route'
js
Object.defineProperty(Vue.prototype, '$route', {
get: function get () { return this._routerRoot._route }
});
自定义全局组件router-view拿到$route也就拿到了响应式数据'_route'。然后在matched中去寻找匹配的component,通过h函数进行渲染。当通过router-link切换路由的时候,会调用this.transitionTo然后调用update(route),就会去更新app._route的值。触发视图重新更新。router-view根据新的_route值获取需要渲染的组件。
js
History.prototype.listen = function listen (cb) { this.cb = cb; };
在初始化的时候执行history.listen,对this.cb进行赋值。
js
history.listen(function (route) {
this$1$1.apps.forEach(function (app) {
app._route = route;
});
});
调用updateRoute时候会执行this.cb,更新app._route的值,重新渲染视图。
js
History.prototype.updateRoute = function updateRoute (route) {
this.current = route;
this.cb && this.cb(route);
};