vue-router源码浅析
关键函数
首先我们从使用路由时的几个函数调用入手观察源码执行的主干。
import { createApp } from 'vue'
const routerHistory = createWebHashHistory()
const router = createRouter({
history: routerHistory,
strict: true,
routes: [{
name: '...'
path: '...',
components: ...
}]
})
const app = createApp()
app.use(router)
createWebHashHistory与createWebHistory
createWebHashHistory
内部同样是调用createWebHistory
差别在于createWebHashHistory
会拼接‘#’
base = location.host ? base || location.pathname + location.search : ''
// allow the user to provide a `#` in the middle: `/base/#/app`
if (!base.includes('#')) base += '#'
两个方法最终返回RouterHistory
对象
//假设请求http://localhost/api/#/index
interface RouterHistory {
//base值/api/#
readonly base: string
//location /index
readonly location: HistoryLocation
//历史状态
readonly state: HistoryState
push(to: HistoryLocation, data?: HistoryState): void
replace(to: HistoryLocation, data?: HistoryState): void
go(delta: number, triggerListeners?: boolean): void
listen(callback: NavigationCallback): () => void
createHref(location: HistoryLocation): string
//取消popstate与beforeunload事件监听,删除事件队列回调函数
destroy(): void
}
createWebHistory主要功能为注册popstate
与beforeunload
事件以及生成并返回 RouterHistory
对象包含了几个router的重要方法:
go方法,功能为调用history.go
// 使用history api
function go(delta: number, triggerListeners = true) {
//history.go()方法会触发popstate事件,此判断为了阻止相关回调中的执行
if (!triggerListeners) historyListeners.pauseListeners()
history.go(delta)
}
push方法,主要功能为通过history
和location
相关api添加浏览记录:
function push(to: HistoryLocation, data?: HistoryState) {
const currentState = assign(
{},
historyState.value,
history.state as Partial<StateEntry> | null,
{
forward: to,
scroll: computeScrollPosition(),
}
)
changeLocation(currentState.current, currentState, true)
const state: StateEntry = assign(
{},
buildState(currentLocation.value, to, null),
{ position: currentState.position + 1 },
data
)
changeLocation(to, state, false)
currentLocation.value = to
}
上面只保留了主要代码,push内部调用changeLocation
方法
function changeLocation(
to: HistoryLocation,
state: StateEntry,
replace: boolean
): void {
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) {
if (__DEV__) {
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)
}
}
(只保留了主要代码)
我们看到通过参数replace
实现使用replaceState还是pushState,因此在push函数中第一个changeLocation
实际上使用replaceState替换了当前路径的history state。第二个才是真正的push。
replace函数同样也是changeLocation(to, state, true)
的方式实现页面记录替换。
listen方法, 主要功能为popstate事件添加订阅者
function listen(callback: NavigationCallback) {
// 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
}
实质上是一个观察者模式,在popStateHandler
回调中调用。
通过createWebHashHistory
创建的routerHistory对象作为createRouter
时的参数传入为createRouter
内部提供调用。
createRouter
createRouter
方法主要功能为输出interface Router
对象,router中一个重要的方法install,提供给vue实例在调用app.use(router)
时对router插件安装:
install(app: 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: () => unref(currentRoute),
})
// this initial navigation is only necessary on client, on server it doesn't
// make sense because it will create an extra unnecessary navigation and could
// lead to problems
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)
})
}
const reactiveRoute = {} as {
[k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
RouteLocationNormalizedLoaded[k]
>
}
for (const key in START_LOCATION_NORMALIZED) {
// @ts-expect-error: the key matches
reactiveRoute[key] = computed(() => currentRoute.value[key])
}
console.log('%ccurrentRoute>>>>','color: red', {currentRoute})
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
const unmountApp = app.unmount
installedApps.add(app)
app.unmount = function () {
installedApps.delete(app)
// the router is not attached to an app anymore
if (installedApps.size < 1) {
// invalidate the current navigation
pendingLocation = START_LOCATION_NORMALIZED
removeHistoryListener && removeHistoryListener()
removeHistoryListener = null
currentRoute.value = START_LOCATION_NORMALIZED
started = false
ready = false
}
unmountApp()
}
// TODO: this probably needs to be updated so it can be used by vue-termui
if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) {
addDevtools(app, router, matcher)
}
}
install函数整体逻辑比较简单,主要包括:
1.注册RouterLink
以及RouterView
组件
2.通过app.config.globalProperties.$router
向vue实例局变量$router注入路由对象
3.push由上面routerHistory
从生成的location地址
4.通过vue的provide
方法向实例提供几个路由相关的对象
const reactiveRoute = {} as {
[k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
RouteLocationNormalizedLoaded[k]
>
}
for (const key in START_LOCATION_NORMALIZED) {
// @ts-expect-error: the key matches
reactiveRoute[key] = computed(() => currentRoute.value[key])
}
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
其中currentRoute
为shollowRef
包装的对象指向当前路由,routeLocationKey指向的对象为对currentRoute
对象属性的响应式绑定。routeLocationKey
与routerViewLocationKey
将在RouterView
和RouterLink
中广泛使用。
RouterView
组件的原理是监听currentRoute
对象的变化,当调用push等方法改变当前route时,组件内响应式更新当前路由需要渲染的组件。
(RouterView源码如下)
const RouterViewImpl = defineComponent({
name: 'RouterView',
inheritAttrs: false,
props: {
name: {
type: String as PropType<string>,
default: 'default',
},
route: Object as PropType<RouteLocationNormalizedLoaded>,
},
compatConfig: { MODE: 3 },
setup(props, { attrs, slots }) {
__DEV__ && warnDeprecatedUsage()
const injectedRoute = inject(routerViewLocationKey)!
const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
() => props.route || injectedRoute.value
)
const injectedDepth = inject(viewDepthKey, 0)
// The depth changes based on empty components option, which allows passthrough routes e.g. routes with children
// that are used to reuse the `path` property
// 查找当前路劲深度
const depth = computed<number>(() => {
let initialDepth = unref(injectedDepth)
const { matched } = routeToDisplay.value
let matchedRoute: RouteLocationMatched | undefined
while (
(matchedRoute = matched[initialDepth]) &&
!matchedRoute.components
) {
initialDepth++
}
return initialDepth
})
const matchedRouteRef = computed<RouteLocationMatched | undefined>(
() => routeToDisplay.value.matched[depth.value]
)
//告诉下一次路径的路径深度下标
provide(
viewDepthKey,
computed(() => depth.value + 1)
)
provide(matchedRouteKey, matchedRouteRef)
provide(routerViewLocationKey, routeToDisplay)
const viewRef = ref<ComponentPublicInstance>()
// 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' }
)
return () => {
const route = routeToDisplay.value
// we need the value at the time we render because when we unmount, we
// navigated to a different location so the value is different
const currentName = props.name
const matchedRoute = matchedRouteRef.value
const ViewComponent =
matchedRoute && matchedRoute.components![currentName]
if (!ViewComponent) {
return normalizeSlot(slots.default, { Component: ViewComponent, route })
}
// props from route configuration
const routePropsOption = matchedRoute.props[currentName]
const routeProps = routePropsOption
? routePropsOption === true
? route.params
: typeof routePropsOption === 'function'
? routePropsOption(route)
: routePropsOption
: null
const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
// remove the instance reference to prevent leak
if (vnode.component!.isUnmounted) {
matchedRoute.instances[currentName] = null
}
}
const component = h(
ViewComponent,
assign({}, routeProps, attrs, {
onVnodeUnmounted,
ref: viewRef,
})
)
if (
(__DEV__ || __FEATURE_PROD_DEVTOOLS__) &&
isBrowser &&
component.ref
) {
// TODO: can display if it's an alias, its props
const info: RouterViewDevtoolsContext = {
depth: depth.value,
name: matchedRoute.name,
path: matchedRoute.path,
meta: matchedRoute.meta,
}
const internalInstances = isArray(component.ref)
? component.ref.map(r => r.i)
: [component.ref.i]
internalInstances.forEach(instance => {
// @ts-expect-error
instance.__vrv_devtools = info
})
}
return (
// pass the vnode to the slot as a prop.
// h and <component :is="..."> both accept vnodes
normalizeSlot(slots.default, { Component: component, route }) ||
component
)
}
},
})
RouteView
组件的主要逻辑为渲染RouteView
插槽中的组件或者当前路径深度匹配到的组件(路由配置中的component)。
routerView通过provide( viewDepthKey, computed(() => depth.value + 1) )
告知下一层匹配路径的深度。例如/children/a
渲染children组件时注入viewDepthKey
值为0,同过以上代码a组件注入的viewDepthKey
值为1。
简单实现
以下代码只模拟主要逻辑,如push如何触发RouterView改变的机制
const RouteView = defineComponent({
setup(props, { attrs, slots }){
const injectionRoute = inject('routeView')
const injectedDepth = inject('viewDepthKey', 0)
//有效深度
const depth = computed(() => {
let initialDepth = unref(injectedDepth)
const { matched } = injectionRoute.value
while (
(matchedRoute = matched[initialDepth]) &&
!matchedRoute.components
) {
initialDepth++
}
return initialDepth
})
//当前路径深度的路由
const matchedRouteRef = computed(() => injectionRoute.value.matched[depth.value])
//注入下一次路由深度的值
provide('injectedDepth', depth.value + 1)
//路由名字
const currentName = props.name
const matchedRoute = matchedRouteRef.value
const RouteComponent = matchedRoute && matchedRoute.components[currentName]
const component = h(RouteComponent)
//渲染路由组件
return slots.default({ Component: component, route: injectionRoute.value }) || component
)
}
})
function createHistory() {
const location = location.path + location.search + location.hash
function toLocation(replace = false) {
try {
history[replace ? 'replaceState' : 'pushState'](state, '', url)
} catch() {
location[replace ? 'replace' : 'assign'](url)
}
}
function push() {
toLocation()
}
function replace() {
toLocation(true)
}
return {
location,
push,
replace
}
}
function createRouterMatcher(routes) {
const matchers = []
function resolve(location) {
const path = location.path
//查找匹配路径的路由对象
const matcher = matchers.find(m => m.re.test(path))
const matched = []
//不考虑children的情况
if(matcher) {
matched.unshift(matcher)
}
return {
matched,
path,
name: matcher.record.name
}
}
function addRoute(route) {
const normalizedRecord = {
path: route.path,
name: route.name,
props: route.props
meta: record.meta || {},
beforeEnter: record.beforeEnter,
children: record.children || [],
components:
'components' in record
? record.components || null
: record.component && { default: record.component },
}
const matcher = {
record: normalizedRecord,
children: []
}
//插入mathcers,用于后面匹配路径
if(matcher.record.components) {
let i = 0
while(
i < matchers.length &&
matchers[i].record.path !== matcher.record.path
) {
i++
}
matchers.splice(i, 0, mathcer)
}
}
//生成matcher并加入matchers数组
routes.forEach((route) => addRoute(route))
return {
resolve,
addRoute
}
}
function createRoute(option) {
//初始状态
const START_LOCATION = {}
const history = option.history
const currentLocation = shallowRef(START_LOCATION)
const matcher = createRouterMatcher(option.routes)
function resolve(rawLocation) {
//实际上还需要处理query和hash,此处只简单模拟
function parseUrl(rawLocation) {
return {
path:rawLocation
}
}
const normalizedLocation = parseUrl(rawLocation)
const matchedRoute = matcher.resolve({
path: normalizedLocation.path
})
return {
...normalizedLocation,
...matchedRoute
}
}
function navigation(to) {
const toLocation = resolve(to)
history.push(to)
currentLocation.value = toLocation
}
function push(to) {
navigation(to)
}
const router = {
install(app) {
app.component('RouterView', RouterView)
//vue组件内通过this.$router访问该router对象
app.config.globalProperties.$router = this
if(currentLocation.value === START_LOCATION) {
push(option.location)
}
provide('routeView', currentLocation)
}
}
return router
}
const router = createRoute({
history: createHistory()
routes: [
{ path: '/home', component: Home },
]
})
可以看到当调用router.push
时currentLocation.value
值被改变,由于RouterView
依赖currentLocation.value
因此会被触发视图更新。
总结
路由的基本原理为路由组件RouteView
利用Vue的响应式原理监听当前路由变化,当push或replace等操作改变当前路由时触发视图更新。
转载自:https://juejin.cn/post/7198043007155978297