本文基于
vue-router 4.1.6
版本源码进行剖析
前言
在上一篇《Vue3 相干源码 -Vue Router 源码解析 (一)》文章中,咱们曾经剖析了createWebHashHistory()
和createRouter()
的相干内容,本文将持续下一个知识点 app.use(router)
展现剖析
// 1. 定义路由组件.
// 也能够从其余文件导入
const Home = {template: '<div>Home</div>'}
const About = {template: '<div>About</div>'}
// 2. 定义一些路由
// 每个路由都须要映射到一个组件。// 咱们前面再探讨嵌套路由。const routes = [{ path: '/', component: Home},
{path: '/about', component: About},
]
// 3. 创立路由实例并传递 `routes` 配置
// 你能够在这里输出更多的配置,但咱们在这里
// 临时放弃简略
const router = VueRouter.createRouter({
// 4. 外部提供了 history 模式的实现。为了简略起见,咱们在这里应用 hash 模式。history: VueRouter.createWebHashHistory(),
routes, // `routes: routes` 的缩写
})
// 5. 创立并挂载根实例
const app = Vue.createApp({})
// 确保 _use_ 路由实例使
// 整个利用反对路由。app.use(router)
app.mount('#app')
初始化
app.use(router)应用 VueRouter
use(plugin, ...options) {if (plugin && isFunction(plugin.install)) {installedPlugins.add(plugin);
plugin.install(app, ...options);
}
else if (isFunction(plugin)) {installedPlugins.add(plugin);
plugin(app, ...options);
}
return app;
}
router.install(app)
从下面 Vue3
的源码能够晓得,最终会触发 Vue Router
的install()
办法
const START_LOCATION_NORMALIZED = {
path: '/',
name: undefined,
params: {},
query: {},
hash: '',
fullPath: '/',
matched: [],
meta: {},
redirectedFrom: undefined,
};
const currentRoute = vue.shallowRef(START_LOCATION_NORMALIZED);
const router = {
//....
addRoute,
removeRoute,
push,
replace,
beforeEach: beforeGuards.add,
isReady,
install(app) {//... 省略,内部 app.use()时调用
},
};
如上面代码块所示,次要执行了:
- 注册了
RouterLink
和RouterView
两个组件 - 注册
router
为Vue
全局对象,保障this.$router
能注入到每一个子组件中 push(routerHistory.location)
:初始化时触发push()
操作(部分作用域下的办法)provide
:router
、reactiveRoute
(实质是currentRoute
的reactive
构造模式)、currentRoute
install(app) {
const router = this;
app.component('RouterLink', RouterLink);
app.component('RouterView', RouterView);
app.config.globalProperties.$router = router;
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
get: () => vue.unref(currentRoute), // 主动解构 Ref,拿出.value
});
if (isBrowser &&
// used for the initial navigation client side to avoid pushing
// multiple times when the router is used in multiple apps
!started &&
currentRoute.value === START_LOCATION_NORMALIZED) {
// see above
started = true;
push(routerHistory.location).catch(err => {warn('Unexpected error when starting the router:', err);
});
}
const reactiveRoute = {};
for (const key in START_LOCATION_NORMALIZED) {reactiveRoute[key] = vue.computed(() => currentRoute.value[key]);
}
app.provide(routerKey, router); // 如下面代码块所示
app.provide(routeLocationKey, vue.reactive(reactiveRoute));
app.provide(routerViewLocationKey, currentRoute); // 如下面代码块所示
// 省略 Vue.unmount 的一些逻辑解决...
}
Vue Router
整体初始化的流程曾经剖析结束,一些根底的API
,如router.push
等也在初始化过程中剖析,因而上面将剖析Vue Router
提供的自定义组件以及组合式API
的内容
组件
RouterView
咱们应用代码扭转路由时,也在扭转
<router-view></router-view>
的Component
内容,实现组件渲染
<script src="https://unpkg.com/vue@3"></script>
<script src="https://unpkg.com/vue-router@4"></script>
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 应用 router-link 组件进行导航 -->
<!-- 通过传递 `to` 来指定链接 -->
<!--`<router-link>` 将出现一个带有正确 `href` 属性的 `<a>` 标签 -->
<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
</p>
<!-- 路由进口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
<router-view></router-view>
就是一个Vue Component
<router-view>
代码量还是挺多的,因而上面将切割为 2 个局部进行剖析
const RouterViewImpl = vue.defineComponent({setup(props, { attrs, slots}) {
//========= 第 1 局部 =============
const injectedRoute = vue.inject(routerViewLocationKey);
const routeToDisplay = vue.computed(() => props.route || injectedRoute.value);
const injectedDepth = vue.inject(viewDepthKey, 0);
const depth = vue.computed(() => {let initialDepth = vue.unref(injectedDepth);
const {matched} = routeToDisplay.value;
let matchedRoute;
while ((matchedRoute = matched[initialDepth]) &&
!matchedRoute.components) {initialDepth++;}
return initialDepth;
});
const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]);
vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));
vue.provide(matchedRouteKey, matchedRouteRef);
vue.provide(routerViewLocationKey, routeToDisplay);
const viewRef = vue.ref();
vue.watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => {if (to) {to.instances[name] = instance;
if (from && from !== to && instance && instance === oldInstance) {if (!to.leaveGuards.size) {to.leaveGuards = from.leaveGuards;}
if (!to.updateGuards.size) {to.updateGuards = from.updateGuards;}
}
}
if (instance &&
to &&
(!from || !isSameRouteRecord(to, from) || !oldInstance)) {(to.enterCallbacks[name] || []).forEach(callback => callback(instance));
}
}, {flush: 'post'});
return () => {
//========= 第 2 局部 =============
//...
};
},
});
第 1 局部:初始化变量以及初始化响应式变动监听
routerViewLocationKey 拿到目前的路由 injectedRoute
// <router-view></router-view> 代码
const injectedRoute = vue.inject(routerViewLocationKey);
在 app.use(router)
的源码中,咱们能够晓得,routerViewLocationKey
代表的是目前的 currentRoute
,每次路由变动时,会触发finalizeNavigation()
,同时更新currentRoute.value
=toLocation
因而 currentRoute
代表的就是目前最新的路由
install(app) {
//...
const router = this;
const reactiveRoute = {};
for (const key in START_LOCATION_NORMALIZED) {reactiveRoute[key] = vue.computed(() => currentRoute.value[key]);
}
app.provide(routerKey, router); // 如下面代码块所示
app.provide(routeLocationKey, vue.reactive(reactiveRoute));
app.provide(routerViewLocationKey, currentRoute); // 如下面代码块所示
// 省略 Vue.unmount 的一些逻辑解决...
}
const START_LOCATION_NORMALIZED = {
path: '/',
name: undefined,
params: {},
query: {},
hash: '',
fullPath: '/',
matched: [],
meta: {},
redirectedFrom: undefined,
};
function createRouter(options) {const currentRoute = vue.shallowRef(START_LOCATION_NORMALIZED);
}
function finalizeNavigation(toLocation, from, isPush, replace, data) {
//...
currentRoute.value = toLocation;
//...
}
routeToDisplay
实时同步为目前最新要跳转的路由
应用 computed
监听路由变动,优先获取 props.route
,如果有产生路由跳转景象,则routeToDisplay
会动态变化
// <router-view></router-view> 代码
const routeToDisplay = vue.computed(() => props.route || injectedRoute.value);
depth
实时同步为目前要跳转的路由对应的 matched 数组的 index
当路由发生变化时,会触发 routeToDisplay
发生变化,depth
应用 computed
监听 routeToDisplay
变动
const depth = vue.computed(() => {let initialDepth = vue.unref(injectedDepth);
const {matched} = routeToDisplay.value;
let matchedRoute;
while ((matchedRoute = matched[initialDepth]) &&
!matchedRoute.components) {initialDepth++;}
return initialDepth;
});
那
Vue Router
是如何利用depth
来一直加载目前路由门路上所有的Component
的呢?
如上面代码块所示,咱们在 push()
->resolve()
的流程中,会收集以后路由的所有parent
举个例子,门路
path
="/child1/child2"
,那么咱们拿到的matched
就是[{path: '/child1', Component: parent}, {path: '/child1/child2', Component: son}]
function push(to) {return pushWithRedirect(to);
}
function pushWithRedirect(to, redirectedFrom) {const targetLocation = (pendingLocation = resolve(to));
//...
}
function resolve(location, currentLocation) {
//....
const matched = [];
let parentMatcher = matcher;
while (parentMatcher) {
// reversed order so parents are at the beginning
matched.unshift(parentMatcher.record);
parentMatcher = parentMatcher.parent;
}
return {matched, ...}
}
当咱们跳转到 path
="/child1/child2"
时,会首先加载 path="/child1"
对应的 Child1
组件,此时 injectedDepth
=0
,在depth
的computed()
计算中,得出 initialDepth
=0
,因为matchedRoute.components
是存在的,无奈进行 initialDepth++
因而此时 <router-view>
组件拿的数据是matched[0]
={path: '/child1', Component: parent}
[{path: '/child1', Component: parent}, {path: '/child1/child2', Component: son}]
// Child1 组件
<div> 目前路由是 Child1</div>
<router-view></router-view>
const injectedDepth = vue.inject(viewDepthKey, 0);
const depth = vue.computed(() => {let initialDepth = vue.unref(injectedDepth);
const {matched} = routeToDisplay.value;
let matchedRoute;
while ((matchedRoute = matched[initialDepth]) &&
!matchedRoute.components) {initialDepth++;}
return initialDepth;
});
vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));
此时 Child1
组件依然有 <router-view></router-view>
组件,因而咱们再次初始化一次 RouterView
,加载path="/child1/child2"
对应的 Child2
组件
在下面的代码咱们能够晓得,viewDepthKey
会变为 depth.value + 1
,因而此时<router-view>
中injectedDepth
=1
,在 depth
的computed()
计算中,得出 initialDepth
=1
,因为matchedRoute.components
是存在的,无奈进行initialDepth++
从这里咱们能够轻易猜测出,如果门路上有一个片段,比方
path
='/child1/child2/child3'
中child2
没有对应的components
,那么就会跳过这个child2
,间接渲染child1
和child3
对应的组件
因而此时 <router-view>
组件拿的数据是matched[1]
={path: '/child1/child2', Component: son}
// Child2 组件
<div> 目前路由是 Child2</div>
const injectedDepth = vue.inject(viewDepthKey, 0);
const depth = vue.computed(() => {let initialDepth = vue.unref(injectedDepth);
const {matched} = routeToDisplay.value;
let matchedRoute;
while ((matchedRoute = matched[initialDepth]) &&
!matchedRoute.components) {initialDepth++;}
return initialDepth;
});
vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));
matchedRouteRef
实时同步为目前最新要跳转的路由对应的matcher
// <router-view></router-view> 代码
const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]);
更新组件示例,触发 beforeRouteEnter
回调
const viewRef = vue.ref();
vue.watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => {if (to) {to.instances[name] = instance;
if (from && from !== to && instance && instance === oldInstance) {if (!to.leaveGuards.size) {to.leaveGuards = from.leaveGuards;}
if (!to.updateGuards.size) {to.updateGuards = from.updateGuards;}
}
}
// trigger beforeRouteEnter next callbacks
if (instance &&
to &&
(!from || !isSameRouteRecord(to, from) || !oldInstance)) {(to.enterCallbacks[name] || []).forEach(callback => callback(instance));
}
}, {flush: 'post'});
而
viewRef.value
是在哪里赋值的呢?请看上面组件渲染的剖析
第 2 局部:组件渲染
setup()
中监听 routeToDisplay
变动触发组件从新渲染
当响应式数据发生变化时,会触发 setup()
从新渲染,调用 vue.h
进行新的 Component
的渲染更新,此时的 viewRef.value
通过 vue.h
赋值
const RouterViewImpl = /*#__PURE__*/ vue.defineComponent({
name: 'RouterView',
setup() {const viewRef = vue.ref();
return () => {
const route = routeToDisplay.value;
const currentName = props.name; // 默认值为 "default"
const matchedRoute = matchedRouteRef.value;
const ViewComponent = matchedRoute && matchedRoute.components[currentName];
const component = vue.h(ViewComponent, assign({}, routeProps, attrs, {
onVnodeUnmounted,
ref: viewRef,
}));
//... 省略了 routeProps 和 onVnodeUnmounted 相干代码逻辑
return (
// pass the vnode to the slot as a prop.
// h and <component :is="..."> both accept vnodes
normalizeSlot(slots.default, { Component: component, route}) ||
component);
}
}
});
RouterLink
从上面的代码块能够晓得,整个组件实质就是应用 useLink()
封装的一系列办法,而后渲染一个 "a"
标签,下面携带了点击事件、要跳转的href
、款式等等
因而这个组件的核心内容就是剖析
useLink()
到底封装了什么办法,这个局部咱们接下来会进行具体的剖析
const RouterLinkImpl = /*#__PURE__*/ vue.defineComponent({
name: 'RouterLink',
props: {
to: {type: [String, Object],
required: true,
},
replace: Boolean,
//...
},
useLink,
setup(props, { slots}) {const link = vue.reactive(useLink(props));
const {options} = vue.inject(routerKey);
const elClass = vue.computed(() => ({[getLinkClass(props.activeClass, options.linkActiveClass, 'router-link-active')]: link.isActive,
[getLinkClass(props.exactActiveClass, options.linkExactActiveClass, 'router-link-exact-active')]: link.isExactActive,
}));
return () => {const children = slots.default && slots.default(link);
return props.custom
? children
: vue.h('a', {
'aria-current': link.isExactActive
? props.ariaCurrentValue
: null,
href: link.href,
onClick: link.navigate,
class: elClass.value,
}, children);
};
},
});
组合式 API
onBeforeRouteLeave
在下面 app.use(router)
的剖析中,咱们能够晓得,一开始就会初始化 RouterView
组件,而在初始化时,咱们会进行 vue.provide(matchedRouteKey, matchedRouteRef)
,将目前匹配路由的matcher
放入到 key:matchedRouteKey
中
install(app) {
const router = this;
app.component('RouterLink', RouterLink);
app.component('RouterView', RouterView);
//...
}
const RouterViewImpl = /*#__PURE__*/ vue.defineComponent({
name: 'RouterView',
//...
setup(props, { attrs, slots}) {const routeToDisplay = vue.computed(() => props.route || injectedRoute.value);
const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]);
vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));
vue.provide(matchedRouteKey, matchedRouteRef);
return () => {//...}
}
})
在 onBeforeRouteLeave()
中,咱们拿到的 activeRecord
就是目前路由对应的 matcher
,而后将内部传入的leaveGuard
放入到咱们的 matcher["leaveGuard"]
中,在路由跳转的 navigate()
办法中进行调用
function onBeforeRouteLeave(leaveGuard) {if (!vue.getCurrentInstance()) {warn('getCurrentInstance() returned null. onBeforeRouteLeave() must be called at the top of a setup function');
return;
}
const activeRecord = vue.inject(matchedRouteKey,
// to avoid warning
{}).value;
if (!activeRecord) {warn('No active route record was found when calling `onBeforeRouteLeave()`. Make sure you call this function inside a component child of <router-view>. Maybe you called it inside of App.vue?');
return;
}
registerGuard(activeRecord, 'leaveGuards', leaveGuard);
}
function registerGuard(record, name, guard) {const removeFromList = () => {record[name].delete(guard);
};
vue.onUnmounted(removeFromList);
vue.onDeactivated(removeFromList);
vue.onActivated(() => {record[name].add(guard);
});
record[name].add(guard);
}
onBeforeRouteUpdate
跟下面剖析的 onBeforeRouteLeave
截然不同的流程,拿到目前路由对应的 matcher
,而后将内部传入的updateGuards
放入到咱们的 matcher["updateGuards"]
中,在路由跳转的 navigate()
办法中进行调用
function onBeforeRouteUpdate(updateGuard) {if (!vue.getCurrentInstance()) {warn('getCurrentInstance() returned null. onBeforeRouteUpdate() must be called at the top of a setup function');
return;
}
const activeRecord = vue.inject(matchedRouteKey,
// to avoid warning
{}).value;
if (!activeRecord) {warn('No active route record was found when calling `onBeforeRouteUpdate()`. Make sure you call this function inside a component child of <router-view>. Maybe you called it inside of App.vue?');
return;
}
registerGuard(activeRecord, 'updateGuards', updateGuard);
}
function registerGuard(record, name, guard) {const removeFromList = () => {record[name].delete(guard);
};
vue.onUnmounted(removeFromList);
vue.onDeactivated(removeFromList);
vue.onActivated(() => {record[name].add(guard);
});
record[name].add(guard);
}
useRouter
在之前的剖析 app.use(router)
中,咱们能够晓得,咱们将以后的 router
应用 provide
进行存储,即app.provide(routerKey, router);
install(app) {
//...
const router = this;
const reactiveRoute = {};
for (const key in START_LOCATION_NORMALIZED) {reactiveRoute[key] = vue.computed(() => currentRoute.value[key]);
}
app.provide(routerKey, router); // 如下面代码块所示
app.provide(routeLocationKey, vue.reactive(reactiveRoute));
app.provide(routerViewLocationKey, currentRoute); // 如下面代码块所示
// 省略 Vue.unmount 的一些逻辑解决...
}
因而 useRouter()
实质就是拿到目前 Vue
应用 Vue Router
示例
function useRouter() {return vue.inject(routerKey);
}
useRoute
从下面 useRouter()
的剖析中,咱们能够晓得,routeLocationKey
对应的就是以后路由 currentRoute
封装的响应式对象 reactiveRoute
因而 const route = useRoute()
代表的就是以后路由的响应式对象
function useRoute() {return vue.inject(routeLocationKey);
}
useLink
Vue Router
将 RouterLink
的外部行为作为一个组合式 API 函数公开。它提供了与 v-slotAPI 雷同的拜访属性:
import {RouterLink, useLink} from 'vue-router'
import {computed} from 'vue'
export default {
name: 'AppLink',
props: {
// 如果应用 TypeScript,请增加 @ts-ignore
...RouterLink.props,
inactiveClass: String,
},
setup(props) {const { route, href, isActive, isExactActive, navigate} = useLink(props)
const isExternalLink = computed(() => typeof props.to === 'string' && props.to.startsWith('http')
)
return {isExternalLink, href, navigate, isActive}
},
}
RouterLink
组件提供了足够的props
来满足大多数根本应用程序的需要,但它并未尝试涵盖所有可能的用例,在某些高级状况下,你可能会发现自己应用了v-slot
。在大多数中型到大型应用程序中,值得创立一个(如果不是多个)自定义RouterLink
组件,以在整个应用程序中重用它们。例如导航菜单中的链接,解决内部链接,增加inactive-class
等useLink()
是为了扩大RouterLink
而服务的
从上面的代码块能够晓得,userLink()
次要提供了
route
: 获取props.to
进行router.resolve()
,拿到要跳转的新路由的route
对象href
: 监听route.value.href
isActive
: 以后路由与props.to
局部匹配isExactActive
: 以后路由与props.to
齐全精准匹配navigate()
: 跳转办法,理论还是调用router.push(props.to)
/router.replace(props.to)
function useLink(props) {const router = vue.inject(routerKey);
const currentRoute = vue.inject(routeLocationKey);
const route = vue.computed(() => router.resolve(vue.unref(props.to)));
const activeRecordIndex = vue.computed(() => {//...});
const isActive = vue.computed(() => activeRecordIndex.value > -1 &&
includesParams(currentRoute.params, route.value.params));
const isExactActive = vue.computed(() => activeRecordIndex.value > -1 &&
activeRecordIndex.value === currentRoute.matched.length - 1 &&
isSameRouteLocationParams(currentRoute.params, route.value.params));
function navigate(e = {}) {if (guardEvent(e)) {return router[vue.unref(props.replace) ? 'replace' : 'push'](vue.unref(props.to)
// avoid uncaught errors are they are logged anyway
).catch(noop);
}
return Promise.resolve();}
return {
route,
href: vue.computed(() => route.value.href),
isActive,
isExactActive,
navigate,
};
}
其中 activeRecordIndex
的逻辑比较复杂,咱们摘出来独自剖析下
const activeRecordIndex = vue.computed(() => {const { matched} = route.value;
const {length} = matched;
const routeMatched = matched[length - 1];
const currentMatched = currentRoute.matched;
if (!routeMatched || !currentMatched.length)
return -1;
const index = currentMatched.findIndex(isSameRouteRecord.bind(null, routeMatched));
if (index > -1)
return index;
const parentRecordPath = getOriginalPath(matched[length - 2]);
return (
length > 1 &&
getOriginalPath(routeMatched) === parentRecordPath &&
currentMatched[currentMatched.length - 1].path !== parentRecordPath
? currentMatched.findIndex(isSameRouteRecord.bind(null, matched[length - 2]))
: index);
});
间接看下面的代码可能有些懵,间接去源码中找对应地位提交的
git
记录
从 RouterLink.spec.ts
中能够发现减少以下这段代码
it('empty path child is active as if it was the parent when on adjacent child', async () => {const { wrapper} = await factory(
locations.child.normalized,
{to: locations.childEmpty.string},
locations.childEmpty.normalized
)
expect(wrapper.find('a')!.classes()).toContain('router-link-active')
expect(wrapper.find('a')!.classes()).not.toContain('router-link-exact-active')
})
从 RouterLink.spec.ts
拿到 locations.child
和locations.childEmpty
的值,如下所示
child: {
string: '/parent/child',
normalized: {
fullPath: '/parent/child',
href: '/parent/child',
matched: [records.parent, records.child]
}
}
childEmpty: {
string: '/parent',
normalized: {
fullPath: '/parent',
href: '/parent',
matched: [records.parent, records.childEmpty]
}
}
下面理论就是进行 /parent/child
->/parent
的跳转,并且 childEmpty
对应的 /parent
还有两个 matched
元素,阐明它的子路由的 path
=""
联合下面 router-link-active
、router-link-exact-active
、/parent/child
->/parent
呈现的关键字,以及 Vue Router
源码提交记录的 App.vue
和router.ts
的批改记录,咱们能够构建出以下的测试文件,具体代码放在 github 地址
<div id="app-wrapper">
<div class="routerLinkWrapper">
<router-link to="/child/a"> 跳转到 /child/a</router-link>
</div>
<div class="routerLinkWrapper">
<router-link :to="{name:'WithChildren'}"> 跳转到父路由 /child</router-link>
</div>
<div class="routerLinkWrapper">
<router-link :to="{name:'default-child'}"> 跳转到没有门路的子路由 /child</router-link>
</div>
<!-- route outlet -->
<!-- component matched by the route will render here -->
<router-view></router-view>
</div>
const routes = [{path: '/', component: Home},
{
path: '/child',
component: TEST,
name: 'WithChildren',
children: [{ path: '', name:'default-child', component: TEST},
{path: 'a', name: 'a-child', component: TEST},
]
}
]
当咱们处于 home
路由时
当咱们点击跳转到 /child/a
路由时,咱们能够发现
- 齐全匹配的路由减少两个
class
:router-link-active
和router-link-exact-active
- 门路上只有
/child
匹配的路由减少了class
:router-link-active
从下面的测试后果中,咱们能够大略猜测出
如果 <route-link to=xxx>
中的 to
所代表的路由是能够在以后路由中找到的,则加上 router-link-active
和router-link-exact-active
如果 to
所代表的路由 (或者它的嵌套parent
路由,只有它 path
= 嵌套parent
路由的 path
) 是能够在以后路由的嵌套 parent
路由中找到的,加上 router-link-active
当初咱们能够尝试对 activeRecordIndex
代码进行剖析
const route = computed(() => router.resolve(unref(props.to)))
const activeRecordIndex = vue.computed(() => {const { matched} = route.value;
const {length} = matched;
const routeMatched = matched[length - 1];
const currentMatched = currentRoute.matched;
if (!routeMatched || !currentMatched.length)
return -1;
const index = currentMatched.findIndex(isSameRouteRecord.bind(null, routeMatched));
if (index > -1)
return index;
const parentRecordPath = getOriginalPath(matched[length - 2]);
return (
length > 1 &&
getOriginalPath(routeMatched) === parentRecordPath &&
currentMatched[currentMatched.length - 1].path !== parentRecordPath
? currentMatched.findIndex(isSameRouteRecord.bind(null, matched[length - 2]))
: index);
});
因为 <router-link>
初始化时就会注册,因而每一个 <router-link>
都会初始化下面的代码,进行 computed
的监听,而此时 route
代表传进来的路由,即
<router-link :to="{name:'default-child'}"> 跳转到没有门路的子路由 /child</router-link>
初始化时的 props.to
="{name:'default-child'}"
当目前的路由发生变化时,即 currentRoute
发生变化时,也会触发 computed(fn)
从新执行一次,此时会去匹配 props.to
="/child"
所对应的 matched[最初一位 index]
是否在 currentRoute
对应的 matched
找到,如果找到了,间接返回 index
如果找不到,则应用 <router-link>
目前对应的 props.to
的matched
的倒数第二个 matched[最初第二位 index]
,看看这个倒数第二个matched[最初第二位 index]
能不能在 currentRoute
对应的 matched
找到,如果找到了,间接返回index
应用
props.to
的matched 数组
的倒数第二个matched[最初第二位 index]
的前提是<router-link to="/child">
对应的路由是它path
= 嵌套parent
路由的path
那
activeRecordIndex
的用途是什么呢?
在下面 userLink
的源码剖析中,咱们晓得了 isActive
和isExactActive
的赋值是通过 computed
计算
const isActive = vue.computed(() => activeRecordIndex.value > -1 &&
includesParams(currentRoute.params, route.value.params));
const isExactActive = vue.computed(() => activeRecordIndex.value > -1 &&
activeRecordIndex.value === currentRoute.matched.length - 1 &&
isSameRouteLocationParams(currentRoute.params, route.value.params));
在嵌套路由中找到符合条件的 <router-link>
是isActive=true
,然而 isExactActive=false
只有合乎 currentRoute.matched.length - 1
条件下匹配的 <router-link>
才是 isActive=true
和isExactActive=true
而
isActive
和isExactActive
也就是增加router-link-active
/router-link-exact-active
的条件
issues 剖析
Vue Router
中有大量正文,其中蕴含一些issues
的正文,本文将进行简略地剖析Vue Router 4.1.6
源码中呈现的issues
正文每一个
issues
都会联合github
上的探讨记录以及作者提交的源码修复记录进行剖析,通过issues
的剖析,能够明确Vue Router 4.1.6
源码中很多看起来不晓得是什么货色的逻辑
issues/685 正文剖析
function changeLocation(to, state, replace) {
/**
* if a base tag is provided, and we are on a normal domain, we have to
* respect the provided `base` attribute because pushState() will use it and
* potentially erase anything before the `#` like at
* https://github.com/vuejs/router/issues/685 where a base of
* `/folder/#` but a base of `/` would erase the `/folder/` section. If
* there is no host, the `<base>` tag makes no sense and if there isn't a
* base tag we can just use everything after the `#`.
*/
const hashIndex = base.indexOf('#');
const url = hashIndex > -1
? (location.host && document.querySelector('base')
? base
: base.slice(hashIndex)) + to
: createBaseLocation() + base + to;
try {
// BROWSER QUIRK
// NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
history[replace ? 'replaceState' : 'pushState'](state, '', url);
historyState.value = state;
}
catch (err) {
{warn('Error with push/replace State', err);
}
// Force the navigation, this also resets the call count
location[replace ? 'replace' : 'assign'](url);
}
}
issues/685 问题形容
当部署我的项目到子目录后,拜访[https://test.vladovic.sk/router-bug/](https://test.vladovic.sk/router-bug/)
- 料想后果门路变为:
https://test.vladovic.sk/router-bug/#/
- 理论门路变为:
https://test.vladovic.sk/#/
提出
issues
的人还提供了正确部署链接:[https://test.vladovic.sk/router](https://test.vladovic.sk/router)
,可能失常跳转到https://test.vladovic.sk/router/#/
,跟谬误链接代码的区别在于<html>
文件应用了<base href="/">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<base href="/">
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
HTML <base> 元素 指定用于一个文档中蕴含的所有绝对 URL 的根 URL。一份中只能有一个 <base> 元素。
一个文档的根本 URL,能够通过应用 document.baseURI(en-US) 的 JS 脚本查问。如果文档不蕴含 <base> 元素,baseURI 默认为 document.location.href。
issues/685 问题产生的起因
在 VueRouter 4.0.2
版本中,changeLocation()
间接应用 base.indexOf("#")
进行前面字段的截取
function changeLocation(to, state, replace) {
// when the base has a `#`, only use that for the URL
const hashIndex = base.indexOf('#');
const url = hashIndex > -1
? base.slice(hashIndex) + to
: createBaseLocation() + base + to;
try {
// BROWSER QUIRK
// NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
history[replace ? 'replaceState' : 'pushState'](state, '', url);
historyState.value = state;
}
catch (err) {if ((process.env.NODE_ENV !== 'production')) {warn('Error with push/replace State', err);
}
else {console.error(err);
}
// Force the navigation, this also resets the call count
location[replace ? 'replace' : 'assign'](url);
}
}
将产生问题的我的项目 git clone
在本地运行,关上 http://localhost:8080/router-bug/
时,初始化时会触发 changeLocation("/")
,从而触发history['pushState'](state, '','#/')
逻辑
- 目前的
location.href
是http://localhost:8080/router-bug/
<html></html>
对应的<base href="/">
- 触发了
history['pushState'](state, '','#/')
下面 3 种条件使得目前的链接更改为:http://localhost:8080/#/
issues/685 修复剖析
在 github issues 的一开始的探讨中,最先倡议的是更改 <html></html>
对应的 <base href="/router-bug/">
,因为history.pushState
会用到 <base>
属性
前面提交了一个修复记录,如下图所示:
那为什么要减少
<base>
标签的判断呢?
咱们在一开始初始化的时候就晓得,createWebHashHistory()
反对传入一个 base
默认的字符串,如果不传入,则取 location.pathname+location.search
,在下面http://localhost:8080/router-bug/
这个例子中,咱们是没有传入一个默认的字符串,因而base
="/router-bug/#"
function createWebHashHistory(base) {
// Make sure this implementation is fine in terms of encoding, specially for IE11
// for `file://`, directly use the pathname and ignore the base
// href="https://example.com" 的 location.pathname 也是 "/"
base = location.host ? (base || location.pathname + location.search) : '';
// allow the user to provide a `#` in the middle: `/base/#/app`
if (!base.includes('#'))
base += '#';
return createWebHistory(base);
}
<base>
HTML 元素指定用于文档中所有绝对 URL 的根本 URL。如果文档没有 <base>
元素,则 baseURI
默认为 location.href
很显著,目前 <html></html>
对应的 <base href="/">
跟目前的 base
="/router-bug/#"
是抵触的,因为 url
=base.slice(hashIndex)+to
是建设在 history.pushState
对应的地址蕴含 location.pathname
的前提下,而可能存在 <html></html>
对应的 <base>
就是不蕴含location.pathname
// VueRouter 4.0.2,未修复前的代码
function changeLocation(to, state, replace) {
// when the base has a `#`, only use that for the URL
const hashIndex = base.indexOf('#');
const url = hashIndex > -1
? base.slice(hashIndex) + to
: createBaseLocation() + base + to;}
因而当咱们检测到 <html></html>
存在 <base>
标签时,咱们间接应用 base
(通过格式化解决过的,蕴含location.pathname
)的字符串进行以后history.pushState()
地址的拼接,杜绝这种可能抵触的状况
上面代码中的
base
不是<html></html>
的<base>
标签!!是咱们解决过的base
字符串
// VueRouter 修复后的代码
function changeLocation(to, state, replace) {const hashIndex = base.indexOf('#');
const url =
hashIndex > -1
? (location.host && document.querySelector('base')
? base
: base.slice(hashIndex)) + to
: createBaseLocation() + base + to}
issues/366 正文剖析
function push(to, data) {
// Add to current entry the information of where we are going
// as well as saving the current position
const currentState = assign({},
// use current history state to gracefully handle a wrong call to
// history.replaceState
// https://github.com/vuejs/router/issues/366
historyState.value, history.state, {
forward: to,
scroll: computeScrollPosition(),});
if (!history.state) {
warn(`history.state seems to have been manually replaced without preserving the necessary values. Make sure to preserve existing history state if you are manually calling history.replaceState:\n\n` +
`history.replaceState(history.state, '', url)\n\n` +
`You can find more information at https://next.router.vuejs.org/guide/migration/#usage-of-history-state.`);
}
changeLocation(currentState.current, currentState, true);
const state = assign({}, buildState(currentLocation.value, to, null), {position: currentState.position + 1}, data);
changeLocation(to, state, false);
currentLocation.value = to;
}
issues/366 问题形容
这个 issues
蕴含了两个人的反馈
手动 history.replaceState 没有传递以后 state,手动触发 router.push
报错
开发者甲在 router.push(...)
之前调用window.history.replaceState(...)
window.history.replaceState({}, '', ...)
router.push(...)
而后就报错:router push gives DOMException: Failed to execute 'replace' on 'Location': 'http://localhost:8080undefined' is not a valid URL
currentState.current 拿不到具体的地址
function push(to, data) {const currentState = assign({}, history.state, {
forward: to,
scroll: computeScrollPosition(),});
changeLocation(currentState.current, currentState, true); // this is the line that fails
}
跳转到受权页面,受权胜利后回来进行 router.push
报错
开发者乙从 A 页面跳转到 B 页面后,B 页面受权胜利后,重定向回来 A 页面,而后调用 router.push()
报错:router push gives DOMException: Failed to execute 'replace' on 'Location': 'http://localhost:8080undefined' is not a valid URL
currentState.current 拿不到具体的地址
function push(to, data) {const currentState = assign({}, history.state, {
forward: to,
scroll: computeScrollPosition(),});
changeLocation(currentState.current, currentState, true); // this is the line that fails
}
issues/366 问题产生的起因
Vue Router
作者在官网文档中强调:
Vue Router 将信息保留在 history.state
上。如果你有任何手动调用 history.pushState()
的代码,你应该防止它,或者用的 router.push()
和 history.replaceState()
进行重构:
// 将
history.pushState(myState, '', url)
// 替换成
await router.push(url)
history.replaceState({...history.state, ...myState}, '')
同样,如果你在调用 history.replaceState() 时没有保留以后状态,你须要传递以后 history.state:
// 将
history.replaceState({}, '', url)
// 替换成
history.replaceState(history.state, '', url)
起因:咱们应用历史状态来保留导航信息,如滚动地位,以前的地址等。
以上内容摘录于 Vue Router 官网文档
开发者甲的问题将以后的 history.state
增加进去后就解决了问题,即
// 将
history.replaceState({}, '', url)
// 替换成
history.replaceState(history.state, '', url)
而开发者乙的问题是在 Vue Router 的 A 页面
-> B 页面
->Vue Router 的 A 页面
的过程中失落了以后的history.state
开发者乙也在应用的受权开源库提了 amplify-js issues
Vue Router
作者则倡议以后 history.state
应该保留,不应该革除
因而无论开发者甲还是开发者乙,实质都是没有保留好以后的 history.state
导致的谬误
issues/366 修复剖析
通过下面问题的形容以及起因剖析后,咱们晓得要修复问题的要害就是保留好以后的 history.state
因而 Vue Router
作者间接应用变量 historyState
来进行数据合并
当你理论的 window.history.state
失落时,咱们还有一个本人保护的 historyState
数据,它在失常路由状况下 historyState.value
就等于 window.history.state
无论因为什么起因失落了以后浏览器本身的 window.history.state
,最起码有个本人定义的historyState.value
,就能保障currentState.current
不会为空,执行到语句 history.replaceState
或者 history.pushState
时都能有正确的state
如上面代码块所示,
historyState.value
初始化就是history.state
,并且在每次路由变动时都实时同步更新
function useHistoryStateNavigation(base) {const historyState = { value: history.state};
function changeLocation(to, state, replace) {
//...
history[replace ? 'replaceState' : 'pushState'](state, '', url)
historyState.value = state;
}
}
function useHistoryListeners(base, historyState, currentLocation, replace) {const popStateHandler = ({ state,}) => {const to = createCurrentLocation(base, location);
//...
if (state) {
currentLocation.value = to;
historyState.value = state;
}
}
window.addEventListener('popstate', popStateHandler);
}
issues/328 剖析
function resolve(rawLocation, currentLocation) {
//...
return assign({
fullPath,
// keep the hash encoded so fullPath is effectively path + encodedQuery +
// hash
hash,
query:
// if the user is using a custom query lib like qs, we might have
// nested objects, so we keep the query as is, meaning it can contain
// numbers at `$route.query`, but at the point, the user will have to
// use their own type anyway.
// https://github.com/vuejs/router/issues/328#issuecomment-649481567
stringifyQuery$1 === stringifyQuery
? normalizeQuery(rawLocation.query)
: (rawLocation.query || {}),
}, matchedRoute, {
redirectedFrom: undefined,
href,
});
}
issues/328 问题形容
在 Vue Router 4.0.0-alpha.13
版本中,并没有思考 query
嵌套的状况,比方上面这种状况
<div id="app">
目前的地址是:{{$route.fullPath}}
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link :to="{query: { users: { page: 1} } }">Page 1</router-link></li>
<li><router-link :to="{query: { users: { page: 2} } }">Page 2</router-link></li>
</ul>
<router-view></router-view>
</div>
点击 Page1
,$route.fullPath
=/?users%5Bpage%5D=1
点击 Page2
,$route.fullPath
还是/?users%5Bpage%5D=1
现实状况下,应该有变动,
$route.fullPath
会变为/?users%5Bpage%5D=2
issues/328 问题产生的起因
在 Vue Router 4.0.0-alpha.13
版本中,在进行路由跳转时,会触发 isSameRouteLocation()
的检测
function push(to) {return pushWithRedirect(to);
}
function pushWithRedirect(to, redirectedFrom) {const targetLocation = (pendingLocation = resolve(to));
//...
if (!force && isSameRouteLocation(from, targetLocation)) {failure = createRouterError(4 /* NAVIGATION_DUPLICATED */, { to: toLocation, from});
}
return (failure ? Promise.resolve(failure) : navigate(toLocation, from));
}
在 isSameRouteLocation()
中,会应用 isSameLocationObject(a.query, b.query)
进行检测,如果此时的 query
是两个嵌套的 Object
数据,会返回 true
,导致Vue Router
认为是同一个路由,无奈跳转胜利,天然也无奈触发 $route.fullPath
扭转
function isSameRouteLocation(a, b) {
//...
return (aLastIndex > -1 &&
aLastIndex === bLastIndex &&
isSameRouteRecord(a.matched[aLastIndex], b.matched[bLastIndex]) &&
isSameLocationObject(a.params, b.params) &&
isSameLocationObject(a.query, b.query) &&
a.hash === b.hash);
}
function isSameLocationObject(a, b) {if (Object.keys(a).length !== Object.keys(b).length)
return false;
for (let key in a) {if (!isSameLocationObjectValue(a[key], b[key]))
return false;
}
return true;
}
function isSameLocationObjectValue(a, b) {return Array.isArray(a)
? isEquivalentArray(a, b)
: Array.isArray(b)
? isEquivalentArray(b, a)
: a === b;
}
function isEquivalentArray(a, b) {return Array.isArray(b)
? a.length === b.length && a.every((value, i) => value === b[i])
: a.length === 1 && a[0] === b;
}
从下面的代码能够晓得,如果咱们的
a.query
是一个嵌套Object
,最终会触发isSameLocationObjectValue()
的a===b
的比拟,最终应该会返回false
才对,那为什么会返回true
呢?
那是因为在调用 isSameRouteLocation()
之前会进行 resolve(to)
操作,而在这个办法中,咱们会进行 normalizeQuery(rawLocation.query)
,无论rawLocation.query
是嵌套多少层的 Object
,normalizeQuery()
中的 ''+ value
都会变成 '[object Object]'
,因而导致了a===b
的比拟理论就是'[object Object]'
==='[object Object]'
,返回true
function createRouter(options) {function resolve(rawLocation, currentLocation) {
return assign({
fullPath,
hash,
query: normalizeQuery(rawLocation.query),
}, matchedRoute, {
redirectedFrom: undefined,
href: routerHistory.base + fullPath,
});
}
function push(to) {return pushWithRedirect(to);
}
function pushWithRedirect(to, redirectedFrom) {const targetLocation = (pendingLocation = resolve(to));
//...
if (!force && isSameRouteLocation(from, targetLocation)) {failure = createRouterError(4 /* NAVIGATION_DUPLICATED */, { to: toLocation, from});
}
}
}
function normalizeQuery(query) {const normalizedQuery = {};
for (let key in query) {let value = query[key];
if (value !== undefined) {normalizedQuery[key] = Array.isArray(value)
? value.map(v => (v == null ? null : '' + v))
: value == null
? value
: '' + value;
}
}
return normalizedQuery;
}
issues/328 修复剖析
初始化可传入 stringifyQuery
,外部对query
不进行解决
因为 query
可能是简单的构造,因而修复该问题第一思考的点就是放开给开发者本人解析
开发者能够在初始化 createRouter()
传入自定义的 parseQuery()
和stringifyQuery()
办法,开发者自行解析和转化目前的 query
参数
- 如果开发者不传入自定义的
stringifyQuery()
办法,那么stringifyQuery
就会等于originalStringifyQuery
(一个Vue Router
内置的stringifyQuery
办法),这个时候query
就会应用normalizeQuery(rawLocation.query)
进行数据的整顿,最终返回的还是一个Object
对象 - 如果开发者传入自定义的
stringifyQuery()
办法,那么就不会触发任何解决,还是应用rawLocation.query
,在下面示例中就是一个嵌套的Object
对象,防止应用normalizeQuery()
将''+ value
变成'[object Object]'
的状况
isSameRouteLocation
比拟 query
时,应用它们 stringify
解决后的字符串进行比拟
开发者能够自定义传入 stringifyQuery()
进行简单构造的解决,而后返回字符串进行比拟
如果不传入stringifyQuery()
,则应用默认的办法进行stringify
,而后依据返回的字符串进行比拟
默认办法的
stringify
不会思考简单的数据结构,只会当做一般对象进行stringify
issues/1124 剖析
function insertMatcher(matcher) {
let i = 0;
while (i < matchers.length &&
comparePathParserScore(matcher, matchers[i]) >= 0 &&
// Adding children with empty path should still appear before the parent
// https://github.com/vuejs/router/issues/1124
(matcher.record.path !== matchers[i].record.path ||
!isRecordChildOf(matcher, matchers[i])))
i++;
matchers.splice(i, 0, matcher);
// only add the original record to the name map
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher);
}
issues/1124 问题形容
在 Vue Router 4.0.11
中
-
应用动静增加路由
addRoute()
为以后的name="Root"
路由增加子路由后- 咱们想要拜访
Component:B
,应用了router.push("/")
,然而无奈渲染出Component:B
,它渲染的是它的上一级门路Component: Root
- 而如果咱们应用
router.push({name: 'Home'})
时,就能失常拜访Component:B
- 咱们想要拜访
- 如果咱们不应用动静增加路由,间接在初始化的时候,如上面正文
children
那样,间接增加Component:B
,当咱们应用router.push("/")
,能够失常渲染出Component:B
const routes = [
{
path: '/',
name: 'Root',
component: Root,
// Work with non dynamic add and empty path
/*children: [
{
path: '',
component: B
}
]*/
}
];
// Doesn't work with empty path and dynamic adding
router.addRoute('Root', {
path: '',
name: 'Home',
component: B
});
issues/1124 问题产生的起因
当动态增加路由时,因为是递归调用 addRoute()
,即父addRoute()
-> 子addRoute
-> 子insertMatcher()
-> 父addRoute()
,因而子matcher
是排在父 matcher
后面的,因为从《Vue3 相干源码 -Vue Router 源码解析 (一)》文章中计算路由权重的逻辑能够晓得,门路雷同分数则雷同,comparePathParserScore()
的值为0
,因而先调用insertMatcher()
,地位就越靠前,因而子路由地位靠前
function addRoute(record, parent, originalRecord) {if ('children' in mainNormalizedRecord) {
const children = mainNormalizedRecord.children
for (let i = 0; i < children.length; i++) {
addRoute(children[i],
matcher,
originalRecord && originalRecord.children[i]
)
}
}
//...
insertMatcher(matcher);
}
function insertMatcher(matcher) {
let i = 0;
while (i < matchers.length &&
comparePathParserScore(matcher, matchers[i]) >= 0 &&
// Adding children with empty path should still appear before the parent
// https://github.com/vuejs/router/issues/1124
(matcher.record.path !== matchers[i].record.path))
i++;
matchers.splice(i, 0, matcher);
// only add the original record to the name map
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher);
}
然而动静增加路由时,同样是触发 matcher.addRoute()
,只不过因为是动静增加,因而要增加的新路由的parent
之前曾经插入到 matchers
数组中去了,由下面的剖析能够晓得,路由权重雷同,因而先调用insertMatcher()
,地位就越靠前,此时子路由地位靠后
function createRouter(options) {function addRoute(parentOrRoute, route) {
let parent;
let record;
if (isRouteName(parentOrRoute)) {parent = matcher.getRecordMatcher(parentOrRoute);
record = route;
}
else {record = parentOrRoute;}
return matcher.addRoute(record, parent);
}
}
function createRouterMatcher(routes, globalOptions) {function addRoute(record, parent, originalRecord) {if ('children' in mainNormalizedRecord) {
const children = mainNormalizedRecord.children
for (let i = 0; i < children.length; i++) {
addRoute(children[i],
matcher,
originalRecord && originalRecord.children[i]
)
}
}
//...
insertMatcher(matcher);
}
}
在上一篇文章《Vue3 相干源码 -Vue Router 源码解析 (一)》的剖析中,咱们晓得,当咱们应用router.push("/")
或者 router.push({path: "/"})
时,会触发正则表达式的匹配,如上面代码所示,matchers.find()
会优先匹配地位靠前的路由matcher
,从而产生了动静增加子路由找不到子路由(地位比拟靠后),初始化增加子路由可能渲染子路由(地位比拟靠前)的状况
path = location.path;
matcher = matchers.find(m => m.re.test(path));
if (matcher) {params = matcher.parse(path);
name = matcher.record.name;
}
issues/1124 修复剖析
既然是增加程序导致的问题,那么只有让子路由动静增加时,遇到它的 parent
不要 i++
即可,如上面修复提交代码所示,当子路由动静增加时,检测目前比照的是不是它的 parent
,如果是它的parent
,则阻止i++
,子路由地位就能够顺利地放在它的parent
后面
issues/916 剖析
很长的一段
navigte()
失败之后的解决 ….. 简化下
function setupListeners() {removeHistoryListener = routerHistory.listen((to, _from, info) => {
//...
const toLocation = resolve(to);
const from = currentRoute.value;
navigate(toLocation, from)
.catch((error) => {if (isNavigationFailure(error, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {return error;}
if (isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) {pushWithRedirect(error.to, toLocation)
.then(failure => {
// manual change in hash history #916 ending up in the URL not
// changing, but it was changed by the manual url change, so we
// need to manually change it ourselves
if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |
16 /* ErrorTypes.NAVIGATION_DUPLICATED */) &&
!info.delta &&
info.type === NavigationType.pop) {routerHistory.go(-1, false);
}
})
.catch(noop);
// avoid the then branch
return Promise.reject();}
// do not restore history on unknown direction
if (info.delta) {routerHistory.go(-info.delta, false);
}
// unrecognized error, transfer to the global handler
return triggerError(error, toLocation, from);
})
.then((failure) => {
failure =
failure ||
finalizeNavigation(
// after navigation, all matched components are resolved
toLocation, from, false);
// revert the navigation
if (failure) {
if (info.delta &&
// a new navigation has been triggered, so we do not want to revert, that will change the current history
// entry while a different route is displayed
!isNavigationFailure(failure, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {routerHistory.go(-info.delta, false);
} else if (info.type === NavigationType.pop &&
isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) {
// manual change in hash history #916
// it's like a push but lacks the information of the direction
routerHistory.go(-1, false);
}
}
triggerAfterEach(toLocation, from, failure);
})
.catch(noop);
});
}
function setupListeners() {removeHistoryListener = routerHistory.listen((to, _from, info) => {
//...
const toLocation = resolve(to);
const from = currentRoute.value;
navigate(toLocation, from)
.catch((error) => {
// error 是 NAVIGATION_ABORTED/NAVIGATION_CANCELLED
return error;
// error 是 NAVIGATION_GUARD_REDIRECT
pushWithRedirect(error.to, toLocation)
.then(failure => {
if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |
16 /* ErrorTypes.NAVIGATION_DUPLICATED */) &&
!info.delta &&
info.type === NavigationType.pop) {routerHistory.go(-1, false);
}
})
})
.then((failure) => {if (failure) {
if (info.type === NavigationType.pop &&
isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |
16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) {routerHistory.go(-1, false);
}
}
})
});
}
issues/916 问题形容
目前有两个路由,/login
路由和 /about
路由,它们配置了一个全局的导航守卫,当遇到 /about
路由时,会重定向到 /login
路由
router.beforeEach((to, from) => {if (to.path.includes('/login')) {return true;} else {
return {path: '/login'}
}
})
目前问题是:
- 目前是
/login
路由,各方面失常,手动在浏览器地址更改/login
->/about
- 冀望行为是:因为
router.beforeEach
配置了重定向跳转,浏览器地址会从新变为/login
,页面也还是保留在Login
组件 - 理论体现是:浏览器地址是
/about
,页面保留在Login
组件,造成浏览器地址跟理论映射组件不合乎的 bug
issues/916 问题产生的起因
issues/916 修复剖析
如果导航守卫 next()
返回的是路由数据,会触发popStateHandler()
->navigate()
->pushWithRedirect()
,而后返回NAVIGATION_DUPLICATED
,因而咱们要做的就是回退一个路由,并且不触发组件更新,即routerHistory.go(-1, false)
因为
NAVIGATION_DUPLICATED
就意味着要重定向的这个新路由(/login
)跟启动重定向路由 (/about
) 之前的路由(/login
)是反复的,那么这个启动重定向路由 (/about
) 就得回退,因为它(/about
)势必会不胜利
除了在 pushWithRedirect()
上修复谬误之外,还在 navigate().then()
上也进行同种状态的判断
这是为什么呢?除了
next("/login")
之外,还有状况导致谬误吗?
这是因为除了 next("/login")
,还有一种可能就是next(false)
也会导致谬误,即
router.beforeEach((to, from) => {if (to.path.includes('/login')) {return true;} else {
return false;
// return {
// path: '/login'
// }
}
})
当重定向路由改为 false
时,如上面代码块所示,navigate()
会返回 NAVIGATION_ABORTED
的谬误,从而触发 navigate().catch(()=> error)
而Promise.catch()
中返回值,这个值也会包裹触发下一个 then()
,也就是说NAVIGATION_ABORTED
会传递给 navigate().catch().then()
中,因而还须要在 then()
外面进行routerHistory.go(-1, false)
NAVIGATION_ABORTED
意味着这种routerHistory.listen
传递的to
路由因为next()
返回false
而勾销,因而须要回退一个路由,因为这个to
路由曾经扭转浏览器的记录了!
function setupListeners() {removeHistoryListener = routerHistory.listen((to, _from, info) => {
//...
const toLocation = resolve(to);
const from = currentRoute.value;
navigate(toLocation, from)
.catch((error) => {
// error 是 NAVIGATION_ABORTED/NAVIGATION_CANCELLED
return error;
})
.then((failure) => {if (failure) {
if (info.type === NavigationType.pop &&
isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |
16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) {routerHistory.go(-1, false);
}
}
})
});
}
总结
1. 内部定义的路由,是如何在 Vue Router
外部建立联系的?
Vue Router
反对多种门路写法,动态门路、一般的动静门路以及动静门路和正则表达式的联合
初始化时会对 routes
进行解析,依据多种门路的不同状态解析出对应的正则表达式、路由权重和路由其它数据,包含组件名称、组件等等
2. Vue Router
是如何实现 push
、replace
、pop
操作的?
-
push
/replace
- 通过
resolve()
整顿出跳转数据的对象,该对象包含找到一开始初始化routes
对应的matched
以及跳转的残缺门路 - 而后通过
navigate()
进行一系列导航守卫的调用 - 而后通过
changeLocation()
,也就是window.history.pushState/replaceState()
实现的以后浏览器门路替换 - 更新目前的
currentRoute
,从而触发<route-view>
中的injectedRoute
->routeToDisplay
等响应式数据发生变化,从而触发<route-view>
的setup
函数从新渲染currentRoute 的 matched
携带的Component
,实现路由更新性能
- 通过
-
pop
- 初始化会进行
setupListeners()
进行routerHistory.listen
事件的监听 - 监听
popstate
事件,后退事件触发时,触发初始化监听的routerHistory.listen
事件 - 通过
resolve()
整顿出跳转数据的对象,该对象包含找到一开始初始化routes
对应的matched
以及跳转的残缺门路 - 而后通过
navigate()
进行一系列导航守卫的调用 - 而后通过
changeLocation()
,也就是window.history.pushState/replaceState()
实现的以后浏览器门路替换 - 更新目前的
currentRoute
,从而触发<route-view>
中的injectedRoute
->routeToDisplay
等响应式数据发生变化,从而触发<route-view>
的setup
函数从新渲染currentRoute 的 matched
携带的Component
,实现路由更新性能
- 初始化会进行
3. Vue Router
是如何命中多层嵌套路由,比方 /parent/child/child1
须要加载多个组件,是如何实现的?
在每次路由跳转的过程中,会解析出以后路由对应的 matcher
对象,并且将它所有的 parent matcher
都退出到 matched
数组中
在实现 push
、replace
、pop
操作时,每一个路由都会在 router-view
中计算出目前对应的嵌套深度,而后依据嵌套深度,拿到下面 matched
对应的item
,实现路由组件的渲染
4. Vue Router
有什么导航守卫?触发的流程是怎么的?
通过 Promise
链式顺序调用多个导航守卫,在每次路由跳转时,会触发 push/replace()
->navigate()
->finalizeNavigation()
,导航守卫就是在navigate()
中进行链式调用,能够在其中一个导航守卫中中断流程,从而中断整个路由的跳转
参考官网文档的材料,咱们能够推断出
/child1
->/child2
导航守卫的调用程序为
- 【组件守卫】在失活的组件里调用
beforeRouteLeave
守卫 - 【全局守卫】
beforeEach
- 【路由守卫】
beforeEnter
- 解析异步路由组件
- 【组件守卫】在被激活的组件里调用
beforeRouteEnter
(无法访问 this,实例未创立) - 【全局守卫】
beforeResolve
- 导航被确认
- 【全局守卫】
afterEach
- 【vue 生命周期】
beforeCreate
、created
、beforeMount
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创立好的组件实例会作为回调函数的参数传入 -
【vue 生命周期】
mounted
参考官网文档的材料,咱们能够推断前途由
/user/:id
从/user/a
->/user/b
导航守卫的调用程序为 - 【全局守卫】
beforeEach
- 【组件守卫】在重用的组件里调用
beforeRouteUpdate
守卫(2.2+) - 【全局守卫】
beforeResolve
- 【全局守卫】
afterEach
- 【vue 生命周期】
beforeUpdate
、updated
5. Vue Router
的导航守卫是如何做到链式调用的?
在 navigate()
的源码中,咱们截取 beforeEach
和beforeRouteUpdate
的片段进行剖析,次要波及有几个点:
runGuardQueue(guards)
实质就是链式不停地调用promise.then()
,而后执行guards[x]()
,最终实现某一个阶段,比方beforeEach
阶段之后,再应用guards
收集下一个阶段的function
数组,而后再启用runGuardQueue(guards)
应用promise.then()
一直执行guards
外面的办法guards.push()
增加的是一个Promise
,传递的是在内部注册的办法guard
、to
、from
三个参数
function navigate(to, from) {return (runGuardQueue(guards)
.then(() => {
// check global guards beforeEach
guards = [];
for (const guard of beforeGuards.list()) {guards.push(guardToPromiseFn(guard, to, from));
}
guards.push(canceledNavigationCheck);
return runGuardQueue(guards);
})
.then(() => {
// check in components beforeRouteUpdate
guards = extractComponentsGuards(updatingRecords, 'beforeRouteUpdate', to, from);
for (const record of updatingRecords) {
record.updateGuards.forEach(guard => {guards.push(guardToPromiseFn(guard, to, from));
});
}
guards.push(canceledNavigationCheck);
// run the queue of per route beforeEnter guards
return runGuardQueue(guards);
})
//....
)
}
function runGuardQueue(guards) {
return guards.reduce((promise, guard) => promise.then(() => guard()),
Promise.resolve());
}
对于 guards.push()
增加的 Promise
,会判断内部function
(也就是guard
)有多少个参数,而后调用不同的条件逻辑,最终依据内部注册的办法,比方beforeEach()
返回的值,进行 Promise()
的返回,从而造成链式调用
Function: length
就是有多少个parameters
参数,guard.length
次要区别在于传不传next
参数,能够看上面代码块的正文局部
如果你在内部的办法,比方beforeEach
携带了next
参数,你就必须调用它,不然会报错
//router.beforeEach((to, from)=> {return false;}
//router.beforeEach((to, from, next)=> {next(false);}
function guardToPromiseFn(guard, to, from, record, name) {
// keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place
const enterCallbackArray = record &&
// name is defined if record is because of the function overload
(record.enterCallbacks[name] = record.enterCallbacks[name] || []);
return () => new Promise((resolve, reject) => {const next = (valid) => {resolve();
};
const guardReturn = guard.call(record && record.instances[name], to, from, canOnlyBeCalledOnce(next, to, from));
let guardCall = Promise.resolve(guardReturn);
if (guard.length < 3)
guardCall = guardCall.then(next);
if (guard.length > 2) {//... 解决有 next()的状况
}
guardCall.catch(err => reject(err));
});
}
function canOnlyBeCalledOnce(next, to, from) {
let called = 0;
return function () {
next._called = true;
if (called === 1)
next.apply(null, arguments);
};
}
6. Vue Router
的 beforeRouteEnter
和beforeRouteUpdate
的触发机会
如果复用一个路由,比方 /user/:id
会导致不同的 path
会应用同一个路由,那么就不会调用 beforeRouteEnter
,因而咱们须要在beforeRouteUpdate
获取数据
export default {data() {
return {
post: null,
error: null,
}
},
beforeRouteEnter(to, from, next) {getPost(to.params.id, (err, post) => {next(vm => vm.setData(err, post))
})
},
// 路由扭转前,组件就曾经渲染完了
// 逻辑稍稍不同
async beforeRouteUpdate(to, from) {
this.post = null
try {this.post = await getPost(to.params.id)
} catch (error) {this.error = error.toString()
}
},
7. Vue Router
中 route
和router
的区别
router
是目前 Vue
应用 Vue Router
示例,具备多个办法和多个对象数据,蕴含 currentRoute
route
是以后路由的响应式对象,内容实质就是currentRoute
8. hash
模式跟 h5 history
模式在 Vue Router
中有什么区别?
hash
模式:监听浏览器地址 hash
值变动,应用 pushState/replaceState
进行路由地址的扭转,从而触发组件渲染扭转,不须要服务器配合配置对应的地址 history
模式:扭转浏览器 url
地址,从而触发浏览器向服务器发送申请,须要服务器配合配置对应的地址
9. Vue Router
的 hash
模式重定向后还会保留浏览记录吗?比方重定向后再应用 router.go(-1)
会返回重定向之前的页面吗?
在 Vue Router
的hash
模式中,如果产生重定向,从 push()
->pushWithRedirect()
的源码能够晓得,会在 navigate()
之前就进行重定向的跳转,因而不会触发 finalizeNavigation()
的pushState()
办法往浏览器中留下记录,因而不会在 Vue Router
的hash
模式中,不会保留浏览器 history state
记录
function pushWithRedirect(to, redirectedFrom) {const targetLocation = (pendingLocation = resolve(to));
const from = currentRoute.value;
const data = to.state;
const force = to.force;
// to could be a string where `replace` is a function
const replace = to.replace === true;
const shouldRedirect = handleRedirectRecord(targetLocation);
if (shouldRedirect) {
//... 解决重定向的逻辑
return pushWithRedirect(...)
}
// if it was a redirect we already called `pushWithRedirect` above
const toLocation = targetLocation;
toLocation.redirectedFrom = redirectedFrom;
//... 解决 SameRouteLocation 的状况
// ... 去除 failure 的解决,默认都胜利
return navigate(toLocation, from)
.then((failure) => {failure = finalizeNavigation(toLocation, from, true, replace, data);
triggerAfterEach(toLocation, from, failure);
return failure;
});
}
10. Vue Router
的 hash
模式什么中央最容易导致路由切换失败?
在之前的剖析中,咱们能够晓得,咱们能够应用 go(-1, false)
进行 pauseListeners()
的调用
function go(delta, triggerListeners = true) {if (!triggerListeners)
historyListeners.pauseListeners();
history.go(delta);
}
function listen(callback) {
// set up the listener and prepare teardown callbacks
listeners.push(callback);
const teardown = () => {const index = listeners.indexOf(callback);
if (index > -1)
listeners.splice(index, 1);
};
teardowns.push(teardown);
return teardown;
}
function pauseListeners() {pauseState = currentLocation.value;}
从下面的代码中,咱们能够总结出几个要害的问题:
pauseListeners()
是如何暂停监听办法执行的?pauseState
什么时候应用?- 什么时候触发
listen()
注册监听办法? listen()
注册的监听办法有什么用途?- 为什么要应用
pauseListeners()
暂停监听?
pauseListeners()
是如何暂停监听办法执行的?
pauseState
是如何做到暂停监听办法执行的?
当触发后退事件时,会检测 pauseState
是否存在以及是否等于后退之前的路由,如果是的话,则间接 return
,阻止后续的listeners
的循环调用,达到暂停 listeners
的目标
const popStateHandler = ({state,}) => {
const from = currentLocation.value;
//....
let delta = 0;
if (state) {
//....
// ignore the popstate and reset the pauseState
if (pauseState && pauseState === from) {
pauseState = null;
return;
}
delta = fromState ? state.position - fromState.position : 0;
} else {replace(to);
}
listeners.forEach(listener => {//...});
};
window.addEventListener('popstate', popStateHandler);
什么时候触发 listen()
注册监听办法?
在上一篇文章的 setupListeners()
注册 pop
操作相干监听办法的剖析中,咱们能够晓得,初始化 app.use(router)
会触发 router.install(app)
,而后进行一次push()
操作,此时就是初始化阶段 !ready
,push()
->navigate()
->finalizeNavigation()
,而后触发listen()
注册监听办法
function finalizeNavigation(toLocation, from, isPush, replace, data) {markAsReady();
}
function markAsReady(err) {if (!ready) {
ready = !err;
setupListeners();}
return err;
}
function setupListeners() {removeHistoryListener = routerHistory.listen((to, _from, info) => {//...navigate()
});
}
listen()
监听办法有什么用途?
在上一篇文章的 setupListeners()
注册 pop
操作相干监听办法的剖析中,咱们能够晓得,在原生 popstate
后退事件触发时,会触发对应的 listeners
监听办法执行,将以后的路由作为参数传递过来,因而 listen()
监听办法实质的性能就是监听后退事件,执行一系列导航守卫,实现路由切换相干逻辑
pop()
事件的监听实质跟push()
执行的逻辑是统一的,都是切换路由映射的组件
function useHistoryListeners(base, historyState, currentLocation, replace) {const popStateHandler = ({ state,}) => {const to = createCurrentLocation(base, location);
if (state) {currentLocation.value = to;}
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
});
});
};
window.addEventListener('popstate', popStateHandler);
return {
pauseListeners,
listen,
destroy,
};
}
listen()
监听办法触发后,具体做了什么?
当后退事件触发,listen()
监听办法触发,实质就是执行了 navigate()
导航,进行了对应的组件切换性能
手动模仿了被动触发路由切换,然而不会进行
pushState
/replaceState
扭转以后的路由地址,只是扭转currentRoute
,触发<router-view>
的组件从新渲染
function setupListeners() {removeHistoryListener = routerHistory.listen((to, _from, info) => {navigate(toLocation, from).then(() => {
// false 代表不会触发 pushState/replaceState
finalizeNavigation(toLocation, from, false);
});
});
}
function finalizeNavigation(toLocation, from, isPush, replace, data) {if (isPush) {if (replace || isFirstNavigation)
routerHistory.replace(toLocation.fullPath, assign({scroll: isFirstNavigation && state && state.scroll,}, data));
else
routerHistory.push(toLocation.fullPath, data);
}
currentRoute.value = toLocation;
}
什么时候调用 pauseListeners()
?pauseListeners()
的作用是什么?
go()
办法中,如果咱们应用 router.go(-1, false)
,那么咱们就会触发pauseListeners()
->pauseState = currentLocation.value
路由后退会触发 popStateHandler()
,此时咱们曾经注册pauseState = currentLocation.value
,因而在popStateHandler()
中会阻止后续的 listeners
的循环调用,达到暂停 listeners
的目标
function go(delta, triggerListeners = true) {if (!triggerListeners)
historyListeners.pauseListeners();
history.go(delta);
}
咱们什么时候调用
go(-xxxx, false)
?第二个参数跟popStateHandler()
有什么分割?
在 Vue Router 4.1.6
的源码中,咱们能够发现,所有波及到 go(-xxxx, false)
都集中在后退事件对应的监听办法中,如上面代码块所示,在后退事件触发,进行组件的切换过程中,Vue Router
可能会产生多种不同类型的路由切换失败,比方下面剖析的 issues/916
一样,当咱们手动更改路由,新路由重定向的路由跟目前路由反复时,咱们就须要被动后退一个路由,然而咱们不心愿从新渲染组件,只是单纯想要回进路由记录,那么咱们就能够调用 routerHistory.go(-1, false)
触发 pauseListeners()
,从而暂停listeners
执行,从而阻止 navigate()
函数的调用,阻止导航守卫的产生和 scroll
滚动地位的复原等一系列逻辑
function setupListeners() {removeHistoryListener = routerHistory.listen((to, _from, info) => {navigate(toLocation, from)
.catch((error) => {if (isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) {pushWithRedirect(error.to, toLocation)
.then(failure => {
if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |
16 /* ErrorTypes.NAVIGATION_DUPLICATED */) &&
!info.delta &&
info.type === NavigationType.pop) {routerHistory.go(-1, false);
}
});
} else {if (info.delta) {routerHistory.go(-info.delta, false);
}
}
})
.then((failure) => {
// false 代表不会触发 pushState/replaceState
failure = failure || finalizeNavigation(toLocation, from, false);
if (failure) {
if (info.delta &&
// a new navigation has been triggered, so we do not want to revert, that will change the current history
// entry while a different route is displayed
!isNavigationFailure(failure, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {routerHistory.go(-info.delta, false);
}
else if (info.type === NavigationType.pop &&
isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) {
// manual change in hash history #916
// it's like a push but lacks the information of the direction
routerHistory.go(-1, false);
}
}
})
});
}
参考文章
- 7 张图,从零实现一个简易版 Vue-Router,太通俗易懂了!
- VueRouter4 路由权重
Vue 系列其它文章
- Vue2 源码 - 响应式原理浅析
- Vue2 源码 - 整体流程浅析
- Vue2 源码 - 双端比拟 diff 算法 patchVNode 流程浅析
- Vue3 源码 - 响应式零碎 - 依赖收集和派发更新流程浅析
- Vue3 源码 - 响应式零碎 -Object、Array 数据响应式总结
- Vue3 源码 - 响应式零碎 -Set、Map 数据响应式总结
- Vue3 源码 - 响应式零碎 -ref、shallow、readonly 相干浅析
- Vue3 源码 - 整体流程浅析
- Vue3 源码 -diff 算法 -patchKeyChildren 流程浅析