本文基于
vue-router 4.1.6
版本源码进行剖析本文重点剖析
Vue Router
的WebHashHistory
模式,不会对WebHistory
和MemoryHistory
模式过多剖析
文章内容
从 Vue Router
的初始化代码动手,逐渐剖析对应的代码流程和波及到的操作方法(push
、replace
、pop
)
本文将着重于:
Vue Router
是如何利用routes
数组建设路由映射,路由权重是如何初始化Vue Router
的push
、replace
、pop
流程具体执行了什么?是如何找到对应的路由数据Vue Router
提供的RouterView
和RouterLink
源码剖析Vue Router
提供的多个组合式 API 的源码剖析,包含onBeforeRouteLeave
、onBeforeRouteLeave
、useRouter
、useRoute
、useLink
Vue Router
源码正文中波及到的issues
的问题解说和对应的修复代码剖析
本文的最初将基于多个问题进行 Vue Router
源码的系统性总结
前置常识
Vue Router 介绍
摘录于 Vue Router 官网文档
Vue Router 是 Vue.js 的官网路由。它与 Vue.js 外围深度集成,让用 Vue.js 构建单页利用变得轻而易举。性能包含:
- 嵌套路由映射
- 动静路由抉择
- 模块化、基于组件的路由配置
- 路由参数、查问、通配符
- 展现由 Vue.js 的过渡零碎提供的过渡成果
- 粗疏的导航管制
- 主动激活 CSS 类的链接
- HTML5 history 模式或 hash 模式
- 可定制的滚动行为
- URL 的正确编码
window.location 属性
属性 | 形容 |
---|---|
href | https://test.example.com:8090/vue/router#test?search=1 |
protocol | https: |
host/hostname | test.example.com |
port | 8090 |
pathname | /vue/router |
search | ?search=1 |
hash | #test |
<base>:文档根 URL 元素
摘录于 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
- HTML
<base>
元素 指定用于一个文档中蕴含的所有绝对 URL 的根 URL - 只能有一个
<base>
元素 - 一个文档的根本 URL,能够通过应用 document.baseURI(en-US) 的 JS 脚本查问,如果文档不蕴含
<base>
元素,baseURI
默认为document.location.href
前置问题
- 内部定义的路由,是如何在
Vue Router
外部建立联系的 Vue Router
是如何实现push
、replace
、pop
操作的Vue Router
是如何命中多层嵌套路由,比方/parent/child/child1
须要加载多个组件,是如何实现的Vue Router
有什么导航?触发的流程是怎么的Vue Router
的导航守卫是如何做到链式调用的Vue Router
的beforeRouteEnter
和beforeRouteUpdate
的触发机会Vue Router
中route
和router
的区别hash
模式跟h5 history
模式在Vue Router
中有什么区别Vue Router
的hash
模式重定向后还会保留浏览记录吗?比方重定向后再应用router.go(-1)
会返回重定向之前的页面吗Vue Router
的hash
模式什么中央最容易导致路由切换失败
下面所有问题将在第二篇文章 Vue3 相干源码 -Vue Router 源码解析 (二) 中进行解答
示例代码
代码来自于 Vue Router 官网文档
// 1. 定义路由组件.
// 也能够从其余文件导入
const Home = {template: '<div>Home</div>'}
const About = {template: '<div>About</div>'}
// 2. 定义一些路由
// 每个路由都须要映射到一个组件。// 咱们前面再探讨嵌套路由。const routes = [{ path: '/', component: Home},
{path: '/about', component: About},
]
// 3. 创立路由实例并传递 `routes` 配置
// 你能够在这里输出更多的配置,但咱们在这里
// 临时放弃简略
const router = VueRouter.createRouter({
// 4. 外部提供了 history 模式的实现。为了简略起见,咱们在这里应用 hash 模式。history: VueRouter.createWebHashHistory(),
routes, // `routes: routes` 的缩写
})
// 5. 创立并挂载根实例
const app = Vue.createApp({})
// 确保 _use_ 路由实例使
// 整个利用反对路由。app.use(router)
app.mount('#app')
剖析的外围示例代码
const router = VueRouter.createRouter({history: VueRouter.createWebHashHistory(),
routes
})
const app = Vue.createApp({})
app.use(router)
上面将依照创立
history
、初始化router
、Vue 应用router
的程序进行源码剖析
createWebHashHistory 创立 history
hash
模式是用createWebHashHistory()
创立的历史记录模式
从上面代码能够晓得,解决了 base
数据:如果没有传入 url
,则拼接location.pathname + location.search
,而后加上#
最初还是调用了 createWebHistory(base)
的办法进行 history
的创立
function createWebHashHistory(base) {
// Make sure this implementation is fine in terms of encoding, specially for IE11
// for `file://`, directly use the pathname and ignore the base
// href="https://example.com" 的 location.pathname 也是 "/"
base = location.host ? (base || location.pathname + location.search) : '';
// allow the user to provide a `#` in the middle: `/base/#/app`
if (!base.includes('#'))
base += '#';
return createWebHistory(base);
}
间接看下面的代码会有点懵,间接看官网文档会比拟好了解点,上面内容参考自 https://router.vuejs.org/zh/api/#createwebhashhistory,是
createWebHashHistory(base)
的参数阐明
Parameter | Type | Description |
---|---|---|
base | string |
提供可选的 base。默认是 location.pathname + location.search。如果 head 中有一个 <base>,它的值将被疏忽,而采纳这个参数。但请留神它会影响所有的 history.pushState() 调用 ,这意味着如果你应用一个 <base> 标签,它的 href 值 必须与这个参数相匹配 (请疏忽 # 前面的所有内容) |
// at https://example.com/folder
createWebHashHistory() // 给出的网址为 `https://example.com/folder#`
createWebHashHistory('/folder/') // 给出的网址为 `https://example.com/folder/#`
// 如果在 base 中提供了 `#`,则它不会被 `createWebHashHistory` 增加
createWebHashHistory('/folder/#/app/') // 给出的网址为 `https://example.com/folder/#/app/`
// 你应该防止这样做,因为它会更改原始 url 并打断正在复制的 url
createWebHashHistory('/other-folder/') // 给出的网址为 `https://example.com/other-folder/#`
// at file:///usr/etc/folder/index.html
// 对于没有 `host` 的地位,base 被疏忽
createWebHashHistory('/iAmIgnored') // 给出的网址为 `file:///usr/etc/folder/index.html#`
从下面例子能够看出,如果传入 base
字符串,会以传入 base
优先级最高去拼接,而后才是 location.pathname + location.search
同时会检测传入 base
是否含有"#"
,如果没有,则在前面增加"#"
当然,也会存在传入
base
跟location.pathname
不一样的状况,如下面例子/other-folder/
所示,会间接更改网址去除location.pathname
,改为base
createWebHistory 创立 history
整体概述
routerHistory
对标的是原生的 history
对象,routerHistory
在原生的 history
对象的 API
根底上,削减了一些逻辑解决,实现 Vue
的路由切换、路由映射组件、组件切换的性能
function createWebHistory(base) {
// 步骤 1: normalizeBase 整顿 url
base = normalizeBase(base);
// 步骤 2: useHistoryStateNavigation
const historyNavigation = useHistoryStateNavigation(base);
// 步骤 3: useHistoryListeners
const historyListeners = useHistoryListeners(base,
historyNavigation.state, historyNavigation.location, historyNavigation.replace);
// 步骤 4: 合并 historyNavigation 和 historyListeners,整合为 routerHistory
const routerHistory = assign({
// it's overridden right after
location: '',
base,
go,
createHref: createHref.bind(null, base),
}, historyNavigation, historyListeners);
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,});
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,});
return routerHistory;
}
function go(delta, triggerListeners = true) {if (!triggerListeners)
historyListeners.pauseListeners();
history.go(delta);
}
步骤 1: normalizeBase
从下面剖析能够晓得,如果有传入 base
或者 自身存在location.pathname
,那么达到normalizeBase(base)
的base
就不可能为空
然而也有可能存在
- 没有传入
base
+ 自身不存在location.pathname
- 没有传入
base
+ 自身不存在location.host
(file://xxx
)
当没有 base
时,咱们就会检测是否具备 <base>
标签,<base>
标签的含意能够参考 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base,如果没有 <base>
标签,则应用 location.href
作为根底的链接,如果有 <base>
标签,则应用该标签作为根底链接
For example, given <base href=”
https://example.com/
“> and this link: To anchor. The link points tohttps://example.com/#anchor
而后替换域名局部的字符串,最终返回 base
="/xxxx"
或者"#xxxxx"
function normalizeBase(base) {if (!base) {if (isBrowser) {
// respect <base> tag
const baseEl = document.querySelector('base');
base = (baseEl && baseEl.getAttribute('href')) || '/';
// 剔除 https://xx.xxxx.com 这部分的字母
base = base.replace(/^\w+:\/\/[^\/]+/, '');
} else {base = '/';}
}
if (base[0] !== '/' && base[0] !== '#')
base = '/' + base;
// 删除尾部的斜杠
return removeTrailingSlash(base);
}
const TRAILING_SLASH_RE = /\/$/;
const removeTrailingSlash = (path) => path.replace(TRAILING_SLASH_RE, '');
个别状况下,通过 normalizeBase()
失去的 base
的构造为location.pathname
+location.search
+"#"
,具体构造如上面所示:
上面链接为测试链接,仅仅示意不思考非凡状况下的
base
构造,没有其它含意
base = "/Frontend-Articles/vue3-debugger/router/vue-router.html?_ijt=q70k46ba94eddqp8fu39ta28fh&_ij_reload#"
步骤 2: useHistoryStateNavigation
整体概述
该办法初始化的 historyNavigation
蕴含了以后的路由、以后的堆栈信息以及对应的 push
加操作和 replace
笼罩操作,同时检测 history.state
是否为空,如果为空,须要压入一个初始化的currentLocation
除了
push
、replace
等惯例操作,咱们晓得一个路由还须要具备路由后退的监听,Vue Router
将后退操作的监听放在下一节要剖析的useHistoryListeners
中
// const historyNavigation = useHistoryStateNavigation(base);
function useHistoryStateNavigation(base) {const { history, location} = window;
// currentLocation.value= 除去 "#" 后的字符串
const currentLocation = {value: createCurrentLocation(base, location),
};
const historyState = {value: history.state};
// build current history entry as this is a fresh navigation
if (!historyState.value) {
changeLocation(currentLocation.value, {
back: null,
current: currentLocation.value,
forward: null,
// the length is off by one, we need to decrease it
position: history.length - 1,
replaced: true,
// don't add a scroll as the user may have an anchor, and we want
// scrollBehavior to be triggered without a saved position
scroll: null,
}, true);
}
function changeLocation(to, state, replace) {...}
function replace(to, data) {...}
function push(to, data) {...}
return {
location: currentLocation,
state: historyState,
push,
replace,
};
}
createCurrentLocation()
从 window.location
对象中创立一个规范化的数据作为以后的门路,能够简略认为最初失去的门路就是下面注册的 routes
中的其中一个路由门路
具体逻辑剖析请看上面的例子剖析
function createCurrentLocation(base, location) {const { pathname, search, hash} = location;
// allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
const hashPos = base.indexOf('#');
if (hashPos > -1) {let slicePos = hash.includes(base.slice(hashPos))
? base.slice(hashPos).length
: 1;
let pathFromHash = hash.slice(slicePos);
// prepend the starting slash to hash so the url starts with /#
if (pathFromHash[0] !== '/')
pathFromHash = '/' + pathFromHash;
return stripBase(pathFromHash, '');
}
const path = stripBase(pathname, base);
return path + search + hash;
}
function stripBase(pathname, base) {
// no base or base is not found at the beginning
if (!base || !pathname.toLowerCase().startsWith(base.toLowerCase()))
return pathname;
return pathname.slice(base.length) || '/';
}
连续下面例子所用的文件门路,咱们能够晓得:
location.href = "http://localhost:63342/Frontend-Articles/vue3-debugger/router/vue-router.html?_ijt=q70k46ba94eddqp8fu39ta28fh&_ij_reload#/about";
base = "/Frontend-Articles/vue3-debugger/router/vue-router.html?_ijt=q70k46ba94eddqp8fu39ta28fh&_ij_reload#";
location.hash = "#/about";
location.pathname = "/Frontend-Articles/vue3-debugger/router/vue-router.html";
通过 hash.slice(slicePos)
失去的 pathFromHash
为
pathFromHash = "/about"
最终通过stripBase(pathFromHash, '')
,间接返回pathFromHash ="/about"
!!!! 下面的举例是比拟失常一点的门路,然而事实中必定会存在各种各样的门路 =_= 前面遇到奇怪的门路再回来补充例子,临时跳过这部分规范化门路逻辑的思考
最终失去目前的路由门路为
const currentLocation = {value: "/about"};
changeLocation()
function changeLocation(to, state, replace) {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) {
// Force the navigation, this also resets the call count
location[replace ? 'replace' : 'assign'](url);
}
}
传入要跳转的门路 to
,而后拼接要跳转的门路url
在下面的例子中,咱们晓得
base = "/Frontend-Articles/vue3-debugger/router/vue-router.html?_ijt=q70k46ba94eddqp8fu39ta28fh&_ij_reload#";
url = base.slice(hashIndex)) + to = "#/Frontend-Articles/vue3-debugger/router/vue-router.html?_ijt=q70k46ba94eddqp8fu39ta28fh&_ij_reload#/about"
changeLocation()
实质就是拼接了跳转门路,而后
- 调用浏览器原生 API 提供的
window.history.pushState
/window.history.replaceState
办法 - 更新
historyState.value
一开始调用 changeLocation()
传入的 state
数据如下所示
{
back: null,
current: currentLocation.value,
forward: null,
// the length is off by one, we need to decrease it
position: history.length - 1,
replaced: true,
// don't add a scroll as the user may have an anchor, and we want
// scrollBehavior to be triggered without a saved position
scroll: null,
}
changeLocation()
的过程中如果产生谬误,则强制跳转,调用原生的 window.location.assign(url)
:使窗口加载并显示指定url
处的document
导航产生后,用户能够通过按“后退”按钮导航回到
location.assign
替换的页面,即这个办法会产生浏览器历史记录,不是间接笼罩以后页面
window.location.assign
能够参考文档 assign,跟window.location.replace
不同点在于,replace
不会保留目前要替换的页面记录
push()
代码逻辑也比拟清晰和简略,次要分为 5 个步骤
- 应用
forward: to
记录要跳转的路由,并且放入currentState
中 - 应用
changeLocation()
进行history.replaceState()
替换以后的浏览记录 - 应用
buildState()
构建出新的路由对象,造成新的路由state
- 应用
changeLocation()
进行history.pushState()
新增一条新的浏览记录 - 更新多个办法都应用的变量
currentLocation
为以后的新的路由地址currentLocation.value
=to
function push(to, data) {const currentState = assign({},
// use current history state to gracefully handle a wrong call to
// history.replaceState
// https://github.com/vuejs/router/issues/366
historyState.value, history.state, {
forward: to,
scroll: computeScrollPosition(),});
if (!history.state) {
// 如果以后没有 history.state,报错提醒:// 如果您手动调用 history.replaceState,请确保保留现有的历史状态
}
changeLocation(currentState.current, currentState, true);
const state = assign({}, buildState(currentLocation.value, to, null), {position: currentState.position + 1}, data);
changeLocation(to, state, false);
currentLocation.value = to;
}
function buildState(back, current, forward, replaced = false, computeScroll = false) {
return {
back,
current,
forward,
replaced,
position: window.history.length,
scroll: computeScroll ? computeScrollPosition() : null,};
}
replace()
构建出新的路由对象,造成新的路由 state
,而后应用changeLocation()
进行 history.replaceState()
替换以后的浏览记录
在
changeLocation()
中也进行historyState.value
=state
更新多个办法都应用的变量 currentLocation
为以后的新的路由地址currentLocation.value
=to
function replace(to, data) {const state = assign({}, history.state, buildState(historyState.value.back,
// keep back and forward entries but override current position
to, historyState.value.forward, true), data, {position: historyState.value.position});
changeLocation(to, state, true);
currentLocation.value = to;
}
小结
从下面的代码能够晓得,historyNavigation
最终是一个对象,具备 location
、state
、push
、replace
属性,其中
location
:代表除去"#"
之后的门路内容,治理以后页面的门路数据,比方location="/about"
-
state
:保留原生的window.history.state
数据,比方上面的histroyState
就是代表window.history.state
数据// history.state 格局如下 const histroyState = { back: null, current: "/Frontend-Articles/vue3-debugger/router/vue-router.html?_ijt=q70k46ba94eddqp8fu39ta28fh&_ij_reload#/about", forward: null, position: 14, replaced: true, scroll: null } const state = {value: histroyState};
push()
:新增路由时调用的办法replace()
:替换路由时调用的办法
步骤 3: useHistoryListeners
整体概述
传入初始化门路以及 historyNavigation
保护的状态和办法,进行 historyListeners
的初始化
const historyListeners = useHistoryListeners(base,
historyNavigation.state, historyNavigation.location, historyNavigation.replace);
function useHistoryListeners(base, historyState, currentLocation, replace) {let listeners = [];
let teardowns = [];
let pauseState = null;
const popStateHandler = ({state,}) => {...};
function pauseListeners() {...}
function listen(callback) {...}
function beforeUnloadListener() {...}
function destroy() {...}
// set up the listeners and prepare teardown callbacks
window.addEventListener('popstate', popStateHandler);
window.addEventListener('beforeunload', beforeUnloadListener);
return {
pauseListeners,
listen,
destroy,
};
}
popStateHandler()
监听后退事件,这个监听办法比较复杂并且十分重要!
简略示例
window.addEventListener("popstate", (event) => {
console.log(`location: ${document.location}, state: ${JSON.stringify(event.state)}`
);
});
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // Logs "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // Logs "location: http://example.com/example.html, state: null"
history.go(2); // Logs "location: http://example.com/example.html?page=3, state: {"page":3}"
触发机会
history.pushState()
或者history.replaceState()
不会触发popStateHandler()
- 用户手动点击浏览器的后退按钮、
history.back()
、history.forward()
都会触发popStateHandler()
history.go(数字)
实质就是history.back()
/history.forward()
调用 1 次或者屡次,也会触发popStateHandler()
触发时携带的参数 state
摘录于 https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event#the_history_stack
如果被激活的历史条目是通过调用 history.pushState() 创立的,或者受到调用 history.replaceState() 的影响,则 popstate 事件的状态属性蕴含历史条目状态对象的正本
换句话说,
popStateHandler({state})
中state
不为空的前提是咱们始终都有应用pushState
/replaceState
源码剖析
从下面的简略示例能够晓得,携带的参数 state
是后退后以后的路由参数,如果存在,间接把它当作目前最新的 historyState.value=state
,而后依据后退前缓存的数据fromState
计算出对应的 delta
,最终调用listeners
进行监听办法的触发
const popStateHandler = ({state,}) => {const to = createCurrentLocation(base, location);
const from = currentLocation.value;
const fromState = 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);
}
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
});
});
};
window.addEventListener('popstate', popStateHandler);
如果没有携带参数 state
,咱们就无奈晓得目前的状态,因而会间接应用const to = createCurrentLocation(base, location)
构建出新的路由对象,造成新的路由 state
,而后应用changeLocation()
进行 history.replaceState()
替换以后的浏览记录
没有携带参数
state
时,listeners
进行监听办法触发的状态为NavigationDirection.unknown
function replace(to, data) {const state = assign({}, history.state, buildState(historyState.value.back,
// keep back and forward entries but override current position
to, historyState.value.forward, true), data, {position: historyState.value.position});
changeLocation(to, state, true);
currentLocation.value = to;
}
listen() and pauseListeners()
提供给内部注册监听的办法,并且返回进行该监听的办法teardown
function listen(callback) {
// 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;
}
function pauseListeners() {pauseState = currentLocation.value;}
问题:
- 什么时候触发
listen()
注册监听办法? listen()
注册的监听办法有什么用途?- 为什么要应用
pauseListeners()
暂停监听? pauseListeners()
是如何暂停办法执行的?
这些问题将在第二篇文章 Vue3 相干源码 -Vue Router 源码解析 (二) 的总结
Vue Router 的 hash 模式什么中央最容易导致路由切换失败
进行解答
beforeUnloadListener()注册监听
beforeunload
事件在行将来到以后页面(刷新或敞开)时触发,触发时会重置 history.replaceState
保留以后的 scroll
(来自Vue Router
的代码 git 提交记录形容)
该事件可用于弹出对话框,提醒用户是持续浏览页面还是来到以后页面,比方“确定要来到此页吗?”
function beforeUnloadListener() {const { history} = window;
if (!history.state)
return;
history.replaceState(assign({}, history.state, {scroll: computeScrollPosition() }), '');
}
window.addEventListener('beforeunload', beforeUnloadListener);
destroy()
革除所有 listeners
监听办法,移除全局注册的事件
function destroy() {for (const teardown of teardowns)
teardown();
teardowns = [];
window.removeEventListener('popstate', popStateHandler);
window.removeEventListener('beforeunload', beforeUnloadListener);
}
步骤 4: go()和 routerHistory 初始化
除了上述的 3 个步骤,最初一个步骤就是将下面步骤失去的 historyNavigation
、historyListeners
以及对应的其它根底数据进行合并成为routerHistory
其中
go()
事件除了第一个参数回退的层级,还有第二个参数triggerListeners
,阻止监听器触发执行,该办法的剖析将放在第二篇文章 Vue3 相干源码 -Vue Router 源码解析(二) 的总结中进行解答
function go(delta, triggerListeners = true) {if (!triggerListeners)
historyListeners.pauseListeners();
history.go(delta);
}
const routerHistory = assign({
// it's overridden right after
location: '',
base,
go,
createHref: createHref.bind(null, base),
}, historyNavigation, historyListeners);
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,});
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,});
return routerHistory;
createMemoryHistory 创立 history
createMemoryHistory
会创立一个基于内存的 history
,次要目标是为了解决 SSR
与后面两个办法不同的是,createMemoryHistory
保护一个队列 queue
和一个position
,来保障历史记录存储的正确性
这里不开展详细分析,请读者自行钻研
createRouter 创立 VueRouter 对象
整体概述
createRouter()
对 VueRouter.createWebHashHistory()
和routes
的数据进行合并和拼接,组合成 router
对象
// Vue 初始化 Vue Router 时的示例代码
const router = VueRouter.createRouter({
// 4. 外部提供了 history 模式的实现。为了简略起见,咱们在这里应用 hash 模式。history: VueRouter.createWebHashHistory(),
routes, // `routes: routes` 的缩写
})
function createRouter(options) {const matcher = createRouterMatcher(options.routes, options);
//... 多个办法,根本都是为上面初始化 router 的属性
// 比方 addRoute()、removeRoute()、hasRoute()
const router = {
currentRoute,
listening: true,
addRoute,
removeRoute,
hasRoute,
getRoutes,
resolve,
options,
push,
replace,
go,
back: () => go(-1),
forward: () => go(1),
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add,
afterEach: afterGuards.add,
onError: errorHandlers.add,
isReady,
install(app) {//... 省略,内部 app.use()时调用
},
};
return router;
}
这个办法的逻辑十分繁冗,本文不会具体开展讲,前面会讲一些重点的办法以及依据一些业务去剖析
createRouter
外面的办法,咱们只须要明确,这个办法创立的数据就是Vue Router
提供给内部应用的对象即可
createRouterMatcher: 依据内部传入的 options.routes 初始化路由配置
整体概述
依据内部传入的 options.routes
初始化路由配置,建设 matchers
数组
留神:
createRouterMatcher()
外部有addRoute()
办法,下面createRouter()
外部也有addRoute()
办法,不要搞混
// globalOptions={history, routes}
function createRouterMatcher(routes, globalOptions) {
// normalized ordered array of matchers
const matchers = [];
const matcherMap = new Map();
globalOptions = mergeOptions({strict: false, end: true, sensitive: false}, globalOptions);
function getRecordMatcher(name)
function addRoute(record, parent, originalRecord) {...}
function removeRoute(matcherRef) {...}
function getRoutes() {...}
function insertMatcher(matcher) {...}
function resolve(location, currentLocation) {...}
// add initial routes
routes.forEach(route => addRoute(route));
return {addRoute, resolve, removeRoute, getRoutes, getRecordMatcher};
}
从下面的代码,咱们能够晓得,除了一些办法的初始化之外,次要就执行了两个步骤:mergeOptions()
和routers.forEach(route=>addRoute(route))
mergeOptions
合并传递过去的参数
// partialOptions={history, routes}
// defaults={strict: false, end: true, sensitive: false}
function mergeOptions(defaults, partialOptions) {const options = {};
for (const key in defaults) {options[key] = key in partialOptions ? partialOptions[key] : defaults[key];
}
return options;
}
routers.forEach(route=>addRoute(route))
function addRoute(record, parent, originalRecord) {
//...
const mainNormalizedRecord = normalizeRouteRecord(record);
const normalizedRecords = [mainNormalizedRecord,];
if ('alias' in record) {const aliases = typeof record.alias === 'string' ? [record.alias] : record.alias;
for (const alias of aliases) {
// ... 省略解决别名的逻辑
normalizedRecords.push(assign({}, mainNormalizedRecord, {...}));
}
}
let matcher;
let originalMatcher;
for (const normalizedRecord of normalizedRecords) {
//...
// 步骤 1: 创立 matcher 对象
matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
//...
// 步骤 2: 创立 matcher 对象和它的 children-matcher 之间的关系
if (mainNormalizedRecord.children) {
const children = mainNormalizedRecord.children;
for (let i = 0; i < children.length; i++) {addRoute(children[i], matcher, originalRecord && originalRecord.children[i]);
}
}
//...
// 步骤 3: 将创立好的 matcher 插入到 matchers 数组中
insertMatcher(matcher);
}
return originalMatcher
? () => fcd
// since other matchers are aliases, they should be removed by the original matcher
removeRoute(originalMatcher);
}
: noop;
}
步骤 1: createRouteRecordMatcher 创立 matcher 对象
在路由对象 record
的根底上进行路由权重的计算以及正则表达式的构建,为前面门路的映射提供对应的 matcher
对象
function createRouteRecordMatcher(record, parent, options) {const parser = tokensToParser(tokenizePath(record.path), options);
// warn against params with the same name
const existingKeys = new Set();
for (const key of parser.keys) {existingKeys.add(key.name);
}
const matcher = assign(parser, {
record,
parent,
// these needs to be populated by the parent
children: [],
alias: [],});
if (parent) {
// both are aliases or both are not aliases
// we don't want to mix them because the order is used when
// passing originalRecord in Matcher.addRoute
if (!matcher.record.aliasOf === !parent.record.aliasOf)
parent.children.push(matcher);
}
return matcher;
}
tokenizePath()
和tokensToParser()
解析多种模式下的路由门路,上面将着重剖析Vue Router4
中的路由权重计算逻辑
多种类型的路由介绍
上面内容参考自 Vue Router 官网文档
动态路由
const routes = [{ path: '/', component: Home},
{path: '/about', component: About},
]
带参数的动静路由匹配
像 /users/johnny
和 /users/jolyn
这样的 URL 都会映射到同一个路由
const routes = [
// 动静字段以冒号开始
{path: '/users/:id', component: User},
]
正则表达式路由
惯例参数只匹配 url
片段之间的字符,用/
分隔。如果咱们想匹配任意门路,咱们能够应用自定义的 门路参数
正则表达式,在 门路参数
前面的括号中退出 正则表达式
const routes = [
// 将匹配所有内容并将其放在 `$route.params.pathMatch` 下
{path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
]
const routes = [
// 将匹配以 `/user-` 结尾的所有内容,并将其放在 `$route.params.afterUser` 下
{path: '/user-:afterUser(.*)', component: UserGeneric }
]
const routes = [
// /:orderId -> 仅匹配数字
{path: '/:orderId(\\d+)' },
// /:productName -> 匹配其余任何内容
{path: '/:productName'},
]
const routes = [
// /:chapters -> 匹配 /one, /one/two, /one/two/three, 等
{path: '/:chapters+'},
// /:chapters -> 匹配 /, /one, /one/two, /one/two/three, 等
{path: '/:chapters*'},
]
具体例子剖析
const routes = [{path: '/', component: Home},
{path: '/child', component: Child1}, // 动态路由
{path: '/child/:id', component: Child2}, // 动静路由
{path: '/child/:id?', component: Child3}, // 动静路由(可选){path: '/:child1(\\d+)', component: Child4}, // 动静路由(限度数字)TokenizerState.ParamRegExp
{path: '/:child2+', component: Child5}, // 动静路由(可反复)]
具体例子应用 Vue Router
官网文档提供的在线测试链接 Path Ranker 进行测试,后果为下图所示
上面将剖析如何失去下面图中的分数以及对应的路由匹配逻辑
tokenizePath()
该办法就是间接依据不同的
const char=path[i++]
进行不同状态的赋值,而后造成对应的数据
上面就是Vue Router
源码中标记的几种状态
const enum TokenizerState {
Static, // 静态数据,比方 /child 这种类型中的 "child"
Param, // 动静路由以及常见的正则表达式,比方 "/:child1(\\d+)"
ParamRegExp, // custom re for a param
ParamRegExpEnd, // check if there is any ? + *
EscapeNext,
}
间接应用
debugger
断点调试该办法即可清晰明确整个流程
取一个比较复杂的正则表达式例子为:path: "/c\\hil\\d3/new:c\\hild1(\\d+)?"
,这个例子中有静态数据 /child3
,有动态数据:child1
,也有一些正则表达式\d+
以及 ?
,还有一些两头乱入的\\
tokenizePath()
外围办法为上面代码块,依据判断目前 item
是"/"
,还是":"
,还是"("
,而后进行不同的状态的赋值
function tokenizePath(path) {while (i < path.length) {char = path[i++];
if (char === '\\' && state !== 2 /* TokenizerState.ParamRegExp */) {
previousState = state;
state = 4 /* TokenizerState.EscapeNext */;
continue;
}
switch (state) {
case 0 /* TokenizerState.Static */:
break;
case 4 /* TokenizerState.EscapeNext */:
break;
case 1 /* TokenizerState.Param */:
break;
case 2 /* TokenizerState.ParamRegExp */:
break;
case 3 /* TokenizerState.ParamRegExpEnd */:
break;
default:
crash('Unknown state');
break;
}
}
}
TokenizerState.Static
: 遇到"/"
,进入该状态,解决动态门路,比方"/child3"
TokenizerState.Param
: 遇到":"
,进入该状态,解决动静门路,比方":child1"
TokenizerState.ParamRegExp
: 遇到"("
,进入该状态,开始解决正则表达式TokenizerState.ParamRegExpEnd
: 遇到")"
,进入该状态,完结解决正则表达式,而后从新回到TokenizerState.Static
状态TokenizerState.EscapeNext
: 在不是TokenizerState.ParamRegExp
(解决正则表达式) 的状态下遇到了"\\"
,间接跳过"\\"
,比方/c\\hil\\d3
->/child3
最终造成 segment
数组为:
segment.push({
type: 1 /* TokenType.Param */,
value: buffer, // path 的局部内容
regexp: customRe, // 正则表达式的内容
repeatable: char === '*' || char === '+', // 是否容许反复,代表 "+" 或者 "*" 这些示意反复的正则表达式
optional: char === '*' || char === '?', // 是否可选,代表 "?" 或者 "*" 这些示意可选的正则表达式
});
segment
代表每一个片段的值,在 path: "/c\\hil\\d3/new:c\\hild1(\\d+)?"
中,能够分为 2 个片段
c\\hil\\d3
new:c\\hild1(\\d+)?
最终要依据 segment
再造成一个总体的数组tokens
function finalizeSegment() {if (segment)
tokens.push(segment);
segment = [];}
path: "/c\\hil\\d3/new:c\\hild1(\\d+)?"
造成 tokens
如下所示
c\\hil\\d3
: 动态门路new:c\\hild1(\\d+)?
: 动态门路 + 动静门路(蕴含正则表达式 + 可选)
[
[
{
"type": 0,
"value": "child3"
}
],
[
{
"type": 0,
"value": "new"
},
{
"type": 1,
"value": "child1",
"regexp": "\\d+",
"repeatable": false,
"optional": true
}
]
]
tokensToParser()
遍历
tokenizePath()
拿到的tokens
进行权重得分、正则表达式的计算
function tokensToParser(segments, extraOptions) {
//...
for (const segment of segments) {//...}
return {
re,
score,
keys,
parse,
stringify,
};
}
上面剖析正则表达式
re
和权重分数score
的构建流程
正则表达式 re
的构建
TokenType.Static
: 动态门路,比方path
="/child"
,应用原来的门路值,会进行一些特殊字符的转译,最终造成re
=/^\/child$/i
const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g;
if (!tokenIndex)
pattern += '/';
pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
TokenType.Param
: 动静匹配门路,蕴含各种正则表达式
当动静匹配门路不蕴含正则表达式时,间接应用 BASE_PARAM_PATTERN = '[^/]+?'
作为最终 re
的值
比方path
="/child/:id"
,最终造成re
=/^\/child\/([^/]+?)$/i
因为
path
蕴含动态门路 + 动静门路,因而re
= 动态门路 + 动静门路 =^\/child\/
+([^/]+?)
+i
当动静匹配门路蕴含正则表达式时,间接门路中的正则表达式,即 segment.regexp
去构建最终的 re
的值
比方path
="/:child1(\\d+)?"
,失去的segment.regexp
="\\d+"
,最终造成re
=/^(?:\/(\d+))?$/i
const BASE_PARAM_PATTERN = '[^/]+?';
const re = regexp ? regexp : BASE_PARAM_PATTERN;
let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`;
// prepend the slash if we are starting a new segment
if (!tokenIndex)
subPattern =
// avoid an optional / if there are more segments e.g. /:p?-static
// or /:p?-:p2
optional && segment.length < 2
? `(?:/${subPattern})`
: '/' + subPattern;
if (optional)
subPattern += '?';
如果 path
中蕴含可反复的正则表达式,比方path
="/:child1(\\d+)+"
,失去的segment.regexp
="\\d+"
,因为最初有个+
,因而repeatable
=true
,最终造成re
=/^\/((?:\d+)(?:\/(?:\d+))*)$/i
权重分数 score
的构建
const enum PathScore {
_multiplier = 10,
Root = 9 * _multiplier, // just /
Segment = 4 * _multiplier, // /a-segment
SubSegment = 3 * _multiplier, // /multiple-:things-in-one-:segment
Static = 4 * _multiplier, // /static
Dynamic = 2 * _multiplier, // /:someId
BonusCustomRegExp = 1 * _multiplier, // /:someId(\\d+)
BonusWildcard = -4 * _multiplier - BonusCustomRegExp, // /:namedWildcard(.*) we remove the bonus added by the custom regexp
BonusRepeatable = -2 * _multiplier, // /:w+ or /:w*
BonusOptional = -0.8 * _multiplier, // /:w? or /:w*
// these two have to be under 0.1 so a strict /:page is still lower than /:a-:b
BonusStrict = 0.07 * _multiplier, // when options strict: true is passed, as the regex omits \/?
BonusCaseSensitive = 0.025 * _multiplier, // when options strict: true is passed, as the regex omits \/?
}
原始分数为PathScore.Segment
=40
- 如果对大小写敏感(
options.sensitive
=true
),则减少分数PathScore.BonusCaseSensitive
=0.25
,即便40
+0.25
=40.25
- 如果最初一个
segment
设置了options.strict
=true
,额定失去PathScore.BonusStrict
的0.7
下面是初始化的分数,上面将针对各种状态进行剖析
TokenType.Static
动态门路会额定失去 PathScore.Static
的+40 分
,如/child
的分数是40+40+0.7=80.7
TokenType.Param
动静匹配门路会额定失去 PathScore.Dynamic
的+20 分
,如/:child
的分数是40+20+0.7=60.7
- 如果蕴含正则表达式,正则表达式会额定失去
PathScore.BonusCustomRegExp
的+10 分
,如/:child1(\\d+)
的分数是60+10+0.7=70.7
- 如果蕴含可选
optional
符号,额定失去PathScore.BonusOptional
的-10 分
,如/:child1?
的分数是60-10+0.7=60.7
- 如果蕴含匹配所有字符
.*
符号,额定失去PathScore.BonusWildcard
的-50 分
,如/:child1(.*)
的分数是60+10-50+0.7=20.7
- 如果蕴含反复
repeatable
符号,额定失去PathScore.BonusRepeatable
的-20 分
,如/:child1+
的分数是60-20+0.7=40.7
/:child1+
的正则表达式为空,即TokenizePath [[{"type":1,"value":"child1","regexp":"","repeatable":true,"optional":false}]]
/:child1?
同理,正则表达式也为空,因而没有PathScore.BonusCustomRegExp
的+10 分
如果存在多段segment
/child/:child1
,分数为child
+:child1
=[[80], [60.7]]
/child/pre-:child1
,分数为child
+pre-
+:child1
=[[80],[80,60.7]]
小结
通过 tokenizePath()
拿到 routes
解析后的路由数据
{
"type": 1,
"value": "child1",
"regexp": "\\d+",
"repeatable": false,
"optional": true
}
依据 tokenizePath()
解析后的数据,进行路由权重的计算,通过 tokensToParser()
拿到权重以及拼接对应的正则表达式,造成一个更加欠缺的路由数据
parse()
和stringify()
会在应用RouterMatcher
匹配门路时用到
{
re, // 正则表达式
score, // 权重分数
keys, //{name,repeatable,optional}
parse, // 动静路由匹配,匹配出动静路由对应那个动态门路的办法
stringify // 格式化 params 失去 path 的办法
}
最终拼接所有数据造成 matcher
,createRouteRecordMatcher()
返回数据如下所示
function createRouteRecordMatcher(record, parent, options) {const parser = tokensToParser(tokenizePath(record.path), options);
const matcher = assign(parser, {
record,
parent,
// these needs to be populated by the parent
children: [],
alias: [],});
return matcher;
}
步骤 2: 创立 matcher 对象和它的 children-matcher 之间的关系
遍历目前 route
的children
,将以后 route
创立的 matcher
作为 parent
传入 addRoute()
,递归调用addRoute()
创立新的matcher
if (mainNormalizedRecord.children) {
const children = mainNormalizedRecord.children;
for (let i = 0; i < children.length; i++) {addRoute(children[i], matcher, originalRecord && originalRecord.children[i]);
}
}
步骤 3: insertMatcher()将创立好的 matcher 插入到 matchers 数组和 matcherMap 对象中
- 应用
comparePathParserScore()
对matchers
进行排序,每次从头开始遍历let i=0
,如果目前matchers[i]
权重较大,则i++
,否则间接调用matchers.splice(i, 0, matcher)
插入matcher
进去 matcherMap
以路由对象record
的名称作为key
,matcher
作为value
在应用
matchers
查找门路时,会应用matcher=matchers.find(m => m.re.test(path))
,权重越大的元素放在越后面,会最先被找到,因而查找门路时会先找到权重最大的那个matcher
function insertMatcher(matcher) {
let i = 0;
while (i < matchers.length &&
comparePathParserScore(matcher, matchers[i]) >= 0 &&
// Adding children with empty path should still appear before the parent
// https://github.com/vuejs/router/issues/1124
(matcher.record.path !== matchers[i].record.path ||
!isRecordChildOf(matcher, matchers[i])))
i++;
matchers.splice(i, 0, matcher);
// only add the original record to the name map
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher);
}
而权重的比拟也比较简单,不是依照总分计算权重,而是依据数组中的每一项从头到尾进行比拟,如果a
<b
,则返回大于 0 的数字
-
如果雷同
index
比拟能出后果,间接返回后果- 长度雷同时,间接比拟雷同
index
,间接返回差值 - 如果
a
的长度只有 1,并且a[0]
=PathScore.Static
+PathScore.Segment
,示意a
是一个动态门路,并且只有一个元素,那么a
的权重较大,返回-1
,反之返回1
- 如果
b
的长度只有 1,并且b[0]
=PathScore.Static
+PathScore.Segment
,示意b
是一个动态门路,并且只有一个元素,那么b
的权重较大,返回-1
,反之返回1
- 如果
a
和b
的长度雷同,每一个值比拟都等于 0,证实a
===b
,返回0
,持续上面流程
- 长度雷同时,间接比拟雷同
- 如果雷同
index
的后果都一样,并且长度相差 1,则比拟最初一位是否为正数,哪个为正数,就比拟小 - 如果雷同
index
的后果都一样,并且长度不止 1,则比拟谁的长度大,谁大权重就大
function comparePathParserScore(a, b) {
let i = 0;
const aScore = a.score;
const bScore = b.score;
while (i < aScore.length && i < bScore.length) {const comp = compareScoreArray(aScore[i], bScore[i]);
// do not return if both are equal
if (comp)
return comp;
i++;
}
if (Math.abs(bScore.length - aScore.length) === 1) {if (isLastScoreNegative(aScore))
return 1;
if (isLastScoreNegative(bScore))
return -1;
}
return bScore.length - aScore.length;
}
function compareScoreArray(a, b) {
let i = 0;
while (i < a.length && i < b.length) {const diff = b[i] - a[i];
// only keep going if diff === 0
if (diff)
return diff;
i++;
}
if (a.length < b.length) {return a.length === 1 && a[0] === 40 /* PathScore.Static */ + 40 /* PathScore.Segment */
? -1
: 1;
}else if (a.length > b.length) {return b.length === 1 && b[0] === 40 /* PathScore.Static */ + 40 /* PathScore.Segment */
? 1
: -1;
}
return 0;
}
应用 RouterMatcher 匹配路由
在下面
createRouterMatcher()
中,咱们晓得了怎么初始化匹配路由,在这个大节中,咱们将剖析如何利用matcher
进行路由的匹配
// 只保留 matcher 匹配的代码逻辑
function createRouter(options) {const matcher = createRouterMatcher(options.routes, options);
//... 多个办法,根本都是为上面初始化 router 的属性
// 比方 addRoute()、removeRoute()、hasRoute()
const router = {
currentRoute,
listening: true,
addRoute,
removeRoute,
hasRoute,
getRoutes,
resolve,
options,
push,
replace,
go,
back: () => go(-1),
forward: () => go(1),
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add,
afterEach: afterGuards.add,
onError: errorHandlers.add,
isReady,
install(app) {//... 省略,内部 app.use()时调用
},
};
return router;
}
push(routerHistory.location)整体概述
这个
push()
办法是在createRouter()
创立的部分办法,不是下面useHistoryStateNavigation()
创立的router.push()
办法
当内部调用 router.push()
跳转到新的路由时,理论调用的是 pushWithRedirect()
,而在pushWithRedirect()
中第一行代码,应用 resolve(to)
进行以后要跳转的路由的计算
function push(to) {return pushWithRedirect(to);
}
function pushWithRedirect(to, redirectedFrom) {const targetLocation = (pendingLocation = resolve(to));
const from = currentRoute.value;
const data = to.state;
const force = to.force;
// to could be a string where `replace` is a function
const replace = to.replace === true;
const shouldRedirect = handleRedirectRecord(targetLocation);
if (shouldRedirect) {
//... 解决重定向的逻辑
return pushWithRedirect(...)
}
// if it was a redirect we already called `pushWithRedirect` above
const toLocation = targetLocation;
toLocation.redirectedFrom = redirectedFrom;
//... 解决 SameRouteLocation 的状况
// ... 去除 failure 的解决,默认都胜利
return navigate(toLocation, from)
.then((failure) => {failure = finalizeNavigation(toLocation, from, true, replace, data);
triggerAfterEach(toLocation, from, failure);
return failure;
});
}
resolve(rawLocation, currentLocation)
解析跳转门路
传递的 rawLocation
次要分为 2 种状况进行剖析
rawLocation
为字符串,比方rawLocation
="./child1"
,而后调用parseURL()
->matcher.resolve()
->routerHistory.createHref()
rawLocation
为Object
数据,解决它携带的path
、params
,而后调用matcher.resolve()
->routerHistory.createHref()
上面将先对
parseURL()
、matcher.resolve()
、routerHistory.createHref()
开展剖析,而后再剖析resolve()
的整体流程
parseURL()
解析门路拿到fullPath
、path
、query
、hash
传入参数
parseQuery()
是一个办法,能够解析链接中?A=xx&B=xx
的局部,返回一个key-value
数据,开发者可在初始化传入自定义的解析办法location
代表行将要跳转的路由门路currentLocation
代表目前的路由门路
在这个办法中,咱们通过 "#"
以及 "?"
拿到对应的字符串片段,塞入到 hash
和query
字段中,将删除掉 "#"
和"?"
的局部塞入到 path
字段中
function parseURL(parseQuery, location, currentLocation = '/') {let path, query = {}, searchString = '', hash ='';
// Could use URL and URLSearchParams but IE 11 doesn't support it
// TODO: move to new URL()
const hashPos = location.indexOf('#');
let searchPos = location.indexOf('?');
// the hash appears before the search, so it's not part of the search string
if (hashPos < searchPos && hashPos >= 0) {searchPos = -1;}
if (searchPos > -1) {path = location.slice(0, searchPos);
searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length);
query = parseQuery(searchString);
}
if (hashPos > -1) {path = path || location.slice(0, hashPos);
// keep the # character
hash = location.slice(hashPos, location.length);
}
// no search and no query
path = resolveRelativePath(path != null ? path : location, currentLocation);
// empty path means a relative query or hash `?foo=f`, `#thing`
return {fullPath: path + (searchString && '?') + searchString + hash,
path,
query,
hash,
};
}
而后触发 resolveRelativePath()
办法,代码逻辑也非常简单,就是判断 toPosition
是否是相对路径
- 如果是
"/"
结尾,不解决间接返回 - 如果是
"."
结尾,则代表跟fromSegments
同级目录,不进行position
的解决 - 如果是
".."
结尾,代表是fromSegments
的上一级目录,进行position--
最终拼接绝对路径进行返回
function resolveRelativePath(to, from) {if (to.startsWith('/'))
return to;
if (!from.startsWith('/')) {warn(`Cannot resolve a relative location without an absolute path. Trying to resolve "${to}" from "${from}". It should look like "/${from}".`);
return to;
}
if (!to)
return from;
const fromSegments = from.split('/');
const toSegments = to.split('/');
let position = fromSegments.length - 1;
let toPosition;
let segment;
for (toPosition = 0; toPosition < toSegments.length; toPosition++) {segment = toSegments[toPosition];
// we stay on the same position
if (segment === '.')
continue;
// go up in the from array
if (segment === '..') {
// we can't go below zero, but we still need to increment toPosition
if (position > 1)
position--;
// continue
}
// we reached a non-relative path, we stop here
else
break;
}
return (fromSegments.slice(0, position).join('/') +
'/' +
toSegments
// ensure we use at least the last element in the toSegments
.slice(toPosition - (toPosition === toSegments.length ? 1 : 0))
.join('/'));
}
最终返回绝对路径下的全门路 fullPath
(包含query
和hash
)以及对应的path
、query
、hash
function parseURL(parseQuery, location, currentLocation = '/') {
//...
return {fullPath: path + (searchString && '?') + searchString + hash,
path,
query,
hash,
};
}
matcher.resolve(location, currentLocation)
拿到门路上所有对应的 matcher
数组
留神,
matcher
是createRouterMatcher()
返回的对象,具备多个办法属性createRouterMatcher()
外部会进行addRoute()
创立对应的matcherMap
对象,提供给matcher.resolve()
应用
// globalOptions={history, routes}
function createRouterMatcher(routes, globalOptions) {
//...
function resolve(location, currentLocation) {...}
return {addRoute, resolve, removeRoute, getRoutes, getRecordMatcher};
}
matcher.resolve()
办法,次要分为 3 个条件进行查找
function resolve(location, currentLocation) {
let matcher;
let params = {};
let path;
let name;
if ('name' in location && location.name) {//... 传递 name 查找路由} else if ('path' in location) {//... 传递 path 查找路由} else {//... 其它}
const matched = [];
let parentMatcher = matcher;
while (parentMatcher) {matched.unshift(parentMatcher.record);
parentMatcher = parentMatcher.parent;
}
return {
name,
path,
params,
matched,
meta: mergeMetaFields(matched),
};
}
function insertMatcher(matcher) {
//...
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher);
}
上面将针对下面这个代码进行合成
传递 name
查找对应的路由
router.push({name: 'user', params: { username: 'eduardo'} })
间接依据 name
找到对应的 matcher
,而后进行params
的合并,这里间接利用 location.params
笼罩 currentLocation.params
反复的key
paramsFromLocation()
: 筛选第一个传入Object
,筛选出key
存在于第二个参数,即第二个参数存在的key
,能力保留下来第一个参数对应的key-value
matcher = matcherMap.get(location.name);
name = matcher.record.name;
params = assign(
paramsFromLocation(currentLocation.params,
matcher.keys.filter(k => !k.optional).map(k => k.name)),
location.params &&
paramsFromLocation(location.params, matcher.keys.map(k => k.name)));
path = matcher.stringify(params);
而后依据 params
进行对应门路的拼接,如果是动态门路,间接拼接动态门路的值,如果是动静门路,则拼接传递的 params
值
如果动静匹配路由没有传递对应的参数,并且是不可选
optional=false
,则会报错
function stringify(params) {
let path = '';
let avoidDuplicatedSlash = false;
for (const segment of segments) {if (!avoidDuplicatedSlash || !path.endsWith('/'))
path += '/';
avoidDuplicatedSlash = false;
for (const token of segment) {if (token.type === 0 /* TokenType.Static */) {path += token.value;} else if (token.type === 1 /* TokenType.Param */) {const { value, repeatable, optional} = token;
const param = value in params ? params[value] : '';
const text = isArray(param)
? param.join('/')
: param;
if (!text) {if (optional) {// 可选条件下,如果 path.endsWith('/'),则去掉最初面的 '/'
} else {throw new Error(`Missing required param "${value}"`); }
}
path += text;
}
}
}
return path || '/';
}
传递 path
查找对应的路由
通过正则表达式匹配门路找到对应的 matcher
,通过matcher.parse()
解析门路,拿到路由动静匹配的字符串
路由配置为: /:child1
->router.push("/test")
->params: ["child1": ["test"]]
path = location.path;
matcher = matchers.find(m => m.re.test(path));
if (matcher) {//params: ["child1": ["test"]]
params = matcher.parse(path);
name = matcher.record.name;
}
没有传递 name
也没有传递path
获取以后路由的 matcher
,而后合并传递的params
,应用params
造成新的门路
matcher = currentLocation.name
? matcherMap.get(currentLocation.name)
: matchers.find(m => m.re.test(currentLocation.path));
name = matcher.record.name;
params = assign({}, currentLocation.params, location.params);
path = matcher.stringify(params);
寻找到路由后,遍历这条路由所有segment
,拿到所有的matcher
,波及门路上的多个Component
咱们加载子
Component
的同时也会加载父Component
const matched = [];
let parentMatcher = matcher;
while (parentMatcher) {matched.unshift(parentMatcher.record);
parentMatcher = parentMatcher.parent;
}
return {
name,
path,
params,
matched,
meta: mergeMetaFields(matched),
};
routerHistory.createHref()
将base
(除去域名后的残余门路),应用正则表达式替换为"#"
,比方"/router/vue-router-path-ranker.html?a=1#test#xixi"
->"#test#xixi"
此时 location
为目前的门路,比方/one/two/three
,那么组合起来就是"#test#xixi/one/two/three"
一般来说应该是
"#/one/two/three"
,不会呈现两个#
const href = routerHistory.createHref(fullPath);
const BEFORE_HASH_RE = /^[^#]+#/;
function createHref(base, location) {return base.replace(BEFORE_HASH_RE, '#') + location;
}
resolve()
详细分析
parseURL()
: 解析失去 fullPath
、path
、query
、hash
matcher.resolve()
: 解析失去matchedRouted
,初始化传入routes
失去的数组对象,蕴含该路由所映射的组件,对应的参数以及一系列的路由守卫,具体的数据结构如上面代码块所示routerHistory.createHref()
: 解析失去href
// matcher.resolve()
function resolve(location, currentLocation) {
return {
name,
path,
params,
matched,
meta: mergeMetaFields(matched),
};
}
// matched: RouteRecordNormalized[]
interface RouteRecordNormalized {path: _RouteRecordBase['path']
redirect: _RouteRecordBase['redirect'] | undefined
name: _RouteRecordBase['name']
components: RouteRecordMultipleViews['components'] | null | undefined
children: RouteRecordRaw[]
meta: Exclude<_RouteRecordBase['meta'], void>
props: Record<string, _RouteRecordProps>
beforeEnter: _RouteRecordBase['beforeEnter']
leaveGuards: Set<NavigationGuard>
updateGuards: Set<NavigationGuard>
enterCallbacks: Record<string, NavigationGuardNextCallback[]>
instances: Record<string, ComponentPublicInstance | undefined | null>
aliasOf: RouteRecordNormalized | undefined
}
从下面的剖析,咱们能够晓得 parseURL()
、matcher.resolve()
、routerHistory.createHref()
的返回值,当初咱们能够对 resolve()
办法进行具体的剖析
function resolve(rawLocation, currentLocation) {currentLocation = assign({}, currentLocation || currentRoute.value);
if (typeof rawLocation === 'string') {const locationNormalized = parseURL(parseQuery$1, rawLocation, currentLocation.path);
const matchedRoute = matcher.resolve({path: locationNormalized.path}, currentLocation);
const href = routerHistory.createHref(locationNormalized.fullPath);
return assign(locationNormalized, matchedRoute, {params: decodeParams(matchedRoute.params),
hash: decode(locationNormalized.hash),
redirectedFrom: undefined,
href,
});
} else {
let matcherLocation;
if ('path' in rawLocation) {matcherLocation = assign({}, rawLocation, {path: parseURL(parseQuery$1, rawLocation.path, currentLocation.path).path,
});
} else {const targetParams = assign({}, rawLocation.params);
for (const key in targetParams) {if (targetParams[key] == null) {delete targetParams[key];
}
}
matcherLocation = assign({}, rawLocation, {params: encodeParams(rawLocation.params),
});
currentLocation.params = encodeParams(currentLocation.params);
}
const matchedRoute = matcher.resolve(matcherLocation, currentLocation);
const hash = rawLocation.hash || '';
matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params));
const fullPath = stringifyURL(stringifyQuery$1, assign({}, rawLocation, {hash: encodeHash(hash),
path: matchedRoute.path,
}));
const href = routerHistory.createHref(fullPath);
return assign({
fullPath,
hash,
query: stringifyQuery$1 === stringifyQuery
? normalizeQuery(rawLocation.query)
: (rawLocation.query || {}),
}, matchedRoute, {
redirectedFrom: undefined,
href,
});
}
}
parseURL()
: 解析失去fullPath
、path
、query
、hash
matcher.resolve()
: 解析失去matchedRouted
,初始化传入routes
失去的Array
,蕴含该路由所映射的组件,对应的参数以及一系列的路由守卫
routerHistory.createHref()
: 解析失去href
typeof rawLocation === 'string'
时,字符串代表路由 path
,通过matcher.resolve({path: xxx})
获取对应的路由对象
const locationNormalized = parseURL(parseQuery$1, rawLocation, currentLocation.path);
const matchedRoute = matcher.resolve({path: locationNormalized.path}, currentLocation);
const href = routerHistory.createHref(locationNormalized.fullPath);
// locationNormalized is always a new object
return assign(locationNormalized, matchedRoute, {params: decodeParams(matchedRoute.params),
hash: decode(locationNormalized.hash),
redirectedFrom: undefined,
href,
});
typeof rawLocation !== 'string'
时,也是同样的逻辑
- 如果有
path
属性,则跟下面的逻辑统一,通过matcher.resolve({path: xxx})
获取对应的路由对象 - 如果没有
path
属性,解决params
数据(从path
中提取的已解码参数字典),而后再通过matcher.resolve({path: xxx})
获取对应的路由对象
fullPath
:path + (searchString && '?') + searchString + hash
let matcherLocation;
//======== 第 1 局部:整顿参数 ========
if ('path' in rawLocation) {matcherLocation = assign({}, rawLocation, {path: parseURL(parseQuery$1, rawLocation.path, currentLocation.path).path,
});
} else {
//...
matcherLocation = assign({}, rawLocation, {params: encodeParams(rawLocation.params),
});
currentLocation.params = encodeParams(currentLocation.params);
}
//======== 第 2 局部:matcher.resolve ========
const matchedRoute = matcher.resolve(matcherLocation, currentLocation);
const hash = rawLocation.hash || '';
matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params));
const fullPath = stringifyURL(stringifyQuery$1, assign({}, rawLocation, {hash: encodeHash(hash),
path: matchedRoute.path,
}));
//======== 第 3 局部:routerHistory.createHref ========
const href = routerHistory.createHref(fullPath);
// ======== 第 4 局部:返回对象数据 ========
return assign({
fullPath,
hash,
query: stringifyQuery$1 === stringifyQuery
? normalizeQuery(rawLocation.query)
: (rawLocation.query || {}),
}, matchedRoute, {
redirectedFrom: undefined,
href,
});
resolve()
最终返回值对象也就是下文中targetLocation
的值和toLocation
的值
通过解说下面的一系列办法,当初咱们能够开始解析
push()
->pushWithRedirect()
pushWithRedirect()
真正 push 逻辑
咱们通过 resolve(to)
拿到了目前匹配的门路对象,而后解决重定向的逻辑,而后雷同 Route
对象逻辑,而后触发 navigate()
办法
function pushWithRedirect(to, redirectedFrom) {const targetLocation = (pendingLocation = resolve(to));
const from = currentRoute.value;
const data = to.state;
const force = to.force;
// to could be a string where `replace` is a function
const replace = to.replace === true;
const shouldRedirect = handleRedirectRecord(targetLocation);
if (shouldRedirect) {
//... 解决重定向的逻辑
return pushWithRedirect(...)
}
// if it was a redirect we already called `pushWithRedirect` above
const toLocation = targetLocation;
toLocation.redirectedFrom = redirectedFrom;
//... 解决 SameRouteLocation 的状况
// ... 去除 failure 的解决,默认都胜利
return navigate(toLocation, from)
.then((failure) => {failure = finalizeNavigation(toLocation, from, true, replace, data);
triggerAfterEach(toLocation, from, failure);
return failure;
});
}
navigate(toLocation, from)
function navigate(to, from) {
let guards;
// 将旧的路由的 beforerRouteLeave 守卫函数放入 guards
return (runGuardQueue(guards)// 先执行 beforerRouteLeave 守卫函数
.then(() => {// 解决全局的 beforeEach 守卫函数})
.then(() => {// 解决该路由的 beforeRouteUpdate 守卫函数})
.then(() => {// 解决该路由的 beforeEnter 守卫函数}).then(() => {// 解决该路由的 beforeRouteEnter 守卫函数}).then(() => {// 解决全局的 beforeResolve 守卫函数})
// catch any navigation canceled
.catch(err => isNavigationFailure(err, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)
? err
: Promise.reject(err)));
}
function runGuardQueue(guards) {return guards.reduce((promise, guard) => promise.then(() => guard()), Promise.resolve());
}
路由切换失败状况剖析
除了执行一系列的导航守卫须要关注外,咱们还须要关注下 error
产生时的解决状况,次要的谬误状况分为
export const enum ErrorTypes {
// they must be literals to be used as values, so we can't write
// 1 << 2
MATCHER_NOT_FOUND = 1,
NAVIGATION_GUARD_REDIRECT = 2,
NAVIGATION_ABORTED = 4,
NAVIGATION_CANCELLED = 8,
NAVIGATION_DUPLICATED = 16,
}
ErrorTypes.MATCHER_NOT_FOUND
当应用 matcher.solve()
寻找对应的 matched
数据时,如果传入的参数是路由的 name
,然而咱们却无奈依据name
找到对应的 matcher
时,咱们返回 ErrorTypes.MATCHER_NOT_FOUND
的谬误
因为对于一个路由,
name
是惟一的标识,如果传入name
,会依据matcherMap
去找对应存储过的matcher
function createRouterMatcher(routes, globalOptions) {function resolve(location, currentLocation) {
let matcher;
let params = {};
let path;
let name;
if ('name' in location && location.name) {matcher = matcherMap.get(location.name);
if (!matcher)
throw createRouterError(1 /* ErrorTypes.MATCHER_NOT_FOUND */, {location,});
//...
}
return {
name,
path,
params,
matched,
meta: mergeMetaFields(matched),
};
}
}
ErrorTypes.NAVIGATION_ABORTED
如上面代码块所示,咱们导航守卫会传递对应的 next()
提供给内部应用,比方开发者在内部的 next()
返回false
,则触发NAVIGATION_ABORTED
,示意勾销导航
ErrorTypes.NAVIGATION_GUARD_REDIRECT
如上面代码块所示,咱们导航守卫会传递对应的 next()
提供给内部应用,比方开发者在内部的 next()
返回 "/login"
或者{"name": "login"}
,则isRouteLocation
=true
,从而返回NAVIGATION_GUARD_REDIRECT
,示意导航重定向到其它路由
// guards.push(guardToPromiseFn(guard, to, from));
function guardToPromiseFn(guard, to, from, record, name) {return () => new Promise((resolve, reject) => {const next = (valid) => {if (valid === false) {
reject(createRouterError(4 /* ErrorTypes.NAVIGATION_ABORTED */, {
from,
to,
}));
} else if (valid instanceof Error) {reject(valid);
} else if (isRouteLocation(valid)) {
reject(createRouterError(2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */, {
from: to,
to: valid,
}));
} else {resolve();
}
};
}
}
ErrorTypes.NAVIGATION_CANCELLED
如上面代码块所示,当应用 push()
->pushWithRedirect()
->navigate()
->finalizeNavigation()
时,会进行 checkCanceledNavigation()
的检测,如果以后要跳转的路由跟 pushWithRedirect()
的路由不同时,阐明又有新的导航曾经产生,之前的导航勾销
ErrorTypes.NAVIGATION_DUPLICATED
如上面代码块所示,如果没有应用 force
,当检测到雷同路由时,会产生NAVIGATION_DUPLICATED
谬误,阻止持续调用navigate()
->finalizeNavigation()
isSameRouteLocation
:matched
、params
、query
、hash
都雷同
function pushWithRedirect(to, redirectedFrom) {const targetLocation = (pendingLocation = resolve(to));
if (!force && isSameRouteLocation(stringifyQuery$1, from, targetLocation)) {failure = createRouterError(16 /* ErrorTypes.NAVIGATION_DUPLICATED */, { to: toLocation, from});
}
if(failure) return Promise.resolve(failure);
return navigate(toLocation, from).then((failure) => {if (!failure) {failure = finalizeNavigation(toLocation, from, true, replace, data);
}
});
}
function finalizeNavigation(toLocation, from, isPush, replace, data) {
// a more recent navigation took place
const error = checkCanceledNavigation(toLocation, from);
if (error)
return error;
}
function checkCanceledNavigation(to, from) {if (pendingLocation !== to) {
return createRouterError(8 /* ErrorTypes.NAVIGATION_CANCELLED */, {
from,
to,
});
}
}
finalizeNavigation()
从上面代码块能够晓得,finalizeNavigation()
的步骤为:
- 触发
routerHistory.replace
/routerHistory.push
更新 - 更新
currentRoute.value
为目前的路由门路 - 检测是否是初始化阶段,如果是初始化阶段,则触发
setupListeners()
办法
function finalizeNavigation(toLocation, from, isPush, replace, data) {
//... 处理错误
if (isPush) {if (replace || isFirstNavigation)
routerHistory.replace(toLocation.fullPath, assign({scroll: isFirstNavigation && state && state.scroll,}, data));
else
routerHistory.push(toLocation.fullPath, data);
}
// accept current navigation
currentRoute.value = toLocation;
handleScroll(toLocation, from, isPush, isFirstNavigation);
markAsReady();}
function markAsReady(err) {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;
}
setupListeners()注册 pop 操作相干监听办法
初始化会进行一次
push()
操作,此时就是初始化阶段
!ready
,push()
->navigate()
->finalizeNavigation()
初始化阶段会进行 routerHistory.listen()
的办法注册
function finalizeNavigation(toLocation, from, isPush, replace, data) {markAsReady();
}
function markAsReady(err) {if (!ready) {
ready = !err;
setupListeners();}
return err;
}
function setupListeners() {
// avoid setting up listeners twice due to an invalid first navigation
if (removeHistoryListener)
return;
removeHistoryListener = routerHistory.listen((to, _from, info) => {if (!router.listening)
return;
// cannot be a redirect route because it was in history
const toLocation = resolve(to);
//... 解决重定向的逻辑
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)
.then((failure) => {
finalizeNavigation(
// after navigation, all matched components are resolved
toLocation, from, false);
triggerAfterEach(toLocation, from, failure);
})
.catch(noop);
});
}
那么初始化就注册的监听在什么时候会触发呢?这个监听又有什么作用呢?
在下面咱们说到 useHistoryListeners()
初始化的时候,咱们提供了 listen()
办法进行事件的注册,而后在 popStateHandler()
触发时,进行 listeners
注册办法的调用
因为咱们能够晓得,这个监听事件实质就是为了在用户进行浏览器后退按钮点击时,可能失常监听到路由变动并且主动实现 Component
切换
function useHistoryListeners(base, historyState, currentLocation, replace) {let listeners = [];
let teardowns = [];
// TODO: should it be a stack? a Dict. Check if the popstate listener
// can trigger twice
let pauseState = null;
const popStateHandler = ({state,}) => {...};
function pauseListeners() {...}
function listen(callback) {...}
function beforeUnloadListener() {...}
function destroy() {...}
// set up the listeners and prepare teardown callbacks
window.addEventListener('popstate', popStateHandler);
window.addEventListener('beforeunload', beforeUnloadListener);
return {
pauseListeners,
listen,
destroy,
};
}
const popStateHandler = ({state,}) => {
//...
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
});
});
};
app.use(router)应用 VueRouter
use(plugin, ...options) {if (plugin && isFunction(plugin.install)) {installedPlugins.add(plugin);
plugin.install(app, ...options);
}
else if (isFunction(plugin)) {installedPlugins.add(plugin);
plugin(app, ...options);
}
return app;
}
router.install(app)
从下面 Vue3
的源码能够晓得,最终会触发 Vue Router
的install()
办法
const START_LOCATION_NORMALIZED = {
path: '/',
name: undefined,
params: {},
query: {},
hash: '',
fullPath: '/',
matched: [],
meta: {},
redirectedFrom: undefined,
};
const currentRoute = vue.shallowRef(START_LOCATION_NORMALIZED);
const router = {
//....
addRoute,
removeRoute,
push,
replace,
beforeEach: beforeGuards.add,
isReady,
install(app) {//... 省略,内部 app.use()时调用
},
};
因为篇幅起因,接下来的剖析请看下一篇文章 Vue3 相干源码 -Vue Router 源码解析(二)
参考文章
- 7 张图,从零实现一个简易版 Vue-Router,太通俗易懂了!
- VueRouter4 路由权重
Vue 系列其它文章
- Vue2 源码 - 响应式原理浅析
- Vue2 源码 - 整体流程浅析
- Vue2 源码 - 双端比拟 diff 算法 patchVNode 流程浅析
- Vue3 源码 - 响应式零碎 - 依赖收集和派发更新流程浅析
- Vue3 源码 - 响应式零碎 -Object、Array 数据响应式总结
- Vue3 源码 - 响应式零碎 -Set、Map 数据响应式总结
- Vue3 源码 - 响应式零碎 -ref、shallow、readonly 相干浅析
- Vue3 源码 - 整体流程浅析
- Vue3 源码 -diff 算法 -patchKeyChildren 流程浅析