前言
【vue-router 源码】系列文章将带你从 0 开始理解 vue-router
的具体实现。该系列文章源码参考 vue-router v4.0.15
。
源码地址:https://github.com/vuejs/router
浏览该文章的前提是你最好理解 vue-router
的根本应用,如果你没有应用过的话,可通过 vue-router 官网学习下。
本篇文章将介绍 router.go
、router.back
、router.forward
的实现。
应用
go
函数容许你在历史中后退或后退,指定步数如果 >0
,代表后退;<0
代表后退。
router.go(-2)
router.back()
// 等同于
router.go(-1)
router.forward()
// 等同于
router.go(1)
go
go
接管一个参数delta
,示意绝对以后页面,挪动多少步,正数示意后退,负数示意后退。
const go = (delta: number) => routerHistory.go(delta)
在 routerHistory.go
中会调用 history.go
,进而触发popstate
监听函数。如果你看过之前 createWebHistory
的解析,你会晓得在 createWebHistory
中通过 useHistoryListeners
创立 historyListeners
时,会注册一个 popstate
监听函数,这个监听函数在调用 history.go
后就会触发。
// 文件地位:src/history/html5.ts useHistoryListeners 办法
window.addEventListener('popstate', popStateHandler)
const popStateHandler: PopStateListener = ({state,}: {state: StateEntry | null}) => {
// 以后 location,字符串
const to = createCurrentLocation(base, location)
const from: HistoryLocation = currentLocation.value
const fromState: StateEntry = historyState.value
let delta = 0
// 如果不存在 state
// 对于为什么 state 可能为空,可参考:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event
if (state) {
currentLocation.value = to
historyState.value = state
// 如果暂停监听了,并且暂停时的状态是 from,间接 return
if (pauseState && pauseState === from) {
pauseState = null
return
}
// 计算挪动的步数
delta = fromState ? state.position - fromState.position : 0
} else {replace(to)
}
// 循环调用监听函数
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
})
})
}
能够看到,在监听函数最初会循环调用 listeners
中的 listener
,那么listener
是什么?什么时候被增加的呢?
在后面文章介绍 install
的实现时,其中有一步十分重要的操作就是要依据地址栏的 url 进行第一次跳转。而个跳转是通过调用 push
办法实现的,因为 push
会调用 pushWidthRedirect
办法,在 pushWidthRedirect
中的最初会执行 finalizeNavigation
(不思考两头 reject 谬误)。而在finalizeNavigation
中的最初会调用一个 markAsReady
办法。
function markAsReady<E = any>(err?: E): E | void {if (!ready) {
// still not ready if an error happened
ready = !err
setupListeners()
readyHandlers
.list()
.forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
readyHandlers.reset()}
return err
}
在 markAsReady
中调用了 setupListeners
的一个办法。在这个办法中会调用 routerHistory.listen()
增加一个函数。
let removeHistoryListener: undefined | null | (() => void)
function setupListeners() {
// 如果有 removeHistoryListener,阐明曾经增加过 listener
if (removeHistoryListener) return
// 调用 routerHistory.listen 增加监听函数,routerHistory.listen 返回一个删除这个 listener 函数
removeHistoryListener = routerHistory.listen((to, _from, info) => {const toLocation = resolve(to) as RouteLocationNormalized
// 确定是否存在重定向
const shouldRedirect = handleRedirectRecord(toLocation)
if (shouldRedirect) {
pushWithRedirect(assign(shouldRedirect, { replace: true}),
toLocation
).catch(noop)
return
}
pendingLocation = toLocation
const from = currentRoute.value
// 保留 from 滚动地位
if (isBrowser) {
saveScrollPosition(getScrollKey(from.fullPath, info.delta),
computeScrollPosition())
}
navigate(toLocation, from)
.catch((error: NavigationFailure | NavigationRedirectError) => {
// 导航被勾销
if (
isNavigationFailure(
error,
ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_CANCELLED
)
) {return error}
// 在钩子中进行了重定向
if (isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
) {
pushWithRedirect((error as NavigationRedirectError).to,
toLocation
)
.then(failure => {
// 钩子中的重定向过程中如果导航被勾销或导航冗余,回退一步
if (
isNavigationFailure(
failure,
ErrorTypes.NAVIGATION_ABORTED |
ErrorTypes.NAVIGATION_DUPLICATED
) &&
!info.delta &&
info.type === NavigationType.pop
) {routerHistory.go(-1, false)
}
})
.catch(noop)
return Promise.reject()}
// 复原历史记录,,但不触发监听
if (info.delta) routerHistory.go(-info.delta, false)
// 无奈辨认的谬误,交给全局谬误处理器
return triggerError(error, toLocation, from)
})
.then((failure: NavigationFailure | void) => {
failure =
failure ||
finalizeNavigation(
toLocation as RouteLocationNormalizedLoaded,
from,
false
)
if (failure) {
// 如果存在错误信息,回到原始地位,但不触发监听
if (info.delta) {routerHistory.go(-info.delta, false)
} else if (
info.type === NavigationType.pop &&
isNavigationFailure(
failure,
ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_DUPLICATED
)
) { // 谬误类型时导航被勾销或冗余,回退历史记录,但不触发监听
routerHistory.go(-1, false)
}
}
// 触发全局 afterEach 钩子
triggerAfterEach(
toLocation as RouteLocationNormalizedLoaded,
from,
failure
)
})
.catch(noop)
})
}
能够看到这个监听函数和 push
的过程十分相似,与 push
不同的是,在触发监听时,一旦呈现了一些错误信息(如导航被勾销、导航时冗余的、地位谬误),须要将历史记录回退到相应地位。
go
的执行流程:
back
back
,回退一个历史记录,相当于go(-1)
。
const router = {
// ...
back: () => go(-1),
// ...
}
forward
forward
,后退一个历史记录,相当于go(1)
。
const router = {
// ...
forward: () => go(1),
// ...
}
总结
go
、back
、forward
办法最终通过调用 history.go
办法,触发 popstate
事件(popstate
中的监听函数在第一次路由跳转时被增加),而在 popstate
事件中的过程和 push
的过程是十分相似的,与 push
不同的是,一旦呈现了一些错误信息(如导航被勾销、导航时冗余的、地位谬误),须要将历史记录回退到相应地位。