「这是我参与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源码学习中的一些关键逻辑梳理。可能有些地方不够细节或者有错误地方欢迎指出。