vue-router4源码解析(2)

这是我参与更文挑战的第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方法接收todata参数,首先通过参数构建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 属性

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享