router-view
视图组件通过监听响应式对象currentRoute
实现视图的更新。
router/packages/router/src/router.ts
js
export function createRouter(options: RouterOptions): Router {
const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
START_LOCATION_NORMALIZED
)
const router : Router = {
install(app: App) {
const router = this
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
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 => {
if (__DEV__) warn('Unexpected error when starting the router:', err)
})
}
```
app.provide(routerViewLocationKey, currentRoute)}
}
return router
}
router/packages/router/src/RouterView.ts
js
// router-view组件
export const RouterViewImpl = /*#__PURE__*/ defineComponent({
name: 'RouterView',
inheritAttrs: false,
props: {
name: {
type: String as PropType<string>,
default: 'default',
},
route: Object as PropType<RouteLocationNormalizedLoaded>,
},
// Better compat for @vue/compat users
// https://github.com/vuejs/router/issues/1315
...
__DEV__ && warnDeprecatedUsage()
const injectedRoute = inject(routerViewLocationKey)!
const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
() => props.route || injectedRoute.value
)
const matchedRouteRef = computed<RouteLocationMatched | undefined>(
() => routeToDisplay.value.matched[depth.value]
)
// watch at the same time the component instance, the route record we are
// rendering, and the name
watch(
() => [viewRef.value, matchedRouteRef.value, props.name] as const,
([instance, to, name], [oldInstance, from, oldName]) => {
// copy reused instances
if (to) {
// this will update the instance for new instances as well as reused
// instances when navigating to a new route
to.instances[name] = instance
// the component instance is reused for a different route or name, so
// we copy any saved update or leave guards. With async setup, the
// mounting component will mount before the matchedRoute changes,
// making instance === oldInstance, so we check if guards have been
// added before. This works because we remove guards when
// unmounting/deactivating components
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 &&
// if there is no instance but to and from are the same this might be
// the first visit
(!from || !isSameRouteRecord(to, from) || !oldInstance)
) {
;(to.enterCallbacks[name] || []).forEach(callback =>
callback(instance)
)
}
},
{ flush: 'post' }
)
在finalizeNavigation
方法中会对响应式对象currentRoute
重新赋值
js
function finalizeNavigation(toLocation, from, isPush, replace2, data) {
const error = checkCanceledNavigation(toLocation, from);
if (error)
return error;
const isFirstNavigation = from === START_LOCATION_NORMALIZED;
const state = !isBrowser ? {} : history.state;
if (isPush) {
if (replace2 || isFirstNavigation)
routerHistory.replace(toLocation.fullPath, assign({
scroll: isFirstNavigation && state && state.scroll
}, data));
else
routerHistory.push(toLocation.fullPath, data);
}
currentRoute.value = toLocation;
handleScroll(toLocation, from, isPush, isFirstNavigation);
markAsReady();
}
什么情况下会触发finalizeNavigation
方法呢?
- 点击自定义组件
router-link
创建的链接会触发finalizeNavigation
方法,调用链如下:
点击自定义组件 router-link
后,也会调用history
的pushState
或者replaceState
方法修改url。pushState
和replaceState
不会触发popstate事件
js
function changeLocation(to, state, replace2) {
const hashIndex = base.indexOf("#");
const url = hashIndex > -1 ? (location2.host && document.querySelector("base") ? base : base.slice(hashIndex)) + to : createBaseLocation() + base + to;
try {
history2[replace2 ? "replaceState" : "pushState"](state, "", url);
historyState.value = state;
} catch (err) {
if (true) {
warn("Error with push/replace State", err);
} else {
console.error(err);
}
location2[replace2 ? "replace" : "assign"](url);
}
}
- 手动修改url或者点击浏览器的回退前进按钮,也会触发
finalizeNavigation
方法,调用链如下:
此时会触发window
上绑定的popstate
事件的回掉函数,对比vue-router v4.x和 vue-router v3.x发现老版本中hash
模式对应的hashchange
事件已经没有了。
结果实践后发现改变url
上的hash
部分也会触发popstate
事件。
router/packages/router/src/history/html5.ts
js
window.addEventListener('popstate', popStateHandler)
const popStateHandler: PopStateListener = ({
state,
}: {
state: StateEntry | null
}) => {
const to = createCurrentLocation(base, location)
const from: HistoryLocation = currentLocation.value
const fromState: StateEntry = historyState.value
let delta = 0
if (state) {
currentLocation.value = to
historyState.value = 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)
}
}
vue-router v3.x的监听事件hashchange和popstate
js
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
this.listeners.push(() => {
window.removeEventListener(eventType, handleRoutingEvent)
})
}