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