vue-router
vue分析的差不多了,那么我们来看看最重要的两个周边产品,先看vue-router。这块的例子直接使用vue-router官网的例子就行了, 如果用vue-cli新建的项目,记得在vue.config.js中配置runtimeCompiler为true。
初始化
在vue-router的例子中我们可以看到,首先通过Vue.use注册router,然后new一个实例,并传入路由配置数组。那么我们就可以从这两个方面入手
首先是Vue.use,该方法全局方法,可以在global-api/use.js查看,就是调用了install方法,传入了当前Vue。 所以vue-router其实就是vue插件的调用方法,那么我们直接看router的install方法。
install方法比较长,但是总结起来也就五点
- 确保只
install一次,即多次install只会缓存第一次的Vue - 声明
registerInstance方法 - 使用
Vue.mixin混入beforeCreate和destroyed,这里使用了mergeOptions,并且混入的是Vue所以在每个组件上都会执行 - 定义
Vue.prototype.$router和Vue.prototype.$route的拦截器,指向this._routerRoot.xx。 - 注册
RouterView和RouterLink组件
install的核心就这些,接下来要看new VueRouter的过程。
它也可以总结为几个点
- 定义实例变量
- 执行
createMatcher该方法是创建路由映射和获取路由方法的核心 - 根据
mode实例化history,这里注意在使用history模式的时候,如果浏览器不支持router会自动降级为hash
createMatcher
这个方法传入两个参数一个是routes一个是当前实例,然后我们看下源码
export function createMatcher(routes, router) {
// 创建一个路由映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)
...
return {
match,
addRoute,
getRoutes,
addRoutes
}
}
2
3
4
5
6
7
8
9
10
11
就做了两件事
- 通过
createRouteMap创建路由映射表 - 返回
route相关方法和match方法,match方法很重要,等调用的时候再提
根据流程,我们进入createRouteMap方法看看,这个方法核心就一段代码
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})
2
3
我们进入addRouteRecord看看
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
const pathToRegexpOptions: PathToRegexpOptions =
route.pathToRegexpOptions || {}
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
// record 对象 是路由核心的描述
const record: RouteRecord = {...}
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
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
该方法主要就是循环执行一个route对象,然后序列化成vue-router需要的record对象,然后将其存入pathMap、pathList中,如果route中定义了 name在nameMap中也保存一份,这里主要注意两个地方
- 命名视图,这个功能可以看文档介绍,即路由对象中可以写
components: {},如果定义component在源码中也是components: { default: route.component } normalizePath方法在嵌套路由时候返回的差别,如果是子级路由,返回的就是/foo/bar
这样初始化就完成了。
如何开始执行的?
别忘记我们在new Vue的时候需要传入router实例,而且在之前通过Vue.mixin混入了beforeCreate和destroyed。 那么很显然在运行到beforeCreate的时候,就会执行。我们进入这个方法看看
beforeCreate: function 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)
},
2
3
4
5
6
7
8
9
10
11
因为在new Vue中传入了router实例,所以我们会走if分支,else是做什么的?当我们执行到组件的 beforeCreate的时候,当前实例就不存在router了。赋值好变量之后,就是执行init方法和_route响应式
这里先不说init,我们先看defineReactive,为什么要对_route做响应式,可以看到this._route会返回this._router.history.current也就是当前路由。 而之前我们看到两个拦截器,this.$route返回的是this._routerRoot._route,而根据之前的定义this._routerRoot = this,那么很显然了 this.$route其实就是返回的current当前路由,知道了这个我们之后再回来看。
在init方法中,会第一次调用history.transitionTo。那么在解析transitionTo之前,我们需要先去history的定义。
本例子使用的hash模式,那么我们就分析HashHistory,在history文件夹中几种模式都是用的class定义,并且都extend了History类,那么直接进去看 History中的构造函数,可以发现其实就是定义类一些属性和方法,因为太简单了,这里就不赘述了。
我们看init方法
init (app) {
this.apps.push(app)
if (this.app) {
return
}
this.app = app
const history = this.history
if (history instanceof HTML5History || history instanceof HashHistory) {
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这里稍微讲解一下,通过判断使后面的内容只会在new Vue的时候执行一次。而我们主要要了解的就是切换路由的transitionTo方法。
transitionTo
transitionTo定义在history/base.js中,该方法其实就关注两点
- 通过
this.router.match获取计算后的路由 - 通过
this.confirmTransition切换路由
先看match,在router类中可以看到,match其实调用的就是matcher.match。
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute, false, router)
if (location.path) {
location.params = {}
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i]
const record = pathMap[path]
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
// no match
return _createRoute(null, location)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
因为例子中没有定义name,所以相关代码先删除。先看normalizeLocation方法,该方法是对当前router做了一次计算,在这里我们可以不去看源码实现, 看看他的单元测试,在location.spec.js中,很明显就是对location进行了序列化,然后通过matchRoute去和之前的pathList匹配,初始化的话就会 直接生成route。
初始化的route我们知道就是/。完成第一步,我们看第二步调用confirmTransition
该方法很长,但是也能分成四块,(这里建议自己去打上断点分析,方法和异步组件分析一致)
- 通过
resolveQueue方法解析出回调 - 生成
queue - 声明
iterator函数 - 并且执行
runQueue
这里重点提一下:runQueue是一个异步函数的队列化执行函数
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step = index => {
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
我们观察他的入参,queue是之前生成的一位函数队列,是一个定义的导航队列,fn是传入的iterator,cb是定义好的回调函数, 当我们执行完队列会调用cb进行首次渲染的结束,fn的第二个参数是下一个步进器,显然就是传入的next。
查看iterator方法的入参(hook: NavigationGuard, next),非常明显了,执行hook,然后在结束后执行next到下一个。 这也就是为啥每次导航守卫需要next原因。
稍微了解了这些,我们就可以结合官网说明的导航解析流程分析
- 在失活的组件里调用
beforeRouteLeave守卫。 - 调用全局的
beforeEach守卫。 - 在重用的组件里调用
beforeRouteUpdate守卫 (2.2+)。 - 在路由配置里调用
beforeEnter。 - 解析异步路由组件。
首先在首次渲染中,是没有失活组件的,那么直接跳过。beforeEach将被触发。触发完成后注意runQueue的第三个参数
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
2
3
4
5
6
我们看其中的这一段代码,首先执行了onComplete,在该方法中对route做了一些处理,并且执行了beforeEnter,这两个都是全局路由钩子 这样首次渲染的路由钩子就执行完了。然后执行了$nextTick。
两个函数组件
这里主要看路由跳转时,路由守卫的执行过程,其他的建议自己debugger尝试
router-link
router-link组件应该就是a组件的包装,当我们点击的时候,其实就是触发点击事件,然后去执行router.push()。看render部分的相关源码
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location, noop)
} else {
router.push(location, noop)
}
}
}
const on = { click: guardEvent }
data.on = on
data.attrs = { href, 'aria-current': ariaCurrentValue }
2
3
4
5
6
7
8
9
10
11
12
在push方法内部,执行的还是transitionTo执行过程和之前分析的一样,重点还是看queue、runQueue、iterator这个三执行的过程。
在这里我们会再次执行全局前置路由beforeEach,然后调用路由配置中定义的beforeEnter,之后就是执行异步组件。执行完queue队列就执行完了,那么就和上面一样,执行 runQueue定义的cb
const enterGuards = extractEnterGuards(activated)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
在该方法的执行过程中,我们将拿到了路由中定义的组件对象,进行Vue.extend。所以可以看到,路由上的组件其实是全局混入的。并且返回了routerEnter,也就是说我们在之后执行的 runQueue中,必然有beforeRouteEnter钩子,但是当前组件没有被实例化,所以我们拿不到this。在onComplete方法中,将完成url的更新和全局后置路由afterEach的执行。
那为什么next中可以访问到this呢
首先根据runQueue可以知道,next其实就是执行了下一步,如果queue执行完了,就是执行cb,也就是runQueue的第三个参数,在这里,可以看到$nextTick。重新渲染组件, 在这个过程中render函数中有_c("router-view"),因此我们会运行到router-view组件的创建过程。
router-view
这是一个函数式组件,如果不明白可以查看官网对于functional组件的定义。函数式组件主要看render函数,在render函数中有这么一段代码
const route = parent.$route
之前已经说过,this.$route会进行一次依赖收集,收集的就是当前的route。并且会缓存当前组件。新增data.hook.init和data.hook.prepatch。最后就是执行h函数渲染, 这里component就是路由配置中的组件,在组建执行过程中,会执行data.hook.init,而在这个方法中会执行handleRouteEntered。 在这个方法中会执行之前保存的cbs,也就是next的回调。也就是说这里可以访问到组件的this了。
这样路由所有组件的首次渲染就完成了,还有一个点
在被激活的组件里调用 beforeRouteEnter
当我们再次切换到已经初始化过的组件中时,这时候resolveQueue解析出来的就存在updated,也就是说会执行beforeRouteLeave钩子。
总结
vue-router使用Vue.use的方式注册,通过Vue.mixin混入beforeCreate,在其中执行init方法- 如果浏览器不支持
history模式会自动降级,并且所有的路由跳转最终都是transitionTo方法的调用 - 通过类似
generator函数的runQueue执行钩子函数队列queue。在runQueue的最后执行全局afterEach和nextTick