「这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战」
前言
最近抽时间学习了vueRouter源码,基本也就是走马观花式地看了一遍。虽然很多细节和原理没有去深入分析,但还是想通过博客记录下自己从中学习到的点滴。
history
history实现了路由切换及守卫函数执行的逻辑,我们一起看看其中关键逻辑。
构造函数
比较简单,就是初始化了一些实例属性
// router实例
this.router = router
// base的处理
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
this.listeners = []
listen
listen可以传入回调函数,当history修改将调用cb
listen (cb: Function) {
this.cb = cb
}
transitionTo
我们修改路由,无论是通过浏览器前进后退,push方法或者直接修改链接实际都将调用transitionTo。
首先就是通过路径location匹配mather中生成的record
// transitionTo
let route
// catch redirect option https://github.com/vuejs/vue-router/issues/3201
try {
route = this.router.match(location, this.current)
}
然后再调用confirmTransition,我们先看看成功回调
this.confirmTransition(
route,
() => {
// 切换成功后执行的回调函数
// 更新当前路径即history.current
// 执行listen中定义的cb
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
// 全局后置钩子调用
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
)
confirmTransition
confirmTransition即执行路由切换,伴随路由切换的关键逻辑在于各个组件的导航守卫执行。
首先通过resolveQueue获取组件列表(分为更新组件 失效组件 激活组件)
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
resolveQueue的实现很有意思,利用了前后的matched数组进行对比,仅单次遍历就实现组件获取,应该属于一个双指针
算法的实现吧。
let i
const max = Math.max(current.length, next.length)
for (i = 0; i < max; i++) {
if (current[i] !== next[i]) {
break
}
}
return {
updated: next.slice(0, i),
activated: next.slice(i),
deactivated: current.slice(i)
}
我们回到刚才,我们上面获取到了updated,deactivated,activated列表。接下来就行执行其中的导航守卫了。
const queue: Array<?NavigationGuard> = [].concat(
// 实际对应我们守卫的执行顺序
// 具体的extractLeaveGuards等逻辑就不展开了
extractLeaveGuards(deactivated),
this.router.beforeHooks,
extractUpdateHooks(updated),
activated.map(m => m.beforeEnter),
resolveAsyncComponents(activated)
)
接下来就是执行runQueue逻辑了。实际就是按顺序执行我们之前定义的queue。为什么需要runQueue呢?
我们在使用的时候可以使用next()来执行下一步,并且可以取消。所以这里不能直接执行所有钩子,必须通过next来确定走到下一步。
runQueue(queue, iterator, () => {
// wait until async components are resolved before
// extracting in-component enter guards
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)
})
}
})
})
runQueue的定义在asyncJS中,比较经典,大家可以自己去理解下。
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step = index => {
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
// 关键步骤
// 将step+1作为callBack传递给fn
// 实际调用next就是执行step(index + 1)
// 而执行step(index + 1)则是取执行队列的下一步queue[index + 1]来执行
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
matched属性
我们再补充个matched属性的由来。
matched属性记录了从父到子的路由record,对应我们定义的嵌套路由。
我们的record中使用parent记录了父record,因此可以通过递归寻找parent来生成整个嵌套路径的record。实际vueRouter通过formatMatch函数来生成matched数组。
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
地址更新
我们前面讲了路由的修改更新,但是没有提到如何同步到浏览器地址栏中,我们来看看其中的逻辑实现。
以hash模式为例,在我们调用push的时候,实际将调用hashHistory中的push方法。
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
可以看到在transitionTo执行完毕后,将执行pushState或直接修改hash来修改浏览器状态。
地址栏响应
我们再来看看修改地址栏的时候是如何触发路由切换的。以hash模式为例,实际在初始化时候会执行setupListeners函数,而其中的关键逻辑在于监听浏览器history
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
handleRoutingEvent的主要逻辑则是调用transitionTo实现路由切换。
const handleRoutingEvent = () => {
const current = this.current
if (!ensureSlash()) {
return
}
// 通过getHash获取当前浏览器hash
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
routerLink
routerLink的实现就比较简单了,是个组件实现。
其主要逻辑在于渲染a标签,并且监听click函数进行路由切换的控制。
首先通过router.resolve方法获取路径及record,用于实现active等类名添加。
// RouterLink render
const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(
this.to,
current,
this.append
)
再定义处理函数,主要逻辑在于调用路由的push方法并传入需要跳转的链接location
// RouterLink render
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location, noop)
} else {
router.push(location, noop)
}
}
}
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
this.event.forEach(e => {
on[e] = handler
})
} else {
on[this.event] = handler
}
最后进行组件的渲染逻辑,执行h方法
// RouterLink render
return h(this.tag, data, this.$slots.default)
routerView
routerView的逻辑相对于routerLink会复杂一些。
和routerLink一样,关键逻辑都在render函数中
// 首先会定义routerView属性在后面会使用到
data.routerView = true
const h = parent.$createElement
const name = props.name
// 这边值得一说
// 在这里访问了父组件的$route属性
// 实际会访问到installJS中定义的_router属性
// 而_router被定义为响应式数据
// 因此当路由切换时触发的_router修改将导致渲染函数重新执行
// 非常nice的关联
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
接下来便是嵌套routerView的实现了,通过前面定义的routerView来标记当前组件为routerView。如此便可以通过组件的parent.$vode.data向上寻找计数routerView的嵌套深度。
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) {
depth++
}
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
而通过depth和matched则可以获取到对应匹配到的同层recored,进而进一步获取组件实例。
const matched = route.matched[depth]
const component = matched && matched.components[name]~
接着再调用组件的渲染函数实际渲染成匹配组件的视图component而非routerView。
return h(component, data, children)
结语
本篇文章记录了自己在vueRouter源码学习中的一些关键逻辑梳理。可能有些地方不够细节或者有错误地方欢迎指出。