【vue-rouer源码】系列文章
- 【vue-router源码】一、router.install解析
- 【vue-router源码】二、createWebHistory、createWebHashHistory、createMemoryHistory源码解析
- 【vue-router源码】三、理解Vue-router中的Matcher
- 【vue-router源码】四、createRouter源码解析
- 【vue-router源码】五、router.addRoute、router.removeRoute、router.hasRoute、router.getRoutes源码分析
- 【vue-router源码】六、router.resolve源码解析
- 【vue-router源码】七、router.push、router.replace源码解析
- 【vue-router源码】八、router.go、router.back、router.forward源码解析
- 【vue-router源码】九、全局导航守卫的实现
- 【vue-router源码】十、isReady源码解析
- 【vue-router源码】十一、onBeforeRouteLeave、onBeforeRouteUpdate源码分析
- 【vue-router源码】十二、useRoute、useRouter、useLink源码分析
- 【vue-router源码】十三、RouterLink源码分析
- 【vue-router源码】十四、RouterView源码分析
前言
【vue-router源码】系列文章将带你从0开始了解vue-router
的具体实现。该系列文章源码参考vue-router v4.0.15
。
源码地址:https://github.com/vuejs/router
阅读该文章的前提是你最好了解vue-router
的基本使用,如果你没有使用过的话,可通过vue-router官网学习下。
该篇文章将分析router.push
和router.replace
的实现,通过该文章你会了解一个稍微完整的导航解析流程。
使用
使用router.push
方法导航到不同的 URL。这个方法会向history
栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL。
使用router.replace
方法导航到不同的 URL。这个方法会在history
栈替换历史记录。
router.push('/search?name=pen')
router.push({
path: '/search', query: {
name: 'pen' } })
router.push({
name: 'search', query: {
name: 'pen' } })
// 以上三种方式是等效的。
router.replace('/search?name=pen')
router.replace({
path: '/search', query: {
name: 'pen' } })
router.replace({
name: 'search', query: {
name: 'pen' } })
// 以上三种方式是等效的。
push
push
方法接收一个to
参数,表示要跳转的路由,它可以是个字符串,也可以是个对象。在push
方法中调用了一个pushWithRedirect
函数,并返回其结果。
function push(to: RouteLocationRaw | RouteLocation) {
return pushWithRedirect(to)
}
pushWithRedirect
接收两个参数:to
、redirectedFrom
,并返回pushWithRedirect
的结果。其中to
是要跳转到的路由,redirectedFrom
代表to
是从哪个路由重定向来的,如果多次重定向,它只是最初重定向的那个路由。
function pushWithRedirect(
to: RouteLocationRaw | RouteLocation,
redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
// ...
}
因为要到的to
中可能存在重定向,所以pushWithRedirect
中首先要处理重定向:当to
中存在重定向时,递归调用pushWithRedirect
。
// 将to处理为规范化的路由
const targetLocation: RouteLocation = (pendingLocation = resolve(to))
// 当前路由
const from = currentRoute.value
// 使用 History API(history.state) 保存的状态
const data: HistoryState | undefined = (to as RouteLocationOptions).state
// force代表强制触发导航,即使与当前位置相同
const force: boolean | undefined = (to as RouteLocationOptions).force
// replace代表是否替换当前历史记录
const replace = (to as RouteLocationOptions).replace === true
// 获取要重定向的记录
const shouldRedirect = handleRedirectRecord(targetLocation)
// 如果需要重定向,递归调用pushWithRedirect方法
if (shouldRedirect)
return pushWithRedirect(
assign(locationAsObject(shouldRedirect), {
state: data,
force,
replace,
}),
// 重定向的根来源
redirectedFrom || targetLocation
)
handleRedirectRecord
函数的实现:
function handleRedirectRecord(to: RouteLocation): RouteLocationRaw | void {
// 找到匹配的路由,to.matched中的路由顺序是父路由在子路由前面,所以最后一个路由是我们的最终路由
const lastMatched = to.matched[to.matched.length - 1]
// 如果路由存在redirect
if (lastMatched && lastMatched.redirect) {
const {
redirect } = lastMatched
// 如果redirect是函数,需要执行函数
let newTargetLocation =
typeof redirect === 'function' ? redirect(to) : redirect
// 如果newTargetLocation是string
if (typeof newTargetLocation === 'string') {
// 如果newTargetLocation中存在?或#,需要将newTargetLocation解析成一个LocationNormalized类型的对象
newTargetLocation =
newTargetLocation.includes('?') || newTargetLocation.includes('#')
? (newTargetLocation = locationAsObject(newTargetLocation))
: {
path: newTargetLocation }
// 设置params为一个空对象
newTargetLocation.params = {
}
}
// 如果newTargetLocation中没有path和name属性,则无法找到重定向的路由,开发环境下进行提示
if (
__DEV__ &&
!('path' in newTargetLocation) &&
!('name' in newTargetLocation)
) {
warn(
`Invalid redirect found:\n${
JSON.stringify(
newTargetLocation,
null,
2
)}\n when navigating to "${
to.fullPath
}". A redirect must contain a name or path. This will break in production.`
)
throw new Error('Invalid redirect')
}
return assign(
{
query: to.query,
hash: to.hash,
params: to.params,
},
newTargetLocation
)
}
}
处理完重定向后,接下来会检测要跳转到的路由和当前路由是否为同一个路由,如果是同一个路由并且不强制跳转,会创建一个失败函数赋给failure
,然后处理滚动行为。
const toLocation = targetLocation as RouteLocationNormalized
// 设置重定向的来源
toLocation.redirectedFrom = redirectedFrom
let failure: NavigationFailure | void | undefined
// 如果要跳转到的路由与当前路由一致并且不强制跳转
if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
// 创建一个错误信息,该错误信息代表重复的导航
failure = createRouterError<NavigationFailure>(
ErrorTypes.NAVIGATION_DUPLICATED,
{
to: toLocation, from }
)
// 处理滚动行为
handleScroll(
from,
from,
true,
false
)
}
关于handleScroll
的实现如下:首先从options
中找到scrollBehavior
选项,如果不是浏览器环境或不存在scrollBehavior
,返回一个Promise
对象。相反,获取滚动位置(根据历史记录中的position和path获取),然后在下一次DOM刷新后,执行定义的滚动行为函数,滚动行为函数执行完后,将滚动行为函数结果作为最终的滚动位置将页面滚动到指定位置。
function handleScroll(
to: RouteLocationNormalizedLoaded,
from: RouteLocationNormalizedLoaded,
isPush: boolean,
isFirstNavigation: boolean
): Promise<any> {
const {
scrollBehavior } = options
if (!isBrowser || !scrollBehavior) return Promise.resolve()
// 获取滚动位置
const scrollPosition: _ScrollPositionNormalized | null =
(!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) ||
((isFirstNavigation || !isPush) &&
(history.state as HistoryState) &&
history.state.scroll) ||