Vue Router源码学习(一)-- createRouter
断更了许久,想不到吧,我又回来了。好不容易获得一段离职后的喘息时日,暂时不用过早出晚归的骡马生活,空气都变得香甜了。
言归正传,Vue源码学习先按个暂停键,要记录的东西太多。既然如此,那为什么不先解决Router这种更轻松的问题呢?想到了就开干!下面开始汇报今天的学习成果!
Router作为Vue Router的核心内容,很值得好好研究一番。回顾了下在项目里的使用,是从函数createRouter开始的吧。既如此,那源码学习也从这儿开始咯。虽说高手都喜欢按部就班从index.js开始,但是天才嘛,自然是想到哪儿就看到哪儿。为此很有必要先知道两个重要的类型接口:RouterOptions和Router。
1. RouterOptions
RouterOptions是createRouter初始化Router实例时传入的选项参数。
-
history-- 路由模式(必需):可用createWebHistory来创建history模式,但是要求服务端有相应的配置来解决刷新时的路由问题;也可以使用createWebHashHistory来创建hash模式,无需服务端进行额外的配置,但是搜索引擎不会处理路由对应的页面,不利于SEO; -
routes-- 初始路由列表(必需); -
scrollBehavior-- 控制切换页面时滚动行为的函数(可选),可返回一个Promise来延迟滚动;- 入参:
to,from,savedPosition
- 入参:
-
parseQuery-- 自定义将字符串query转化为对象的方式(可选),一般用不上; -
stringifyQuery-- 自定义讲query对象转化为字符串的方法(可选),一般用不上; -
linkActiveClass-- 活跃路由对应的link的类名,可用于定制活跃路由对应的链接的样式,默认为router-link-exact-active; -
linkExactActiveClass-- 当前精准活跃路由的类名,可用于定制当前精准活跃路由对应的链接的样式,默认为router-link-inactive;
linkActiveClass 和linkExactActiveClass 的区别:
linkActiveClass:符合路由匹配的规则的为活跃路由,例如某个路由的路径为/active,那么当页面处于/active/123时,该路由也能被匹配到,因此该路由也是活跃路由。linkExactActiveClass:精准匹配到的活跃路由,当页面处于/active/123时,仅有路由/active/123能精准匹配。
这里直接把github里的源码复制过来得了,注释给的太清晰了。
/**
* Options to initialize a {@link Router} instance.
* 初始化Router实例时传入的选项
*/
export interface RouterOptions extends PathParserOptions {
/**
* History implementation used by the router. Most web applications should use
* `createWebHistory` but it requires the server to be properly configured.
* You can also use a _hash_ based history with `createWebHashHistory` that
* does not require any configuration on the server but isn't handled at all
* by search engines and does poorly on SEO.
*
* @example
* ```js
* createRouter({
* history: createWebHistory(),
* // other options...
* })
* ```
*/
// 路由模式:可用createWebHistory来创建history模式,但是要求服务端有相应的配置来解决刷新时的路由问题;
// 也可以使用createWebHashHistory来创建hash模式,无需服务端进行额外的配置,但是搜索引擎不会处理路由对应的页面,不利于SEO
history: RouterHistory
/**
* Initial list of routes that should be added to the router.
*/
routes: Readonly<RouteRecordRaw[]>
/**
* Function to control scrolling when navigating between pages. Can return a
* Promise to delay scrolling. Check {@link ScrollBehavior}.
*
* @example
* ```js
* function scrollBehavior(to, from, savedPosition) {
* // `to` and `from` are both route locations
* // `savedPosition` can be null if there isn't one
* }
* ```
*/
scrollBehavior?: RouterScrollBehavior
/**
* Custom implementation to parse a query. See its counterpart,
* {@link RouterOptions.stringifyQuery}.
*
* @example
* Let's say you want to use the [qs package](https://github.com/ljharb/qs)
* to parse queries, you can provide both `parseQuery` and `stringifyQuery`:
* ```js
* import qs from 'qs'
*
* createRouter({
* // other options...
* parseQuery: qs.parse,
* stringifyQuery: qs.stringify,
* })
* ```
*/
parseQuery?: typeof originalParseQuery
/**
* Custom implementation to stringify a query object. Should not prepend a leading `?`.
* {@link RouterOptions.parseQuery | parseQuery} counterpart to handle query parsing.
*/
stringifyQuery?: typeof originalStringifyQuery
/**
* Default class applied to active {@link RouterLink}. If none is provided,
* `router-link-active` will be applied.
*/
linkActiveClass?: string
/**
* Default class applied to exact active {@link RouterLink}. If none is provided,
* `router-link-exact-active` will be applied.
*/
linkExactActiveClass?: string
/**
* Default class applied to non-active {@link RouterLink}. If none is provided,
* `router-link-inactive` will be applied.
*/
// linkInactiveClass?: string
}
2. Router
天天都在用的Router,应该没人会感到陌生,但应该很多人都是“夹生”。来过一遍Router的实例上有哪些属性和方法:
-
currentRoute-- 当前路由对象; -
options-- 由createRouter创建初始路由时的选项; -
配合微前端使用
-
listening-- 布尔值,是否监听history事件,这是个为微前端设计的api,非微前端项目用不上; -
路由的增删查
-
addRoute-- 路由实例可用此方法添加新的路由,多用于从服务端拉取准许的路由信息后再添加到router中,有俩重载签名,接收的参数不一样:(parentName: RouteRecordName, route: RouteRecordRaw) => (() => void),入参为parantName和route,将route作为parentName的子路由来添加;(route: RouteRecordRaw) => (() => void),入参为route,直接添加到根路由下;
-
removeRoute-- 入参为name,移除对应的路由; -
hasRoute-- 入参为name,判断路由实例中是否存在对应的路由; -
getRoutes-- 返回所有的路由数据(数组); -
resolve-- 解析路由,入参为to和currentLocation(可选),生成新的路由位置,其中多出来一个href属性,包含了已存在的base,即存在有效的base时,href可以形成完整的链接。 -
路由切换的几个方法
-
push--(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>,不多解释,push方法大家都在用,只需要注意返回的是Promise; -
replace--(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>,同push,两者的区别应该没人不知道吧; -
back-- 返回历史路由栈中的上一个路由; -
forward-- 切换到历史路由栈中的下一个路由; -
go-- 入参delta,数值类型,切换为历史路由栈中指定跨度的路由; -
全局守卫钩子
-
beforeEach-- 入参guard函数,类型为NavigationGuardWithThis<undefined>,此时新页面的Vue实例尚未创建,在该gurad中的this为undefined;此时guard接收三个参数:to,from,next; -
beforeResolve:入参guard函数,类型为NavigationGuardWithThis<undefined>,此时新页面的Vue实例尚未创建,在该gurad中的this为undefined;此时guard接收三个参数:to,from,next; -
afterEach:入参guard函数,类型为NavigationHookAfter。此时guard接收三个参数:to,from,failure(可选); -
错误处理
-
onError-- 入参为handler错误处理函数 -
路由实例是否已准备完毕
-
isReady-- 得到一个Promise -
安装路由
-
install-- 提供给app.use调用
同样copy一下源码贴在这儿:
/**
* Router instance.
*/
export interface Router {
/**
* @internal
*/
// readonly history: RouterHistory
/**
* Current {@link RouteLocationNormalized}
*/
readonly currentRoute: Ref<RouteLocationNormalizedLoaded>
/**
* Original options object passed to create the Router
*/
readonly options: RouterOptions
/**
* Allows turning off the listening of history events. This is a low level api for micro-frontends.
*/
listening: boolean
/**
* Add a new {@link RouteRecordRaw | route record} as the child of an existing route.
*
* @param parentName - Parent Route Record where `route` should be appended at
* @param route - Route Record to add
*/
addRoute(parentName: RouteRecordName, route: RouteRecordRaw): () => void
/**
* Add a new {@link RouteRecordRaw | route record} to the router.
*
* @param route - Route Record to add
*/
addRoute(route: RouteRecordRaw): () => void
/**
* Remove an existing route by its name.
*
* @param name - Name of the route to remove
*/
removeRoute(name: RouteRecordName): void
/**
* Checks if a route with a given name exists
*
* @param name - Name of the route to check
*/
hasRoute(name: RouteRecordName): boolean
/**
* Get a full list of all the {@link RouteRecord | route records}.
*/
getRoutes(): RouteRecord[]
/**
* Returns the {@link RouteLocation | normalized version} of a
* {@link RouteLocationRaw | route location}. Also includes an `href` property
* that includes any existing `base`. By default, the `currentLocation` used is
* `router.currentRoute` and should only be overridden in advanced use cases.
*
* @param to - Raw route location to resolve
* @param currentLocation - Optional current location to resolve against
*/
resolve(
to: RouteLocationRaw,
currentLocation?: RouteLocationNormalizedLoaded
): RouteLocation & { href: string }
/**
* Programmatically navigate to a new URL by pushing an entry in the history
* stack.
*
* @param to - Route location to navigate to
*/
push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
/**
* Programmatically navigate to a new URL by replacing the current entry in
* the history stack.
*
* @param to - Route location to navigate to
*/
replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
/**
* Go back in history if possible by calling `history.back()`. Equivalent to
* `router.go(-1)`.
*/
back(): ReturnType<Router['go']>
/**
* Go forward in history if possible by calling `history.forward()`.
* Equivalent to `router.go(1)`.
*/
forward(): ReturnType<Router['go']>
/**
* Allows you to move forward or backward through the history. Calls
* `history.go()`.
*
* @param delta - The position in the history to which you want to move,
* relative to the current page
*/
go(delta: number): void
/**
* Add a navigation guard that executes before any navigation. Returns a
* function that removes the registered guard.
*
* @param guard - navigation guard to add
*/
beforeEach(guard: NavigationGuardWithThis<undefined>): () => void
/**
* Add a navigation guard that executes before navigation is about to be
* resolved. At this state all component have been fetched and other
* navigation guards have been successful. Returns a function that removes the
* registered guard.
*
* @param guard - navigation guard to add
* @returns a function that removes the registered guard
*
* @example
* ```js
* router.beforeResolve(to => {
* if (to.meta.requiresAuth && !isAuthenticated) return false
* })
* ```
*
*/
beforeResolve(guard: NavigationGuardWithThis<undefined>): () => void
/**
* Add a navigation hook that is executed after every navigation. Returns a
* function that removes the registered hook.
*
* @param guard - navigation hook to add
* @returns a function that removes the registered hook
*
* @example
* ```js
* router.afterEach((to, from, failure) => {
* if (isNavigationFailure(failure)) {
* console.log('failed navigation', failure)
* }
* })
* ```
*/
afterEach(guard: NavigationHookAfter): () => void
/**
* Adds an error handler that is called every time a non caught error happens
* during navigation. This includes errors thrown synchronously and
* asynchronously, errors returned or passed to `next` in any navigation
* guard, and errors occurred when trying to resolve an async component that
* is required to render a route.
*
* @param handler - error handler to register
*/
onError(handler: _ErrorListener): () => void
/**
* Returns a Promise that resolves when the router has completed the initial
* navigation, which means it has resolved all async enter hooks and async
* components that are associated with the initial route. If the initial
* navigation already happened, the promise resolves immediately.
*
* This is useful in server-side rendering to ensure consistent output on both
* the server and the client. Note that on server side, you need to manually
* push the initial location while on client side, the router automatically
* picks it up from the URL.
*/
isReady(): Promise<void>
/**
* Called automatically by `app.use(router)`. Should not be called manually by
* the user. This will trigger the initial navigation when on client side.
*
* @internal
* @param app - Application that uses the router
*/
install(app: App): void
}
3. createRouter
上正菜咯。这个函数加上注释接近千行,或许要分段拆解了。函数的具体类型:(options: RouterOptions) => Router。接下来逐段分析函数体:
- 创建
matcher - 从选项参数中取出
parseQuery,stringifyQuery,routerHistory;前两者有默认值,而后者是必需的选项 - 如果没有提供
routerHistory选项,则在DEV环境下就会报错告知开发者
// 根据options创建matcher,稍后看一下createRouterMatcher是何许人也
const matcher = createRouterMatcher(options.routes, options)
// 得到解析/字符串化query的方式,以及路由模式
const parseQuery = options.parseQuery || originalParseQuery
const stringifyQuery = options.stringifyQuery || originalStringifyQuery
const routerHistory = options.history
if (__DEV__ && !routerHistory)
throw new Error(
'Provide the "history" option when calling "createRouter()":' +
' https://next.router.vuejs.org/api/#history.'
)
- 创建全局守卫
- 创建当前路由对象(使用
shallowRef) - 初始化
pending路由位置 - 设置滚动恢复行为:如果是在浏览器环境且选项参数中给出了滚动行为,并且路由模式
history中有scrollRestoration属性,则将history.scrollRestoratio设置为手动manual;
const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const afterGuards = useCallbacks<NavigationHookAfter>()
const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
START_LOCATION_NORMALIZED
)
let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED
// leave the scrollRestoration if no scrollBehavior is provided
if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) {
history.scrollRestoration = 'manual'
}
添加几个辅助函数:
normalizeParams-- 规范化参数encodeParams-- 编码参数decodeParams-- 解码参数
const normalizeParams = applyToParams.bind(
null,
paramValue => '' + paramValue
)
const encodeParams = applyToParams.bind(null, encodeParam)
const decodeParams: (params: RouteParams | undefined) => RouteParams =
// @ts-expect-error: intentionally avoid the type check
applyToParams.bind(null, decode)
路由的增删查:
-
addRoute-- 还记得addRoute有两个重载签名的嘛?因此这里的实现签名,第一个参数名为parentOrRoute,兼顾RouteRecordName | RouteRecordRaw,而第二个参数为可选参数,仅当parentOrRoute为RouteRecordName时才用的上。这里其实可以略微优化一下,调换一下参数的位置,使第一个参数为route,第二个参数叫RouteRecordName(可选),这样就不用判断parentOrRoute的TS类型了。-
判断第一个参数:
- 当第一个参数为路由名称时,使用
matcher.getRecordMatcher根据路由名称获取路由对象(如果找不到该路由名称对应的路由对象,则会在开发环境下报错提示),此时要添加的路由为第二个参数 route; - 当第一个参数不是路由名称时,则应为路由对象,要添加的路由即为该参数;
- 调用
matcher.addRoute,将路由添加到对应的父级路由或根路由下
- 当第一个参数为路由名称时,使用
-
-
removeRoute-- 根据参数name查找到对应的路由对象,再调用matcher.removeRoute将其移除;如果没找到该路由对象,会在开发环境下发出提醒; -
getRoutes-- 调用matcher.getRoutes获得所有的routeMatcher,再通过map方法得到对应的record,即所有的路由信息。 -
hasRoute-- 根据路由名称,调用matcher.getRecordMatcher查找是否有对应的路由对象;
function addRoute(
parentOrRoute: RouteRecordName | RouteRecordRaw,
route?: RouteRecordRaw
) {
let parent: Parameters<(typeof matcher)['addRoute']>[1] | undefined
let record: RouteRecordRaw
if (isRouteName(parentOrRoute)) {
// 当第一个参数为路由名称时,使用 matcher.getRecordMatcher 根据路由名称获取路由对象
parent = matcher.getRecordMatcher(parentOrRoute)
// 如果找不到该路由名称对应的路由对象,则会在开发环境下报错提示
if (__DEV__ && !parent) {
warn(
`Parent route "${String(
parentOrRoute
)}" not found when adding child route`,
route
)
}
// 此时要添加的路由为第二个参数 route
record = route!
} else {
// 当第一个参数不是路由名称时,则应为路由对象,要添加的路由即为此参数
record = parentOrRoute
}
// 通过 matcher.addRoute 将路由添加到对应的父级路由下,当 parent 为空时,则添加到根路由下
return matcher.addRoute(record, parent)
}
function removeRoute(name: RouteRecordName) {
// 查找路由对象
const recordMatcher = matcher.getRecordMatcher(name)
if (recordMatcher) {
// 找到了则移除
matcher.removeRoute(recordMatcher)
} else if (__DEV__) {
// 找不到则会在开发环境下进行提示
warn(`Cannot remove non-existent route "${String(name)}"`)
}
}
function getRoutes() {
return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
}
function hasRoute(name: RouteRecordName): boolean {
return !!matcher.getRecordMatcher(name)
}
看到这儿,我的心里充满了问号。matcher是个啥东东?怎么增删查哪儿都有它?createRouter函数体第一句就用到它,一行之下,千行之上,足以显示出其身份之尊贵。略微翻了下代码,关于matcher的有个单独的文件夹,四五个新文件在那儿盯着我,吓得我赶紧关了电脑。天晓得是不是一个文件里又有几千行的蚂蚁等着我呢。毕加思索之后,我决定过两天再另开一篇。现在还是继续createRouter的内容吧,虽然这个也不容易,但是来都来了,总不至于半途而废咯。
-
resolve-- 通过参数rawLocation以及当前路由位置currentLocation来生成一个新的路由位置,还有个私生子href,啧啧。-
拷贝一份当前路由位置信息
currentLocation; -
如果参数
rawLocation是个字符串:- 则根据
rawLocation和currentLocation来得到规范化的路由位置locationNormalized; - 根据
locationNormalized调用matcher.resolve来得到对应的路由信息; - 根据
locationNormalized.fullPath调用routerHistory.createHref来获得href; - 将
locationNormalized,matchedRoute等对象合并并返回合并的结果;
- 则根据
-
如果参数
rawLocation不是字符串:- 处理
params并获取matchedRoute; - 从
rawLocation中取出hash属性,默认为空字符串; - 生成
fullPath字符串; - 根据
fullPath字符串得到href; - 最后依然是合并各个对象并返回结果。
- 处理
-
function resolve(
rawLocation: Readonly<RouteLocationRaw>,
currentLocation?: RouteLocationNormalizedLoaded
): RouteLocation & { href: string } {
// const objectLocation = routerLocationAsObject(rawLocation)
// we create a copy to modify it later
currentLocation = assign({}, currentLocation || currentRoute.value)
if (typeof rawLocation === 'string') {
const locationNormalized = parseURL(
parseQuery,
rawLocation,
currentLocation.path
)
const matchedRoute = matcher.resolve(
{ path: locationNormalized.path },
currentLocation
)
const href = routerHistory.createHref(locationNormalized.fullPath)
if (__DEV__) {
if (href.startsWith('//'))
warn(
`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`
)
else if (!matchedRoute.matched.length) {
warn(`No match found for location with path "${rawLocation}"`)
}
}
// locationNormalized is always a new object
return assign(locationNormalized, matchedRoute, {
params: decodeParams(matchedRoute.params),
hash: decode(locationNormalized.hash),
redirectedFrom: undefined,
href,
})
}
// 当rawLocation不是字符串时,校验参数是否合规,不合规就会在开发环境下发出警告
if (__DEV__ && !isRouteLocation(rawLocation)) {
warn(
`router.resolve() was passed an invalid location. This will fail in production.\n- Location:`,
rawLocation
)
rawLocation = {}
}
let matcherLocation: MatcherLocationRaw
// path could be relative in object as well
if (rawLocation.path != null) {
if (
__DEV__ &&
'params' in rawLocation &&
!('name' in rawLocation) &&
// @ts-expect-error: the type is never
Object.keys(rawLocation.params).length
) {
warn(
`Path "${rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.`
)
}
matcherLocation = assign({}, rawLocation, {
path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path,
})
} else {
// remove any nullish param
// 移除空的参数
const targetParams = assign({}, rawLocation.params)
for (const key in targetParams) {
if (targetParams[key] == null) {
delete targetParams[key]
}
}
// pass encoded values to the matcher, so it can produce encoded path and fullPath
matcherLocation = assign({}, rawLocation, {
params: encodeParams(targetParams),
})
// current location params are decoded, we need to encode them in case the
// matcher merges the params
currentLocation.params = encodeParams(currentLocation.params)
}
const matchedRoute = matcher.resolve(matcherLocation, currentLocation)
const hash = rawLocation.hash || ''
if (__DEV__ && hash && !hash.startsWith('#')) {
warn(
`A `hash` should always start with the character "#". Replace "${hash}" with "#${hash}".`
)
}
// the matcher might have merged current location params, so
// we need to run the decoding again
matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params))
const fullPath = stringifyURL(
stringifyQuery,
assign({}, rawLocation, {
hash: encodeHash(hash),
path: matchedRoute.path,
})
)
const href = routerHistory.createHref(fullPath)
if (__DEV__) {
if (href.startsWith('//')) {
warn(
`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`
)
} else if (!matchedRoute.matched.length) {
warn(
`No match found for location with path "${
rawLocation.path != null ? rawLocation.path : rawLocation
}"`
)
}
}
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 === originalStringifyQuery
? normalizeQuery(rawLocation.query)
: ((rawLocation.query || {}) as LocationQuery),
},
matchedRoute,
{
redirectedFrom: undefined,
href,
}
)
}
locationAsObject-- 将路由位置参数转化为对象并返回;
function locationAsObject(
to: RouteLocationRaw | RouteLocationNormalized
): Exclude<RouteLocationRaw, string> | RouteLocationNormalized {
return typeof to === 'string'
? parseURL(parseQuery, to, currentRoute.value.path)
: assign({}, to)
}
checkCanceledNavigation-- 比对to和pendingLocation,如果不一致创建路由错误对象;
function checkCanceledNavigation(
to: RouteLocationNormalized,
from: RouteLocationNormalized
): NavigationFailure | void {
if (pendingLocation !== to) {
return createRouterError<NavigationFailure>(
ErrorTypes.NAVIGATION_CANCELLED,
{
from,
to,
}
)
}
}
接下来是两个常用的导航方法:push 和 replace。从代码上看,replace也是调用的push方法,只是参数中多了个配置replace = true。因此,合理推测调用push时手动设置replace属性为true(从pushWithRedirect的代码来看必须是true才有效),应该和直接调用replace效果一致。
function push(to: RouteLocationRaw) {
return pushWithRedirect(to)
}
function replace(to: RouteLocationRaw) {
return push(assign(locationAsObject(to), { replace: true }))
}
在进入pushWithRedirect 之前,最好先看看另一个函数,在pushWithRedirect中需要用到的handleRedirectRecord:
-
handleRedirectRecord-- 用于判断本次导航是否要重定向,接收参数to。从to.matched中取出最后一个,即lastMatched;仅当lastMatched有效且其上存在有效的redirect值时,会处理需要定向到的新路由位置,否则函数得到undefiend。- 根据
lastMatched.redirect获得新的目标位置newTargetLocation; - 若
newTargetLocation为字符串(此时redirect也是字符串),则将其处理为对象; - 如果
newTargetLocation中既没有有效的path由没有name属性,则会在开发环境发出警告; - 最后将新路由与
query,hash,params属性合并并返回结果。
- 根据
function handleRedirectRecord(to: RouteLocation): RouteLocationRaw | void {
const lastMatched = to.matched[to.matched.length - 1]
if (lastMatched && lastMatched.redirect) {
const { redirect } = lastMatched
let newTargetLocation =
typeof redirect === 'function' ? redirect(to) : redirect
// newTargetLocation转化为对象
if (typeof newTargetLocation === 'string') {
newTargetLocation =
newTargetLocation.includes('?') || newTargetLocation.includes('#')
? (newTargetLocation = locationAsObject(newTargetLocation))
: // force empty params
{ path: newTargetLocation }
// @ts-expect-error: force empty params when a string is passed to let
// the router parse them again
newTargetLocation.params = {}
}
// 不规范的路由,开发环境警告
if (
__DEV__ &&
newTargetLocation.path == null &&
!('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,
// avoid transferring params if the redirect has a path
params: newTargetLocation.path != null ? {} : to.params,
},
newTargetLocation
)
}
}
-
pushWithRedirect--push内部调用的方法,入参to和redirectedFrom(可选)。-
跟腱炎参数
to获取一些准备数据; -
调用上面的
handleRedirectRecord来判断是否需要重定向,若需要重定向,则递归pushWithRedirect; -
整理新的路由位置
toLocation; -
如若是新旧位置同一个路由位置,且不强制导航(
!force),则产生路由错误,并滚动至此前的页面位置; -
异常处理;
-
触发后置守卫钩子的调用
triggerAfterEach,遍历调用afterGuards中的每个守卫;function triggerAfterEach( to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded, failure?: NavigationFailure | void ): void { // navigation is confirmed, call afterGuards // TODO: wrap with error handlers afterGuards .list() .forEach(guard => runWithContext(() => guard(to, from, failure))) }runWithContext-- 使用app.runWithContext来调用传入的回调fn,或者直接调用fn:
function runWithContext<T>(fn: () => T): T { const app: App | undefined = installedApps.values().next().value // support Vue < 3.3 return app && typeof app.runWithContext === 'function' ? app.runWithContext(fn) : fn() }
-
看到这里,心里的疑问又多了一重,既然后置守卫钩子在这里触发,那么Router实例上另外两个全局守卫是在什么时候触发的呢?组件内守卫又是如何维护及触发的呢?先埋个坑。
function pushWithRedirect(
to: RouteLocationRaw | RouteLocation,
redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
const targetLocation: RouteLocation = (pendingLocation = resolve(to))
const from = currentRoute.value
const data: HistoryState | undefined = (to as RouteLocationOptions).state
const force: boolean | undefined = (to as RouteLocationOptions).force
// to could be a string where `replace` is a function
const replace = (to as RouteLocationOptions).replace === true
const shouldRedirect = handleRedirectRecord(targetLocation)
if (shouldRedirect)
return pushWithRedirect(
assign(locationAsObject(shouldRedirect), {
state:
typeof shouldRedirect === 'object'
? assign({}, data, shouldRedirect.state)
: data,
force,
replace,
}),
// keep original redirectedFrom if it exists
redirectedFrom || targetLocation
)
// if it was a redirect we already called `pushWithRedirect` above
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 }
)
// trigger scroll to allow scrolling to the same anchor
handleScroll(
from,
from,
// this is a push, the only way for it to be triggered from a
// history.listen is with a redirect, which makes it become a push
true,
// This cannot be the first navigation because the initial location
// cannot be manually navigated to
false
)
}
// 失败则返回失败的Promise,否则导航至新的目标位置
return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
// 异常处理
.catch((error: NavigationFailure | NavigationRedirectError) =>
isNavigationFailure(error)
? // navigation redirects still mark the router as ready
isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
? error
: markAsReady(error) // also returns the error
: // reject any unknown error
triggerError(error, toLocation, from)
)
.then((failure: NavigationFailure | NavigationRedirectError | void) => {
if (failure) {
if (
isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
) {
if (
__DEV__ &&
// we are redirecting to the same location we were already at
isSameRouteLocation(
stringifyQuery,
resolve(failure.to),
toLocation
) &&
// and we have done it a couple of times
redirectedFrom &&
// @ts-expect-error: added only in dev
(redirectedFrom._count = redirectedFrom._count
? // @ts-expect-error
redirectedFrom._count + 1
: 1) > 30
) {
warn(
`Detected a possibly infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow.\n Are you always returning a new location within a navigation guard? That would lead to this error. Only return when redirecting or aborting, that should fix this. This might break in production if not fixed.`
)
return Promise.reject(
new Error('Infinite redirect in navigation guard')
)
}
return pushWithRedirect(
// keep options
assign(
{
// preserve an existing replacement but allow the redirect to override it
replace,
},
locationAsObject(failure.to),
{
state:
typeof failure.to === 'object'
? assign({}, data, failure.to.state)
: data,
force,
}
),
// preserve the original redirectedFrom if any
redirectedFrom || toLocation
)
}
} else {
// if we fail we don't finalize the navigation
failure = finalizeNavigation(
toLocation as RouteLocationNormalizedLoaded,
from,
true,
replace,
data
)
}
triggerAfterEach(
toLocation as RouteLocationNormalizedLoaded,
from,
failure
)
return failure
})
}
checkCanceledNavigationAndReject-- 辅助函数,用于新的导航动作发生时通过抛出reject来拒绝并跳过所有的守卫钩子。
/**
* Helper to reject and skip all navigation guards if a new navigation happened
* @param to
* @param from
*/
function checkCanceledNavigationAndReject(
to: RouteLocationNormalized,
from: RouteLocationNormalized
): Promise<void> {
const error = checkCanceledNavigation(to, from)
return error ? Promise.reject(error) : Promise.resolve()
}
-
navigate-- 虽然叫navigate,但不是实际导航动作发生的函数,而是用于依次触发各个路由守卫。入参为to和from,提供给函数guardToPromiseFn,用于得到某些路由守卫,如beforeRouteLeave。- 抽离出变化的路由并解构:
leavingRecords,updatingRecords,enteringRecords得到各组件; - 从所有的即将离开的组件中抽离出
beforeRouteLeave守卫钩子,收集到guards队列中; - 在
guards队列中加入canceledNavigationCheck,用以判断是否需要跳过所有的守卫钩子的执行; - 调用队列中的守卫(除了
canceledNavigationCheck以外都是beforeRouteLeave); - 随后收集全局
beforeEach守卫以及canceledNavigationCheck并调用; - 再是收集需要更新的组件内的
beforeRouteUpdate守卫以及canceledNavigationCheck并调用; - 之后收集对应路由的
beforeEnter守卫以及canceledNavigationCheck并调用(复用的视图不会再次触发beforeEnter); - 清空上次从路由对象
to对应的所有的组件中得到的beforeRouteEnter守卫,重新收集本次相应的beforeRouteEnter守卫以及canceledNavigationCheck并调用; - 最后是收集全局守卫
beforeResolve以及canceledNavigationCheck并调用; - 以及
catch异常捕获并重新抛出,以传递给外层函数消费;
- 抽离出变化的路由并解构:
之前的坑在这儿填上了一半,所有的路由守卫都在这里调用,也明确了从路由A成功切换到路由B的过程中触发各守卫的优先级:
- 路由
A相关的所有组件的beforeRouteLeave; - 全局守卫
beforeEach; - 被更新组件内的路由守卫
beforeRouteUpdate; - 路由
B独享守卫beforeEnter(复用的视图对应的守卫不会再次触发); - 路由
B相关的所有组件的beforeRouteEnter; - 全局守卫
beforeResolve; - 全局守卫
afterEach(在navigate执行后的then中触发,优先级在最后);
function navigate(
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): Promise<any> {
let guards: Lazy<any>[]
const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from)
// all components here have been resolved once because we are leaving
guards = extractComponentsGuards(
leavingRecords.reverse(),
'beforeRouteLeave',
to,
from
)
// leavingRecords is already reversed
for (const record of leavingRecords) {
record.leaveGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(
null,
to,
from
)
guards.push(canceledNavigationCheck)
// run the queue of per route beforeRouteLeave guards
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)
})
.then(() => {
// check the route beforeEnter
guards = []
for (const record of enteringRecords) {
// do not trigger beforeEnter on reused views
if (record.beforeEnter) {
if (isArray(record.beforeEnter)) {
for (const beforeEnter of record.beforeEnter)
guards.push(guardToPromiseFn(beforeEnter, to, from))
} else {
guards.push(guardToPromiseFn(record.beforeEnter, to, from))
}
}
}
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
// clear existing enterCallbacks, these are added by extractComponentsGuards
to.matched.forEach(record => (record.enterCallbacks = {}))
// check in-component beforeRouteEnter
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from,
runWithContext
)
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// check global guards beforeResolve
guards = []
for (const guard of beforeResolveGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
// catch any navigation canceled
.catch(err =>
isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
? err
: Promise.reject(err)
)
)
}
runGuardQueue-- 调用守卫队列
// TODO: type this as NavigationGuardReturn or similar instead of any
function runGuardQueue(guards: Lazy<any>[]): Promise<any> {
return guards.reduce(
(promise, guard) => promise.then(() => runWithContext(guard)),
Promise.resolve()
)
}
在这儿产生了个疑问,guards队列中最后入队的是canceledNavigationCheck,但是按理说每次调用队列里的守卫时,应该是最先执行canceledNavigationCheck从而决定是否执行其它所有的钩子才对?但是从上面的代码来看,似乎canceledNavigationCheck是最后执行的?暂时没搞明白,有懂的小伙伴欢迎评论区指导我一下,不胜感激!
finalizeNavigation-- 清除路由守卫;push类型的导航则会通过routerHistory.replace或routerHistory.push来切换路由,关于routerHistory先留个坑;触发滚动行为;
/**
* - Cleans up any navigation guards
* - Changes the url if necessary
* - Calls the scrollBehavior
*/
function finalizeNavigation(
toLocation: RouteLocationNormalizedLoaded,
from: RouteLocationNormalizedLoaded,
isPush: boolean,
replace?: boolean,
data?: HistoryState
): NavigationFailure | void {
// a more recent navigation took place
const error = checkCanceledNavigation(toLocation, from)
if (error) return error
// only consider as push if it's not the first navigation
const isFirstNavigation = from === START_LOCATION_NORMALIZED
const state: Partial<HistoryState> | null = !isBrowser ? {} : history.state
// change URL only if the user did a push/replace and if it's not the initial navigation because
// it's just reflecting the url
if (isPush) {
// on the initial navigation, we want to reuse the scroll position from
// history state if it exists
// replace或者页面首次导航时调用replace方法
if (replace || isFirstNavigation)
routerHistory.replace(
toLocation.fullPath,
assign(
{
scroll: isFirstNavigation && state && state.scroll,
},
data
)
)
// 否则调用push方法
else routerHistory.push(toLocation.fullPath, data)
}
// accept current navigation
// 设置当前路由
currentRoute.value = toLocation
// 处理滚动
handleScroll(toLocation, from, isPush, isFirstNavigation)
// 一切就绪
markAsReady()
}
-
setupListeners-- 安装路由侦听器,监测路由的手动变更-
若
removeHistoryListener已有有效值,则直接返回,避免因首次导航不合规导致重复安装; -
否则,调用用
routerHistory.listen来对监听路由,并得到removeHistoryListener用于解除监听:- 若是
Router实例上listening属性为虚值,则什么也不做(此时应该是微前端项目,路由的监听可交由微前端框架来处理) - 通过
resolve解析to参数,得到要去的路由位置toLocation; - 检测如果需要重定向,则立即进行重定向导航;
pendingLocation指向toLocation;- 如果是在浏览器环境,则保存当前页面滚动位置,以便后续回到本页面时恢复到该位置;
- 调用
navigate进行导航动作以及异常处理; triggerAfterEach触发afterEach守卫;
- 若是
-
let removeHistoryListener: undefined | null | (() => void)
// attach listener to history to trigger navigations
function setupListeners() {
// avoid setting up listeners twice due to an invalid first navigation
if (removeHistoryListener) return
removeHistoryListener = routerHistory.listen((to, _from, info) => {
// 微前端项目,在Vue Router中关闭路由监听
if (!router.listening) return
// cannot be a redirect route because it was in history
const toLocation = resolve(to) as RouteLocationNormalized
// due to dynamic routing, and to hash history with manual navigation
// (manually changing the url or calling history.hash = '#/somewhere'),
// there could be a redirect record in history
// 动态路由中手动更改url或调用history.hash,就可能会产生重定向记录
const shouldRedirect = handleRedirectRecord(toLocation)
if (shouldRedirect) {
pushWithRedirect(
assign(shouldRedirect, { replace: true }),
toLocation
).catch(noop)
return
}
pendingLocation = toLocation
const from = currentRoute.value
// TODO: should be moved to web history?
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)
) {
// Here we could call if (info.delta) routerHistory.go(-info.delta,
// false) but this is bug prone as we have no way to wait the
// navigation to be finished before calling pushWithRedirect. Using
// a setTimeout of 16ms seems to work but there is no guarantee for
// it to work on every browser. So instead we do not restore the
// history entry and trigger a new navigation as requested by the
// navigation guard.
// the error is already handled by router.push we just want to avoid
// logging the error
pushWithRedirect(
(error as NavigationRedirectError).to,
toLocation
// avoid an uncaught rejection, let push call triggerError
)
.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,
ErrorTypes.NAVIGATION_ABORTED |
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: NavigationFailure | void) => {
failure =
failure ||
finalizeNavigation(
// after navigation, all matched components are resolved
toLocation as RouteLocationNormalizedLoaded,
from,
false // isPush参数为false,不会当成push类型的导航来处理
)
// 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, ErrorTypes.NAVIGATION_CANCELLED)
) {
routerHistory.go(-info.delta, false)
} else if (
info.type === NavigationType.pop &&
isNavigationFailure(
failure,
ErrorTypes.NAVIGATION_ABORTED | 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 as RouteLocationNormalizedLoaded,
from,
failure
)
})
// avoid warnings in the console about uncaught rejections, they are logged by triggerErrors
.catch(noop)
})
}
初始化就绪状态与错误侦听器:
let readyHandlers = useCallbacks<OnReadyCallback>()
let errorListeners = useCallbacks<_ErrorListener>()
let ready: boolean
-
triggerError-- 触发异常:- 就绪状态标记为异常;
- 依次触发
errorListeners中的异常处理程序; - 返回
Peomise拒绝状态;
/**
* Trigger errorListeners added via onError and throws the error as well
*
* @param error - error to throw
* @param to - location we were navigating to when the error happened
* @param from - location we were navigating from when the error happened
* @returns the error as a rejected promise
*/
function triggerError(
error: any,
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): Promise<unknown> {
markAsReady(error)
const list = errorListeners.list()
if (list.length) {
list.forEach(handler => handler(error, to, from))
} else {
if (__DEV__) {
warn('uncaught error during route navigation:')
}
console.error(error)
}
// reject the error no matter there were error listeners or not
return Promise.reject(error)
}
isReady-- 用于判断前端路由器是否已就绪;
function isReady(): Promise<void> {
if (ready && currentRoute.value !== START_LOCATION_NORMALIZED)
return Promise.resolve()
return new Promise((resolve, reject) => {
readyHandlers.add([resolve, reject])
})
}
markAsReady:标记就绪状态以及安装路由监听器;有两个重载签名:接收error并返回错误或不接收参数且无返回值(返回undefiend);
/**
* Mark the router as ready, resolving the promised returned by isReady(). Can
* only be called once, otherwise does nothing.
* @param err - optional error
*/
function markAsReady<E = any>(err: E): E
function markAsReady<E = any>(): void
function markAsReady<E = any>(err?: E): E | void {
if (!ready) {
// still not ready if an error happened
ready = !err
// setupListeners中有检测是否已安装监听器,因此无需担心重复执行
setupListeners()
readyHandlers
.list()
.forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
readyHandlers.reset()
}
return err
}
-
handleScroll-- 处理滚动行为- 非浏览器环境或者没有在选项参数中设置
scrollBehavior,则不会有任何效果; - 获取滚动位置:对于是否是
push类型的导航,获取方式不一样; - 结合
nextTick来滚动至对应位置;
- 非浏览器环境或者没有在选项参数中设置
// Scroll behavior
function handleScroll(
to: RouteLocationNormalizedLoaded,
from: RouteLocationNormalizedLoaded,
isPush: boolean,
isFirstNavigation: boolean
): Promise<unknown> {
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) ||
null
return nextTick()
.then(() => scrollBehavior(to, from, scrollPosition))
.then(position => position && scrollToPosition(position))
.catch(err => triggerError(err, to, from))
}
到这里,Router的实例所需要的内容基本都创建完毕。剩下的还可以关注的内容便是install方法。众所周知这是提供给app.use使用的。
- 添加两个全局组件
RouterLink,RouterView; - 将
Router的实例router和当前路由信息route挂到Vue的全局属性app.config.globalProperties.$router和app.config.globalProperties.$route上,以便再模板以及proxy中使用;值得一提的是,组合式API的useRouter和useRoute也是通过inject取的这俩; - 客户端(浏览器环境下)会导航至初始路由;
- 用
Object.defineProperty配合shallowReactive得到响应式路由信息; - 提供路由器、响应式路由信息等数据;
- 将
Vue应用添加到已安装应用中; - 修改
Vue应用的卸载方法,添加一些重置路由器的逻辑; - 开发环境下还会添加开发者工具;
const go = (delta: number) => routerHistory.go(delta)
let started: boolean | undefined
const installedApps = new Set<App>()
const router: Router = {
currentRoute,
listening: true,
addRoute,
removeRoute,
hasRoute,
getRoutes,
resolve,
options,
push,
replace,
go,
back: () => go(-1),
forward: () => go(1),
// 在项目里使用到的几个路由钩子,都不过是各个守卫的`add`方法
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add,
afterEach: afterGuards.add,
onError: errorListeners.add,
isReady,
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 RouteLocationNormalizedLoaded
for (const key in START_LOCATION_NORMALIZED) {
Object.defineProperty(reactiveRoute, key, {
get: () => currentRoute.value[key as keyof RouteLocationNormalized],
enumerable: true,
})
}
app.provide(routerKey, router)
app.provide(routeLocationKey, shallowReactive(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)
}
},
}
最后是返回创建好的router:
export function createRouter(options: RouterOptions): Router {
// ...
return router
}
顺便看一下用到的另一个方法:extractChangingRecords用于获取离开、更新、进入的路由记录:
- 既能被
from匹配到又会被to匹配到的会被放入将被更新的数组中; - 只被
from匹配到的则是将离开的路由数组中; - 只被
to匹配到的则是在即将进入的路由数组中;
function extractChangingRecords(
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
) {
const leavingRecords: RouteRecordNormalized[] = []
const updatingRecords: RouteRecordNormalized[] = []
const enteringRecords: RouteRecordNormalized[] = []
const len = Math.max(from.matched.length, to.matched.length)
for (let i = 0; i < len; i++) {
const recordFrom = from.matched[i]
if (recordFrom) {
if (to.matched.find(record => isSameRouteRecord(record, recordFrom)))
updatingRecords.push(recordFrom)
else leavingRecords.push(recordFrom)
}
const recordTo = to.matched[i]
if (recordTo) {
// the type doesn't matter because we are comparing per reference
if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) {
enteringRecords.push(recordTo)
}
}
}
return [leavingRecords, updatingRecords, enteringRecords]
}
虽然才一千几百行代码,阅读起来感觉就是个小卡拉米,写起来却跟打Boss一样。一通乱分析看起来洋洋洒洒春风得意,实则早已戴上了痛苦面具饱受折磨。为什么我要经历这种人间疾苦,呜呜呜。
虽然但是,也不算白干,尽管没多少收获,但至少我累着了。那就这样吧,下一章再见!