这是我参与更文挑战的第7天,活动详情查看更文挑战
今天继续分析vue-router4
源码剩下部分
入口文件和导出模块以及createWebHistory
中的base
属性和其他属性在之前已经分析了,接下来分析createWebHistory
中的location
- location
location
属性是在useHistoryStateNavigation()
方法中声明的,该方法中与location
相关的代码简化后如下所示
function useHistoryStateNavigation(base: string) {
const { location } = window
let currentLocation: ValueContainer<HistoryLocation> = {
value: createCurrentLocation(base, location),
}
return {
location: currentLocation,
}
}
function createCurrentLocation(
base: string,
location: Location
): HistoryLocation {
const { pathname, search, hash } = location
// 支持的hash如 #, /#, #/, #!, #!/, /#!/, 或者 /folder#end
const hashPos = base.indexOf('#')
// 如果是hash类型
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: string, base: string): string {
// 没有 base 或者 base 在开始位置找不到
if (!base || pathname.toLowerCase().indexOf(base.toLowerCase()))
return pathname
return pathname.slice(base.length) || '/'
}
复制代码
首先声明内部属性currentLocation
,是只有一个value
属性的对象,value
的值通过createCurrentLocation(base, location)
方法获取,该方法的作用是将当前url格式化为标准的path + search + hash
的字符串
- state
state
属性也是在useHistoryStateNavigation
方法中声明的,与state
相关的代码如下
function useHistoryStateNavigation(base: string) {
const { history } = window
let historyState: ValueContainer<StateEntry> = { value: history.state }
return {
state: historyState,
}
}
复制代码
state
属性是window.history.state
作为value
属性值的对象
在创建声明了state
属性之后,需要进行以下判断
如果该值是假值时,表示浏览器history
未进行任何操作,此时需要调用changeLocation
方法。changeLocation
方法十分重要,它是push
方法和replace
等路由跳转方法的实现基础。该方法包含三个参数:目标location
、目标state
对象、是否为替换当前location
if (!historyState.value) {
changeLocation(
currentLocation.value,
{
back: null,
current: currentLocation.value,
forward: null,
position: history.length - 1,
replaced: true,
scroll: null,
},
true
)
}
let createBaseLocation = () => location.protocol + '//' + location.host
function changeLocation(
to: HistoryLocation,
state: StateEntry,
replace: boolean
): void {
const hashIndex = base.indexOf('#')
const url =
hashIndex > -1
? (location.host && document.querySelector('base')
// base + currentLocation.value
? base
// #xxx + currentLocation.value
: base.slice(hashIndex)) + to
// `url`为:`协议://主机地址 + base + currentLocation.value`;
: createBaseLocation() + base + to
try {
// 尝试使用history api更改window.history
history[replace ? 'replaceState' : 'pushState'](state, '', url)
historyState.value = state
} catch (err) {
// 如果使用history api失败,则降级使用window.location方法代替
location[replace ? 'replace' : 'assign'](url)
}
}
复制代码
- replace 方法
replace
方法也是在useHistoryStateNavigation
中声明的,相关源代码如下,replace
方法接收to
和data
参数,首先通过参数构建state
对象,然后调用changeLocation
方法进行路由跳转
function useHistoryStateNavigation(base: string) {
const { history } = window
function replace(to: HistoryLocation, data?: HistoryState) {
// 整合state对象
const state: StateEntry = assign(
{},
history.state,
buildState(
historyState.value.back,
to,
historyState.value.forward,
true
),
data,
{ position: historyState.value.position }
)
// 调用路由跳转方法
changeLocation(to, state, true)
currentLocation.value = to
}
return {
replace,
}
}
const computeScrollPosition = () =>
({
left: window.pageXOffset,
top: window.pageYOffset,
} as _ScrollPositionNormalized)
function buildState(
back: HistoryLocation | null,
current: HistoryLocation,
forward: HistoryLocation | null,
replaced: boolean = false,
computeScroll: boolean = false
): StateEntry {
return {
back,
current,
forward,
replaced,
position: window.history.length,
scroll: computeScroll ? computeScrollPosition() : null,
}
}
复制代码
- push 方法
push
方法和replace
方法类型,相关源码如下
function useHistoryStateNavigation(base: string) {
const { history } = window
function push(to: HistoryLocation, data?: HistoryState) {
const currentState = assign(
{},
historyState.value,
history.state as Partial<StateEntry> | null,
{
forward: to,
scroll: computeScrollPosition(),
}
)
changeLocation(currentState.current, currentState, true)
const state: StateEntry = assign(
{},
buildState(currentLocation.value, to, null),
{ position: currentState.position + 1 },
data
)
changeLocation(to, state, false)
currentLocation.value = to
}
return {
push,
}
}
复制代码
观察push方法,发现方法内部调用了两次changeLocation方法,这是为什么呢?
主要的原因是需要保存当前location对应的页面滚动信息(scroll)以及将要跳转的页面信息(forward),此时调用changeLocation方法的replace参数为true,也就是更新当前location。
然后再创建要跳转的页面的state,这里和replace的不同的是,postion的值需要+1,此时调用changeLocation方法的replace参数为false
- go 方法
go方法是直接在createWebHistory方法中声明的,调用window.history.go(delta)进行路由跳转。另外支持triggerListeners参数,默认触发listeners中的回调函数执行,如果传入了false,则调用historyListeners.pauseListeners(),该方法修改了pauseState变量,该变量在后面的popState事件中会涉及到
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners()
history.go(delta)
}
复制代码
function pauseListeners() {
pauseState = currentLocation.value
}
复制代码
-listen 方法
listen方法由useHistoryListeners函数返回,在useHistoryListeners函数内部查看listen方法相关源码
function useHistoryListeners() {
let listeners: NavigationCallback[] = []
let teardowns: Array<() => void> = []
function listen(callback: NavigationCallback) {
listeners.push(callback)
const teardown = () => {
const index = listeners.indexOf(callback)
if (index > -1) listeners.splice(index, 1)
}
teardowns.push(teardown)
return teardown
}
return {
listen,
}
}
复制代码
listen方法的作用是将传入的回调函数添加到listeners数组中,并返回移除函数,同时将移除函数添加到teardowns数组中,方便进行批量移除
- createHref 方法
该方法在createWebHistory方法中bind公共的createHref方法,创建一个参数为base的createHref方法,调用routerHistory中的createHref方法时,返回一个由当前base组成href。
// src/history/html5.ts
function createWebHistory(base?: string): RouterHistory {
// 省略其他代码
const routerHistory: RouterHistory = assign(
{
// 省略其他代码
createHref: createHref.bind(null, base),
},
// 省略其他代码
)
// 省略其他代码
}
复制代码
// src/history/common.ts
// remove any character before the hash
const BEFORE_HASH_RE = /^[^#]+#/
export function createHref(base: string, location: HistoryLocation): string {
return base.replace(BEFORE_HASH_RE, '#') + location
}
复制代码
- destroy 方法
destroy方法是在useHistoryListeners函数中生成,简化后的函数源码为:
function useHistoryListeners() {
let teardowns: Array<() => void> = []
const popStateHandler: PopStateListener = ({
state,
}: {
state: StateEntry | null
}) => {
// ...
}
function beforeUnloadListener() {
// ...
}
function destroy() {
for (const teardown of teardowns) teardown()
teardowns = []
window.removeEventListener('popstate', popStateHandler)
window.removeEventListener('beforeunload', beforeUnloadListener)
}
window.addEventListener('popstate', popStateHandler)
window.addEventListener('beforeunload', beforeUnloadListener)
return {
destroy,
}
}
复制代码
destroy方法被调用时,首先遍历teardowns数组,移除所有listener,然后清空teardowns数组,接着解绑在window对象上的popstate和beforeunload事件
popstate事件在活动历史记录条目更改时被触发,此时调用popStateHandler函数,该函数源码如下
const popStateHandler: PopStateListener = ({
state,
}: {
state: StateEntry | null
}) => {
const to = createCurrentLocation(base, location)
const from: HistoryLocation = currentLocation.value
const fromState: StateEntry = historyState.value
let delta = 0
if (state) {
currentLocation.value = to
historyState.value = state
// ignore the popstate and reset the pauseState
if (pauseState && pauseState === from) {
pauseState = null
return
}
delta = fromState ? state.position - fromState.position : 0
} else {
replace(to)
}
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
})
})
}
复制代码
当用户操作浏览器导航按钮或者应用中调用了push/replace/go等方法时,都会触发该事件,调用此函数。除了更新某些对象值之外,这个函数的关键在于遍历listeners数组调用每一个注册的回调函数
在上面分析go方法的时候,提到了pauseListeners方法,当该方法被调用后,且等于上一次的location时,pauseState && pauseState === from条件成立,然后会执行pauseState = null;return语句,所以就不会继续执行后面遍历listeners的逻辑了
beforeunload事件当浏览器窗口关闭或者刷新时被触发,此时会调用beforeUnloadListener函数,该函数源码如下,主要作用是将当前滚动信息保存到当前历史记录实体中
function beforeUnloadListener() {
const { history } = window
if (!history.state) return
history.replaceState(
assign({}, history.state, { scroll: computeScrollPosition() }),
''
)
}
复制代码
createWebHistory和createWebHashHistory的实现原理分析完毕,先做一个小总结。从以上的分析可以得出,history和hash模式的差别在于base的处理,换句话可以说是浏览器url表现方式的差异,路由的跳转、事件的监听,都是基于history api,但是当使用history进行跳转出现错误时,VueRouter做了容错处理,降级使用location进行跳转
3、createMemoryHistory
有了以上两个函数的分析基础,再来看createMemoryHistory函数就容易理解过了,内存类型的RouterHistory,由于不是运行在浏览器端,所以不存在window.history对象,接下来直接看routerHistory对象中的属性和方法分别是如何实现的
- base
直接取的base参数的值,默认是’/’
- location
初始值为常量START,每次获取location时,都会通过queue[position]获取最新location的值
// src/history/common.ts
const START: HistoryLocation = ''
// src/history/memory.ts
const routerHistory: RouterHistory = {
// rewritten by Object.defineProperty
location: START,
}
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
// queue数组模拟的是浏览器历史记录
get: () => queue[position],
})
复制代码
- state
默认为一个空对象state: {},但是源代码中有TODO的注释,后续版本应该会有改动
// TODO: should be kept in queue
state: {},
复制代码
- push方法
简而言之,该方法实现的是往queue数组中追加一个location
let queue: HistoryLocation[] = [START]
function setLocation(location: HistoryLocation) {
position++
if (position === queue.length) {
// we are at the end, we can simply append a new entry
queue.push(location)
} else {
// we are in the middle, we remove everything from here in the queue
queue.splice(position)
queue.push(location)
}
}
const routerHistory: RouterHistory = {
push(to, data?: HistoryState) {
setLocation(to)
},
}
复制代码
- replace方法
与push方法类型,差别在于replace被调用时,会先把queue中最后一个location删掉,然后再调用setLocation方法,追加一个location
const routerHistory: RouterHistory = {
replace(to) {
// remove current entry and decrement position
queue.splice(position--, 1)
setLocation(to)
},
}
复制代码
- go方法
通过delta参数大小,然后更新position的值
这里并没有调用setLocation方法,也就是不需要更新queue,只是更新了position,后续获取location时是通过queue[position]获取的,所以可以拿到正确的location
复制代码
function triggerListeners(
to: HistoryLocation,
from: HistoryLocation,
{ direction, delta }: Pick<NavigationInformation, 'direction' | 'delta'>
): void {
const info: NavigationInformation = {
direction,
delta,
type: NavigationType.pop,
}
for (let callback of listeners) {
callback(to, from, info)
}
}
const routerHistory: RouterHistory = {
go(delta, shouldTrigger = true) {
const from = this.location
const direction: NavigationDirection =
// we are considering delta === 0 going forward, but in abstract mode
// using 0 for the delta doesn't make sense like it does in html5 where
// it reloads the page
delta < 0 ? NavigationDirection.back : NavigationDirection.forward
position = Math.max(0, Math.min(position + delta, queue.length - 1))
if (shouldTrigger) {
triggerListeners(this.location, from, {
direction,
delta,
})
}
},
}
复制代码
- listen方法
跟浏览器端要实现的功能基本一致,方法被调用时,将传进来的回调函数添加到listeners数组中,然后返回删除该回调的函数
let listeners: NavigationCallback[] = []
const routerHistory: RouterHistory = {
listen(callback) {
listeners.push(callback)
return () => {
const index = listeners.indexOf(callback)
if (index > -1) listeners.splice(index, 1)
}
},
}
复制代码
- createHref方法
实现方式和浏览器端一致
const routerHistory: RouterHistory = {
createHref: createHref.bind(null, base),
}
复制代码
- destroy方法
该方法被调用时,重置路由相关变量
const routerHistory: RouterHistory = {
destroy() {
listeners = []
queue = [START]
position = 0
},
}
复制代码
history属性的分析到此完结了,通过history三种创建方式的分析,我们了解到了浏览器端路由相关的属性、方法和事件,以及非浏览器端是如何模拟实现浏览器端的路由的,其中内存类型感觉还存在需要完善的地方,后续版本应该会有些许改动。
后面继续会分享routes 属性