history.transitionTo 是 Vue-Router 中非常重要的方法,当我们切换路由线路的时候,就会执行到该方法
前一节我们分析了 matcher 的相关实现,知道它是如何找到匹配的新线路,那么匹配到新线路后又做了哪些事情,接下来我们来完整分析一下 transitionTo 的实现,它的定义在 src/history/base.js 中
// transitionTo实际上就是在切换this.currenttransitionTo (location: RawLocation, // 目标onComplete?: Function, // 当前路径onAbort?: Function) {let route// catch redirect option https://github.com/vuejs/vue-router/issues/3201try {// 匹配到目标的路径route = this.router.match(location, this.current)} catch (e) {this.errorCbs.forEach(cb => {cb(e)})// Exception should still be thrownthrow e}// 拿到新的路径const prev = this.current// confirmTransition去做真正的切换this.confirmTransition(route,() => {this.updateRoute(route)onComplete && onComplete(route)this.ensureURL()this.router.afterHooks.forEach(hook => {hook && hook(route, prev)})// fire ready cbs onceif (!this.ready) {this.ready = truethis.readyCbs.forEach(cb => {cb(route)})}},err => {if (onAbort) {onAbort(err)}if (err && !this.ready) {// Initial redirection should not mark the history as ready yet// because it's triggered by the redirection instead// https://github.com/vuejs/vue-router/issues/3225// https://github.com/vuejs/vue-router/issues/3331if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {this.ready = truethis.readyErrorCbs.forEach(cb => {cb(err)})}}})}
this.current是history维护的当前路径,它的初始值是在history的构造函数中初始化的
this.current = START
START 的定义在 src/util/route.js 中
// the starting route that represents the initial state// 创建一个初始的Routeexport const START = createRoute(null, {path: '/'})
confirmTransition方法去做真正的切换,由于这个过程可能有一些异步的操作(如异步组件),所以整个 confirmTransition API 设计成带有成功回调函数和失败回调函数,先来看一下它的定义
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {const current = this.currentthis.pending = routeconst abort = err => {// changed after adding errors with// https://github.com/vuejs/vue-router/pull/3047 before that change,// redirect and aborted navigation would produce an err == nullif (!isNavigationFailure(err) && isError(err)) {if (this.errorCbs.length) {this.errorCbs.forEach(cb => {cb(err)})} else {if (process.env.NODE_ENV !== 'production') {warn(false, 'uncaught error during route navigation:')}console.error(err)}}onAbort && onAbort(err)}const lastRouteIndex = route.matched.length - 1const lastCurrentIndex = current.matched.length - 1if (isSameRoute(route, current) && // 判断计算后的route和current是相同路径// in the case the route map has been dynamically appended tolastRouteIndex === lastCurrentIndex &&route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]) {this.ensureURL()if (route.hash) {handleScroll(this.router, current, route, false)}return abort(createNavigationDuplicatedError(current, route))}// 解析出3个队列const { updated, deactivated, activated } = resolveQueue(this.current.matched,route.matched // 是一个RouteRecord的数组)// 1.导航守卫// 路径变换,执行一系列的钩子函数// 构造一个队列,实际上是个数组const queue: Array<?NavigationGuard> = [].concat(// in-component leave guardsextractLeaveGuards(deactivated),// global before hooksthis.router.beforeHooks,// in-component update hooksextractUpdateHooks(updated),// in-config enter guardsactivated.map(m => m.beforeEnter),// async componentsresolveAsyncComponents(activated))// 定义一个迭代器函数const iterator = (hook: NavigationGuard, next) => {if (this.pending !== route) {return abort(createNavigationCancelledError(current, route))}try {// 去执行每一个导航守卫hook,传入route、current和匿名函数 对应 to from nexthook(route, current, (to: any) => {// 根据条件执行abort或next,只有执行next时才会前进到下一个导航守卫钩子函数中// 这也就是为什么官方文档会说只有执行 next 方法来 resolve 这个钩子函数if (to === false) {// next(false) -> abort navigation, ensure current URLthis.ensureURL(true)abort(createNavigationAbortedError(current, route))} else if (isError(to)) {this.ensureURL(true)abort(to)} else if (typeof to === 'string' ||(typeof to === 'object' &&(typeof to.path === 'string' || typeof to.name === 'string'))) {// next('/') or next({ path: '/' }) -> redirectabort(createNavigationRedirectedError(current, route))if (typeof to === 'object' && to.replace) {this.replace(to)} else {this.push(to)}} else {// confirm transition and pass on the valuenext(to)}})} catch (e) {abort(e)}}// 执行这个队列runQueue(queue, iterator, () => {// wait until async components are resolved before// extracting in-component enter guardsconst enterGuards = extractEnterGuards(activated)const queue = enterGuards.concat(this.router.resolveHooks)runQueue(queue, iterator, () => {if (this.pending !== route) {return abort(createNavigationCancelledError(current, route))}this.pending = nullonComplete(route)if (this.router.app) {this.router.app.$nextTick(() => {handleRouteEntered(route)})}})})}
resolveQueue
function resolveQueue (current: Array<RouteRecord>,next: Array<RouteRecord>): {updated: Array<RouteRecord>,activated: Array<RouteRecord>,deactivated: Array<RouteRecord>} {let iconst 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)}}
由于路径是由 current 变向 route,那么就遍历对比 2 边的 RouteRecord,找到一个不一样的位置 i,那么 next 中从 0 到 i 的 RouteRecord 是两边都一样,则为 updated 的部分;从 i 到最后的 RouteRecord 是 next 独有的,为 activated 的部分;而 current 中从 i 到最后的 RouteRecord 则没有了,为 deactivated 的部分
导航守卫
官方的说法叫导航守卫,实际上就是发生在路由路径切换的时候,执行的一系列钩子函数
runQueue定义在 src/util/async.js 中
// 非常经典的异步函数队列化执行的模式export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {// queue是一个NavigationGuard类型的数组// 每次根据index从queue中取一个guard,然后执行fn函数,并且把guard作为参数传入const step = index => {if (index >= queue.length) {cb()} else {if (queue[index]) {// 第二个参数是一个函数,当这个函数执行时再递归执行step函数,前进到下一个// fn就是iterator函数fn(queue[index], () => {step(index + 1)})} else {step(index + 1)}}}step(0)}
fn 就是刚才的 iterator 函数
queue 是怎么构造的
const queue: Array<?NavigationGuard> = [].concat(// in-component leave guardsextractLeaveGuards(deactivated),// global before hooksthis.router.beforeHooks,// in-component update hooksextractUpdateHooks(updated),// in-config enter guardsactivated.map(m => m.beforeEnter),// async componentsresolveAsyncComponents(activated))
按照顺序如下:
- 在失活的组件里调用离开守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫
- 在激活的路由配置里调用 beforeEnter。
- 解析异步路由组件
第一步
通过执行 extractLeaveGuards(deactivated),先来看一下 extractLeaveGuards 的定义
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)}
它内部调用了 extractGuards 的通用方法,可以从 RouteRecord 数组中提取各个阶段的守卫
function extractGuards (records: Array<RouteRecord>,name: string,bind: Function,reverse?: boolean): Array<?Function> {const guards = flatMapComponents(records, (def, instance, match, key) => {// 获取组件中的对应name的导航守卫const guard = extractGuard(def, name)if (guard) {return Array.isArray(guard)? guard.map(guard => bind(guard, instance, match, key)) // 调用bind方法把组件的实例instance作为函数执行的上下文绑定到guard上,bind对应的是bindGuard: bind(guard, instance, match, key)}})return flatten(reverse ? guards.reverse() : guards)}
用到了 flatMapComponents 方法去从 records 中获取所有的导航,它的定义在 src/util/resolve-components.js 中
// flatMapComponents返回一个数组,数组的元素是从matched里获取到所有组件的key,然后返回fn函数执行的结果export function flatMapComponents (matched: Array<RouteRecord>,fn: Function): Array<?Function> {// flatten把二维数组拍平成一维数组return flatten(matched.map(m => {return Object.keys(m.components).map(key => fn(m.components[key],m.instances[key],m, key))}))}export function flatten (arr: Array<any>): Array<any> {return Array.prototype.concat.apply([], arr)}
extractGuard
function extractGuard (def: Object | Function,key: string): NavigationGuard | Array<NavigationGuard> {if (typeof def !== 'function') {// extend now so that global mixins are applied.def = _Vue.extend(def)}return def.options[key]}
bindGuard
function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {if (instance) {return function boundRouteGuard () {return guard.apply(instance, arguments)}}}
对于 extractLeaveGuards(deactivated) 而言,获取到的就是所有失活组件中定义的 beforeRouteLeave 钩子函数
第二步
this.router.beforeHooks,在VueRouter 类中定义了 beforeEach 方法,在 src/index.js 中
beforeEach (fn: Function): Function {return registerHook(this.beforeHooks, fn)}function registerHook (list: Array<any>, fn: Function): Function {list.push(fn)return () => {const i = list.indexOf(fn)if (i > -1) list.splice(i, 1)}}
当用户使用 router.beforeEach 注册了一个全局守卫,就会往 router.beforeHooks 添加一个钩子函数,这样 this.router.beforeHooks 获取的就是用户注册的全局 beforeEach 守卫
第三步
执行了 extractUpdateHooks(updated),来看一下 extractUpdateHooks 的定义
function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {return extractGuards(updated, 'beforeRouteUpdate', bindGuard)}
和 extractLeaveGuards(deactivated) 类似,extractUpdateHooks(updated) 获取到的就是所有重用的组件中定义的 beforeRouteUpdate 钩子函数
第四步
执行 activated.map(m => m.beforeEnter),获取的是在激活的路由配置中定义的 beforeEnter 函数
第五步
执行 resolveAsyncComponents(activated) 解析异步组件,先来看一下 resolveAsyncComponents 的定义,在 src/util/resolve-components.js 中
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {// 返回的是一个导航守卫函数,有标准的to、from、next参数return (to, from, next) => {let hasAsync = falselet pending = 0let error = nullflatMapComponents(matched, (def, _, match, key) => {// if it's a function and doesn't have cid attached,// assume it's an async component resolve function.// we are not using Vue's default async resolving mechanism because// we want to halt the navigation until the incoming component has been// resolved.if (typeof def === 'function' && def.cid === undefined) {hasAsync = truepending++const resolve = once(resolvedDef => {if (isESModule(resolvedDef)) {resolvedDef = resolvedDef.default}// save resolved on async factory in case it's used elsewheredef.resolved = typeof resolvedDef === 'function'? resolvedDef: _Vue.extend(resolvedDef)match.components[key] = resolvedDefpending--if (pending <= 0) {next()}})const reject = once(reason => {const msg = `Failed to resolve async component ${key}: ${reason}`process.env.NODE_ENV !== 'production' && warn(false, msg)if (!error) {error = isError(reason)? reason: new Error(msg)next(error)}})let restry {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.3const comp = res.componentif (comp && typeof comp.then === 'function') {comp.then(resolve, reject)}}}}})if (!hasAsync) next()}}
使用 flatMapComponents 方法从 matched 中获取到每个组件的定义,判断如果是异步组件,则执行异步组件加载逻辑,这块和我们之前分析 Vue 加载异步组件很类似,加载成功后会执行 match.components[key] = resolvedDef 把解析好的异步组件放到对应的 components 上,并且执行 next 函数
这样在 resolveAsyncComponents(activated) 解析完所有激活的异步组件后,就可以拿到这一次所有激活的组件
这样在做完这 5 步后又做了一些事情
runQueue(queue, iterator, () => {// wait until async components are resolved before// extracting in-component enter guardsconst enterGuards = extractEnterGuards(activated)const queue = enterGuards.concat(this.router.resolveHooks)runQueue(queue, iterator, () => {if (this.pending !== route) {return abort(createNavigationCancelledError(current, route))}this.pending = nullonComplete(route)if (this.router.app) {this.router.app.$nextTick(() => {handleRouteEntered(route)})}})})
第六步
在被激活的组件里调用 beforeRouteEnter
const enterGuards = extractEnterGuards(activated)function extractEnterGuards (activated: Array<RouteRecord>): Array<?Function> {return extractGuards(activated,'beforeRouteEnter',(guard, _, match, key) => {return bindEnterGuard(guard, match, key)})}function bindEnterGuard (guard: NavigationGuard,match: RouteRecord,key: string): NavigationGuard {return function routeEnterGuard (to, from, next) {return guard(to, from, cb => {if (typeof cb === 'function') {if (!match.enteredCbs[key]) {match.enteredCbs[key] = []}match.enteredCbs[key].push(cb)}next(cb)})}}
extractEnterGuards 函数的实现也是利用了 extractGuards 方法提取组件中的 beforeRouteEnter 导航钩子函数
beforeRouteEnter 钩子函数中是拿不到组件实例的,因为当守卫执行前,组件实例还没被创建,但是我们可以通过传一个回调给 next 来访问组件实例
在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数
beforeRouteEnter (to, from, next) {next(vm => {// 通过 `vm` 访问组件实例})}
在 bindEnterGuard 函数中,返回的是 routeEnterGuard 函数,所以在执行 iterator 中的 hook 函数的时候,就相当于执行 routeEnterGuard 函数,那么就会执行我们定义的导航守卫 guard 函数,并且当这个回调函数执行的时候,首先执行 next 函数 rersolve 当前导航钩子,然后把回调函数的参数,它也是一个回调函数之后收集起来
然后在最后会执行
if (this.router.app) {this.router.app.$nextTick(() => {handleRouteEntered(route)})}
export function handleRouteEntered (route: Route) {for (let i = 0; i < route.matched.length; i++) {const record = route.matched[i]for (const name in record.instances) {const instance = record.instances[name]const cbs = record.enteredCbs[name]if (!instance || !cbs) continuedelete record.enteredCbs[name]for (let i = 0; i < cbs.length; i++) {if (!instance._isBeingDestroyed) cbs[i](instance)}}}}
在根路由组件重新渲染后,执行handleRouteEntered
因为考虑到一些了路由组件被套 transition 組件在一些缓动模式下不一定能拿到实例,所以用一个轮询方法不断去判断,直到能获取到组件实例,再去调用 cb,并把组件实例作为参数传入,这就是在回调函数中能拿到组件实例的原因
第七步
调用全局的 beforeResolve 守卫
获取 this.router.resolveHooks,这个和 this.router.beforeHooks 的获取类似,在 VueRouter 类中定义了 beforeResolve 方法
beforeResolve (fn: Function): Function {return registerHook(this.resolveHooks, fn)}
当用户使用 router.beforeResolve 注册了一个全局守卫,就会往 router.resolveHooks 添加一个钩子函数,这样 this.router.resolveHooks 获取的就是用户注册的全局 beforeResolve 守卫
第八步
调用全局的 afterEach 钩子
在最后执行了 onComplete(route) 后,会执行 this.updateRoute(route) 方法
updateRoute (route: Route) {this.current = routethis.cb && this.cb(route)}
同样在 VueRouter 类中定义了 afterEach 方法
afterEach (fn: Function): Function {return registerHook(this.afterHooks, fn)}
当用户使用 router.afterEach 注册了一个全局守卫,就会往 router.afterHooks 添加一个钩子函数,这样 this.router.afterHooks 获取的就是用户注册的全局 afterHooks 守卫
那么至此把所有导航守卫的执行分析完毕了,知道路由切换除了执行这些钩子函数,从表象上有 2 个地方会发生变化,一个是 url 发生变化,一个是组件发生变化
url
当点击 router-link 的时候,实际上最终会执行 router.push,如下
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {// $flow-disable-lineif (!onComplete && !onAbort && typeof Promise !== 'undefined') {return new Promise((resolve, reject) => {this.history.push(location, resolve, reject)})} else {this.history.push(location, onComplete, onAbort)}}
this.history.push 函数,这个函数是子类实现的,不同模式下该函数的实现略有不同
平时使用比较多的 hash 模式该函数的实现,在 src/history/hash.js 中
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)}
在切换完成的回调函数中,执行 pushHash 函数
function pushHash (path) {// 如果支持则获取当前完整的url,执行pushStateif (supportsPushState) {pushState(getUrl(path))} else {window.location.hash = path}}
supportsPushState定义在 src/util/push-state.js 中
export const supportsPushState =inBrowser &&(function () {const ua = window.navigator.userAgentif ((ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&ua.indexOf('Mobile Safari') !== -1 &&ua.indexOf('Chrome') === -1 &&ua.indexOf('Windows Phone') === -1) {return false}return window.history && typeof window.history.pushState === 'function'})()
pushState
export function pushState (url?: string, replace?: boolean) {saveScrollPosition()// try...catch the pushState call to get around Safari// DOM Exception 18 where it limits to 100 pushState callsconst history = window.historytry {// 调用浏览器原生的history的replaceState或者pushState接口,更新浏览器的url地址,并把当前url压入历史栈中if (replace) {// preserve existing history state as it could be overriden by the userconst 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)}}
然后在 history 的初始化中,会设置一个监听器,监听历史栈的变化
// this is delayed until the app mounts// to avoid the hashchange listener being fired too earlysetupListeners () {if (this.listeners.length > 0) {return}const router = this.routerconst expectScroll = router.options.scrollBehaviorconst supportsScroll = supportsPushState && expectScrollif (supportsScroll) {this.listeners.push(setupScroll())}const handleRoutingEvent = () => {const current = this.currentif (!ensureSlash()) {return}this.transitionTo(getHash(), route => {if (supportsScroll) {handleScroll(this.router, route, current, true)}if (!supportsPushState) {replaceHash(route.fullPath)}})}const eventType = supportsPushState ? 'popstate' : 'hashchange'window.addEventListener(eventType,handleRoutingEvent)this.listeners.push(() => {window.removeEventListener(eventType, handleRoutingEvent)})}
当点击浏览器返回按钮的时候,如果已经有 url 被压入历史栈,则会触发 popstate 事件,然后拿到当前要跳转的 hash,执行 transtionTo 方法做一次路径转换
在使用 Vue-Router 开发项目的时候,打开调试页面 http://localhost:8080 后会自动把 url 修改为 http://localhost:8080/#/,这是怎么做到呢
原来在实例化 HashHistory 的时候,构造函数会执行 ensureSlash() 方法
function ensureSlash () {var path = getHash();if (path.charAt(0) === '/') {return true}// path为空,执行replaceHashreplaceHash('/' + path);return false}function getHash () {// We can't use window.location.hash here because it's not// consistent across browsers - Firefox will pre-decode it!var href = window.location.href;var index = href.indexOf('#');// empty pathif (index < 0) { return '' }href = href.slice(index + 1);return href}function getUrl (path) {var href = window.location.href;var i = href.indexOf('#');var base = i >= 0 ? href.slice(0, i) : href;return (base + "#" + path)}function replaceHash (path) {if (supportsPushState) {// 内部会执行一次getUrl,计算出新的url为http://localhost:8080/#/replaceState(getUrl(path));} else {window.location.replace(getUrl(path));}}function replaceState (url) {// 最终会执行pushState,这就是url会改变的原因pushState(url, true);}
组件
路由最终的渲染离不开组件,Vue-Router 内置了
// <router-view>是一个functional组件,它的渲染也是依赖render函数export default {name: 'RouterView',functional: true,props: {name: {type: String,default: 'default'}},render (_, { props, children, parent, data }) {// used by devtools to display a router-view badgedata.routerView = true// directly use parent context's createElement() function// so that components rendered by router-view can resolve named slotsconst h = parent.$createElementconst name = props.name// 1.获取当前的路径const route = parent.$routeconst cache = parent._routerViewCache || (parent._routerViewCache = {})// determine current view depth, also check to see if the tree// has been toggled inactive but kept-alive.let depth = 0let inactive = falsewhile (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// render previous view if the tree is inactive and kept-aliveif (inactive) {const cachedData = cache[name]const cachedComponent = cachedData && cachedData.componentif (cachedComponent) {// #2301// pass propsif (cachedData.configProps) {fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)}return h(cachedComponent, data, children)} else {// render previous empty viewreturn h()}}const matched = route.matched[depth]const component = matched && matched.components[name]// render empty node if no matched route or no config componentif (!matched || !component) {cache[name] = nullreturn h()}// cache componentcache[name] = { component }// attach instance registration hook// this will be called in the instance's injected lifecycle hooksdata.registerRouteInstance = (vm, val) => {// val could be undefined for unregistrationconst current = matched.instances[name]if ((val && current !== vm) ||(!val && current === vm)) {matched.instances[name] = val}}// also register instance in prepatch hook// in case the same component instance is reused across different routes;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {matched.instances[name] = vnode.componentInstance}// register instance in init hook// in case kept-alive component be actived when routes changeddata.hook.init = (vnode) => {if (vnode.data.keepAlive &&vnode.componentInstance &&vnode.componentInstance !== matched.instances[name]) {matched.instances[name] = vnode.componentInstance}// if the route transition has already been confirmed then we weren't// able to call the cbs during confirmation as the component was not// registered yet, so we call it here.handleRouteEntered(route)}const configProps = matched.props && matched.props[name]// save route and configProps in cacheif (configProps) {extend(cache[name], {route,configProps})fillPropsinData(component, data, route, configProps)}return h(component, data, children)}}
之前分析过,在 src/install.js 中,给 Vue 的原型上定义了 $route
Object.defineProperty(Vue.prototype, '$route', {get () { return this._routerRoot._route }})
然后在 VueRouter 的实例执行 router.init 方法的时候,会执行如下逻辑,定义在 src/index.js 中
history.listen(route => {this.apps.forEach(app => {app._route = route})})
而 history.listen 方法定义在 src/history/base.js 中
listen (cb: Function) {this.cb = cb}
然后在 updateRoute 的时候执行 this.cb
updateRoute (route: Route) {this.current = routethis.cb && this.cb(route)}
也就是执行 transitionTo 方法最后执行 updateRoute 的时候会执行回调,然后会更新 this.apps 保存的组件实例的 _route 值,this.apps 数组保存的实例的特点都是在初始化的时候传入了 router 配置项,一般的场景数组只会保存根 Vue 实例,因为是在 new Vue 传入了 router 实例
$route 是定义在 Vue.prototype 上
每个组件实例访问 $route 属性,就是访问根实例的 _route,也就是当前的路由线路
// used by devtools to display a router-view badgedata.routerView = true// ...// parent._routerRoot表示是根Vue实例 循环是从当前得<router-view>的父节点向上找,一直找到根Vue实例while (parent && parent._routerRoot !== parent) {const vnodeData = parent.$vnode ? parent.$vnode.data : {}// 碰到父节点也是<router-view>时说明<router-view>有嵌套 depth++if (vnodeData.routerView) {depth++}if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {inactive = true}parent = parent.$parent}// ...// 遍历完成后根据当前线路匹配的路径和depth找到相应的RouteRecord,进而找到该渲染的组件const matched = route.matched[depth]const component = matched && matched.components[name]// ...// attach instance registration hook// this will be called in the instance's injected lifecycle hooks// 定义一个注册路由实例的方法data.registerRouteInstance = (vm, val) => {// val could be undefined for unregistrationconst current = matched.instances[name]if ((val && current !== vm) ||(!val && current === vm)) {matched.instances[name] = val}}// ...// 根据component渲染出对应的组件vnodereturn h(component, data, children)
给 vnode 的 data 定义了 registerRouteInstance 方法,在 src/install.js 中,会调用该方法去注册路由的实例
const registerInstance = (vm, callVal) => {let i = vm.$options._parentVnodeif (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {i(vm, callVal)}}Vue.mixin({beforeCreate () {if (isDef(this.$options.router)) {this._routerRoot = thisthis._router = this.$options.routerthis._router.init(this)Vue.util.defineReactive(this, '_route', this._router.history.current)} else {this._routerRoot = (this.$parent && this.$parent._routerRoot) || this}registerInstance(this, this)},destroyed () {registerInstance(this)}})
在混入的 beforeCreate 钩子函数中,会执行 registerInstance 方法,进而执行 render 函数中定义的 registerRouteInstance 方法,从而给 matched.instances[name] 赋值当前组件的 vm 实例
那么当执行 transitionTo 来更改路由线路后,组件是如何重新渲染的呢?在混入的 beforeCreate 钩子函数中有这么一段逻辑
Vue.mixin({beforeCreate () {if (isDef(this.$options.router)) {this._routerRoot = thisthis._router = this.$options.routerthis._router.init(this)Vue.util.defineReactive(this, '_route', this._router.history.current)} else {this._routerRoot = (this.$parent && this.$parent._routerRoot) || this}registerInstance(this, this)},})
由于我们把根 Vue 实例的 _route 属性定义成响应式的,在每个
Vue-Router 还内置了另一个组件
- 无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致,所以,当要切换路由模式,或者在 IE9 降级使用 hash 模式,无须作任何变动
- 在 HTML5 history 模式下,router-link 会守卫点击事件,让浏览器不再重新加载页面
- 当在 HTML5 history 模式下使用 base 选项之后,所有的 to 属性都不需要写(基路径)了
分析它的实现,它的定义在 src/components/link.js 中
export default {name: 'RouterLink',props: {to: {type: toTypes,required: true},tag: {type: String,default: 'a'},custom: Boolean,exact: Boolean,exactPath: Boolean,append: Boolean,replace: Boolean,activeClass: String,exactActiveClass: String,ariaCurrentValue: {type: String,default: 'page'},event: {type: eventTypes,default: 'click'}},// 渲染render (h: Function) {// 路由解析const router = this.$routerconst current = this.$routeconst { location, route, href } = router.resolve(this.to,current,this.append)// 对exactActiveClass和activeClass做处理const classes = {}const globalActiveClass = router.options.linkActiveClassconst globalExactActiveClass = router.options.linkExactActiveClass// Support global empty active classconst activeClassFallback =globalActiveClass == null ? 'router-link-active' : globalActiveClassconst exactActiveClassFallback =globalExactActiveClass == null? 'router-link-exact-active': globalExactActiveClassconst activeClass =this.activeClass == null ? activeClassFallback : this.activeClassconst exactActiveClass =this.exactActiveClass == null? exactActiveClassFallback: this.exactActiveClassconst compareTarget = route.redirectedFrom? createRoute(null, normalizeLocation(route.redirectedFrom), null, router): route// 当配置 exact 为 true 的时候,只有当目标路径和当前路径完全匹配的时候,会添加 exactActiveClass;而当目标路径包含当前路径的时候,会添加 activeClassclasses[exactActiveClass] = isSameRoute(current, compareTarget, this.exactPath)classes[activeClass] = this.exact || this.exactPath? classes[exactActiveClass]: isIncludedRoute(current, compareTarget)const ariaCurrentValue = classes[exactActiveClass] ? this.ariaCurrentValue : null// 创建一个守卫函数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}const data: any = { class: classes }const scopedSlot =!this.$scopedSlots.$hasNormal &&this.$scopedSlots.default &&this.$scopedSlots.default({href,route,navigate: handler,isActive: classes[activeClass],isExactActive: classes[exactActiveClass]})if (scopedSlot) {if (process.env.NODE_ENV !== 'production' && !this.custom) {!warnedCustomSlot && warn(false, 'In Vue Router 4, the v-slot API will by default wrap its content with an <a> element. Use the custom prop to remove this warning:\n<router-link v-slot="{ navigate, href }" custom></router-link>\n')warnedCustomSlot = true}if (scopedSlot.length === 1) {return scopedSlot[0]} else if (scopedSlot.length > 1 || !scopedSlot.length) {if (process.env.NODE_ENV !== 'production') {warn(false,`<router-link> with to="${this.to}" is trying to use a scoped slot but it didn't provide exactly one child. Wrapping the content with a span element.`)}return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)}}if (process.env.NODE_ENV !== 'production') {if ('tag' in this.$options.propsData && !warnedTagProp) {warn(false,`<router-link>'s tag prop is deprecated and has been removed in Vue Router 4. Use the v-slot API to remove this warning: https://next.router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link.`)warnedTagProp = true}if ('event' in this.$options.propsData && !warnedEventProp) {warn(false,`<router-link>'s event prop is deprecated and has been removed in Vue Router 4. Use the v-slot API to remove this warning: https://next.router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link.`)warnedEventProp = true}}if (this.tag === 'a') {data.on = ondata.attrs = { href, 'aria-current': ariaCurrentValue }} else {// find the first <a> child and apply listener and hrefconst a = findAnchor(this.$slots.default)if (a) {// in case the <a> is a static nodea.isStatic = falseconst aData = (a.data = extend({}, a.data))aData.on = aData.on || {}// transform existing events in both objects into arrays so we can push laterfor (const event in aData.on) {const handler = aData.on[event]if (event in on) {aData.on[event] = Array.isArray(handler) ? handler : [handler]}}// append new listeners for router-linkfor (const event in on) {if (event in aData.on) {// on[event] is always a functionaData.on[event].push(on[event])} else {aData.on[event] = handler}}const aAttrs = (a.data.attrs = extend({}, a.data.attrs))aAttrs.href = hrefaAttrs['aria-current'] = ariaCurrentValue} else {// doesn't have <a> child, apply listener to selfdata.on = on}}return h(this.tag, data, this.$slots.default)}}
router.resolve 是 VueRouter 的实例方法,它的定义在 src/index.js 中
resolve (to: RawLocation,current?: Route,append?: boolean): {location: Location,route: Route,href: string,// for backwards compatnormalizedTo: Location,resolved: Route} {current = current || this.history.current// 规范化生成目标locationconst location = normalizeLocation(to, current, append, this)// 根据location和match通过this.match方法计算生成目标路径routeconst route = this.match(location, current)// 根据base、fullPath和this.mode通过createHref方法计算出最终跳转的hrefconst fullPath = route.redirectedFrom || route.fullPathconst base = this.history.baseconst href = createHref(base, fullPath, this.mode)return {location,route,href,// for backwards compatnormalizedTo: location,resolved: route}}function createHref (base: string, fullPath: string, mode) {var path = mode === 'hash' ? '#' + fullPath : fullPathreturn base ? cleanPath(base + '/' + path) : path}
接着创建了一个守卫函数
const handler = e => {if (guardEvent(e)) {if (this.replace) {router.replace(location, noop)} else {router.push(location, noop)}}}function guardEvent (e) {// don't redirect with control keysif (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return// don't redirect when preventDefault calledif (e.defaultPrevented) return// don't redirect on right clickif (e.button !== undefined && e.button !== 0) return// don't redirect if `target="_blank"`if (e.currentTarget && e.currentTarget.getAttribute) {const target = e.currentTarget.getAttribute('target')if (/\b_blank\b/i.test(target)) return}// this may be a Weex event which doesn't have this methodif (e.preventDefault) {e.preventDefault()}return true}const on = { click: guardEvent }if (Array.isArray(this.event)) {this.event.forEach(e => {on[e] = handler})} else {on[this.event] = handler}
最终会监听点击事件或者其它可以通过 prop 传入的事件类型,执行 hanlder 函数,最终执行 router.push 或者 router.replace 函数,它们的定义在 src/index.js 中
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {// $flow-disable-lineif (!onComplete && !onAbort && typeof Promise !== 'undefined') {return new Promise((resolve, reject) => {this.history.push(location, resolve, reject)})} else {this.history.push(location, onComplete, onAbort)}}replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {// $flow-disable-lineif (!onComplete && !onAbort && typeof Promise !== 'undefined') {return new Promise((resolve, reject) => {this.history.replace(location, resolve, reject)})} else {this.history.replace(location, onComplete, onAbort)}}
实际上就是执行了 history 的 push 和 replace 方法做路由跳转
最后判断当前 tag 是否是 标签,
路径变化是路由中最重要的功能,记住以下内容:路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据
