前言

【vue-router源码】系列文章将带你从0开始理解vue-router的具体实现。该系列文章源码参考vue-router v4.0.15
源码地址:https://github.com/vuejs/router
浏览该文章的前提是你最好理解vue-router的根本应用,如果你没有应用过的话,可通过vue-router官网学习下。

vue-router 4.x中创立router时,须要应用createWebHistorycreateWebHashHistorycreateMemoryHistory中的一个创立一个history,这篇文章将就这三个函数进行解析。

应用

import { createWebHistory, createRouter } from 'vue-router'const routerHistory = createWebHistory()const router = createRouter({    history: routerHistory,    routes: [ ... ]})

createWebHistory

createWebHistory源码所处地位:src/history/html5.ts
首先来看createWebHistory的参数,函数能够承受一个base字符串可选参数,该参数提供了一个根底门路。
createWebHistory中首先会调用normalizeBase函数对传入的base进行标准化。

base = normalizeBase(base)

来看下base标准化的过程:

export function normalizeBase(base?: string): string {  if (!base) {      // 浏览其环境下尝试获取base标签的href属性    if (isBrowser) {      const baseEl = document.querySelector('base')      base = (baseEl && baseEl.getAttribute('href')) || '/'      // 去除htttp(s)://xxx/,如https://example.com/folder/ --> /folder/      base = base.replace(/^\w+:\/\/[^\/]+/, '')    } else {      base = '/'    }  }  // 确保base的前导/  if (base[0] !== '/' && base[0] !== '#') base = '/' + base  return removeTrailingSlash(base)}

如果没有配置base的话,在浏览器环境下会尝试获取<base>标签的href属性作为base,如果没有<base>标签或<base>标签的href属性没有值,base/,而后又对base进行了reaplce(/^\w+:\/\/[^\/]+/, '')操作,该操作是去除basehttp(s)://xxx局部(如果basehttps://example.com/floder/childbase最终会变成/floder/child);非浏览器环境下,base间接取/。在最初会将base的开端/去除,而后返回base,这样做的目标是后续咱们能够通过base + fullPath的模式建设一个href

base标准化后,会申明一个historyNavigationhistoryListeners变量:

const historyNavigation = useHistoryStateNavigation(base)const historyListeners = useHistoryListeners(  base,  historyNavigation.state,  historyNavigation.location,  historyNavigation.replace)

这两个变量是什么呢?接下来看下useHistoryStateNavigation()useHistoryListeners()的实现。

先看useHistoryStateNavigation:

function useHistoryStateNavigation(base: string) {  // 获取window.history、window.location  const { history, location } = window  const currentLocation: ValueContainer<HistoryLocation> = {    value: createCurrentLocation(base, location),  }  const historyState: ValueContainer<StateEntry> = { value:  }  // 如果history.state是空的,构建一条新的历史记录  if (!historyState.value) {    changeLocation(      currentLocation.value,      {        back: null,        current: currentLocation.value,        forward: null,        position: history.length - 1,        replaced: true,        scroll: null,      },      true    )  }  // 批改历史记录  function changeLocation(    to: HistoryLocation,    state: StateEntry,    replace: boolean  ): void {    const hashIndex = base.indexOf('#')    // 获取url,作为history.replaceState/pushState的参数    // 如果hashIndex > -1,url = `{location.host && document.querySelector('base') ? base : base字符串#及前面字符}${to}`    // 否则 url = `${location.protocol}//${location.host}${base}${to}`    const url =      hashIndex > -1        ? (location.host && document.querySelector('base')            ? base            : base.slice(hashIndex)) + to        : createBaseLocation() + base + to    try {      // 利用history.replaceState/pushState批改历史记录      history[replace ? 'replaceState' : 'pushState'](state, '', url)      // historyState更新为最新的历史记录      historyState.value = state    } catch (err) { // 如果历史记录批改过程中报错,则应用location.reaplce/assign导航到对应url      if (__DEV__) {        warn('Error with push/replace State', err)      } else {        console.error(err)      }      location[replace ? 'replace' : 'assign'](url)    }  }  function replace(to: HistoryLocation, data?: HistoryState) {    const state: StateEntry = assign(      {},      history.state,      buildState(        historyState.value.back,        to,        historyState.value.forward,        true      ),      data,      // 因为是replace操作,所以position不变      { position: historyState.value.position }    )    changeLocation(to, state, true)    // 批改以后历史为to    currentLocation.value = to  }  function push(to: HistoryLocation, data?: HistoryState) {    const currentState = assign(      {},      historyState.value,      history.state as Partial<StateEntry> | null,      {        forward: to,        scroll: computeScrollPosition(),      }    )    if (__DEV__ && !history.state) {      warn(        `history.state seems to have been manually replaced without preserving the necessary values. Make sure to preserve existing history state if you are manually calling history.replaceState:\n\n` +          `history.replaceState(history.state, '', url)\n\n` +          `You can find more information at https://next.router.vuejs.org/guide/migration/#usage-of-history-state.`      )    }    // 第一次changeLocation,应用replace刷新以后历史,目标是记录以后页面的滚动地位    changeLocation(currentState.current, currentState, true)    const state: StateEntry = assign(      {},      buildState(currentLocation.value, to, null),      // push操作,历史记录的position+1      { position: currentState.position + 1 },      data    )    // 第二次跳转,跳转到须要跳转的地位    changeLocation(to, state, false)    currentLocation.value = to  }  return {    location: currentLocation,    state: historyState,    push,    replace,  }}

这个函数接管一个base参数,返回一个对象。这个对象中有四个属性:

  • location:一个蕴含value属性的对象,value值是createCurrentLocation()办法的返回值。那么这个value是什么呢?看下createCurrentLocation做了什么。

createCurrentLocation的作用是通过window.location创立一个规范化的history location,办法接管两个参数:通过标准化的base字符串和一个window.location对象

function createCurrentLocation(  base: string,  location: Location): HistoryLocation {  const { pathname, search, hash } = location  // allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end  // 从base中获取#的索引  const hashPos = base.indexOf('#')  // 如果base中蕴含#  if (hashPos > -1) {    // 如果hash蕴含base中的#前面局部,slicePos为base中#及前面字符串的的长度,否则为1    let slicePos = hash.includes(base.slice(hashPos))      ? base.slice(hashPos).length      : 1    // 从location.hash中获取path,/#add, #add    let pathFromHash = hash.slice(slicePos)    // 在结尾加上/,造成/#的格局    if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash    // stripBase(pathname, base):将pathname去除base局部    return stripBase(pathFromHash, '')  }  // 如果base中不蕴含#,把pathname中的base局部删除  const path = stripBase(pathname, base)  return path + search + hash}

能够看到createCurrentLocation其实就是获取window.location绝对baselocation。举几个例子(以下几个例子的base都通过标准化):如果window.location.pathname/a/b/cbase/a,那么通过createCurrentLocation失去的location/b/c;如果是有hash的状况,window.location.hash#/a/b/cbase#/a,那么通过createCurrentLocation失去的location/b/cwindow.location.hash#/a/b/cbase#,那么通过createCurrentLocation失去的location/a/b/c

  • state:一个蕴含value属性的对象,value存储的是以后的history.state
  • push:向历史记录中增加一条记录。在push过程中你会发现调用了两次changeLocation,在第一次调用changeLocation时,目标是为了记录以后页面在的滚动地位,如果应用history.back()或浏览器回退/后退按钮回到这个页面,页面会滚动到对应地位,为了不再历史栈中保留新的记录,第一次记录应用的reaplceState替换以后历史记录。第二次调用changeLocation是会跳转到须要跳转的地位。
  • reaplce:替换以后历史记录。

接下来看下useHistoryListeners办法:

function useHistoryListeners(  base: string,  historyState: ValueContainer<StateEntry>,  currentLocation: ValueContainer<HistoryLocation>,  replace: RouterHistory['replace']) {  let listeners: NavigationCallback[] = []  let teardowns: Array<() => void> = []  let pauseState: HistoryLocation | null = null  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      // 如果暂停监听了,则间接return,同时pauseState赋为null      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,      })    })  }  function pauseListeners() {    pauseState = currentLocation.value  }  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  }  function beforeUnloadListener() {    const { history } = window    if (!history.state) return    // 当页面敞开时记录页面滚动地位    history.replaceState(      assign({}, history.state, { scroll: computeScrollPosition() }),      ''    )  }  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 {    pauseListeners,    listen,    destroy,  }}

useHistoryListeners办法接管四个参数:base(标准化的base)、historyStatecurrentLocationreplace(后三个参数来自useHistoryStateNavigation的返回值)。
useHistoryListeners中,会监听popstate、beforeunload。

useHistoryListeners同样返回一个对象,该对象蕴含三个属性:

  • pauseListeners:一个暂停监听的函数。
  • listen:接管一个回调函数,并返回一个删除监听的函数。该回调函数会被退出listeners数组中,并向teardowns数组中增加卸载函数。
  • destroy:销毁函数,清空listenersteardowns,移除popstatebeforeunload监听。

当初咱们晓得了useHistoryStateNavigationuseHistoryListeners的实现后。当初咱们回到createWebHistory中,创立完historyNavigationhistoryListeners之后,紧跟着申明一个go函数。该函数接管两个变量:delta历史记录挪动的步数,triggerListeners是否触发监听。

function go(delta: number, triggerListeners = true) {  if (!triggerListeners) historyListeners.pauseListeners()  history.go(delta)}

最初创立一个routerHistory对象,并将其返回。

const routerHistory: RouterHistory = assign(  {    location: '',    base,    go,    createHref: createHref.bind(null, base),  },  historyNavigation,  historyListeners)// 拦挡routerHistory.location,使routerHistory.location返回以后路由地址Object.defineProperty(routerHistory, 'location', {  enumerable: true,  get: () => historyNavigation.location.value,})// 拦挡routerHistory.state,使routerHistory.state返回以后的的history.stateObject.defineProperty(routerHistory, 'state', {  enumerable: true,  get: () => historyNavigation.state.value,})return routerHistory

createWebHashHistory

createWebHashHistory利用createWebHashHistory实现。

export function createWebHashHistory(base?: string): RouterHistory {  // 对于应用文件协定关上的页面location.host是空字符串,这时的base为''  // 也就是说在应用文件协定关上页面时,设置了base是不失效的,因为base始终是''  base = location.host ? base || location.pathname + location.search : ''  // 容许两头的#: `/base/#/app`  if (!base.includes('#')) base += '#'  if (__DEV__ && !base.endsWith('#/') && !base.endsWith('#')) {    warn(      `A hash base must end with a "#":\n"${base}" should be "${base.replace(        /#.*$/,        '#'      )}".`    )  }  return createWebHistory(base)}

createMemoryHistory

createMemoryHistory会创立一个基于内存历史记录,次要用来解决SSR。

export function createMemoryHistory(base: string = ''): RouterHistory {  // 用户存储监听函数的数组  let listeners: NavigationCallback[] = []  // 应用一个队列保护历史记录  let queue: HistoryLocation[] = [START]  // 以后历史记录在队列中的地位  let position: number = 0  // base标准化  base = normalizeBase(base)  // 设置记录  function setLocation(location: HistoryLocation) {    position++    // 队列长度等于position时,间接push    if (position === queue.length) {      queue.push(location)    } else {      // 当历史记录在队列中的非开端地位时,删除position及之后的记录,而后再push      // 如果某一刻处在非结尾的历史记录时,这时要进行push或reqlace操作,此时position之后的记录就会生效      queue.splice(position)      queue.push(location)    }  }  // 触发监听  function triggerListeners(    to: HistoryLocation,    from: HistoryLocation,    { direction, delta }: Pick<NavigationInformation, 'direction' | 'delta'>  ): void {    const info: NavigationInformation = {      direction,      delta,      type: NavigationType.pop,    }    for (const callback of listeners) {      callback(to, from, info)    }  }  const routerHistory: RouterHistory = {    location: START,    state: {},    base,    createHref: createHref.bind(null, base),    replace(to) {      // 移除queue中索引为position的记录,并将position--      queue.splice(position--, 1)      // 在setLocation会对position从新++操作,所以position会复原要之前的值      setLocation(to)    },    push(to, data?: HistoryState) {      setLocation(to)    },    listen(callback) {      listeners.push(callback)      return () => {        const index = listeners.indexOf(callback)        if (index > -1) listeners.splice(index, 1)      }    },    destroy() {      listeners = []      queue = [START]      position = 0    },    go(delta, shouldTrigger = true) {      const from = this.location      // go的方向。delta < 0 为 back,相同为 forward      const direction: NavigationDirection =        delta < 0 ? NavigationDirection.back : NavigationDirection.forward      // go之后所处的position:Math.min(position + delta, queue.length - 1)保障了position<=queue.length - 1, 如果position + delta超出了数组最大索引,就取最大索引      // Math.max(0, Math.min(position + delta, queue.length - 1))进一步保障了position>=0,如果position + delta < 0, 则取0      position = Math.max(0, Math.min(position + delta, queue.length - 1))      // 依据shouldTrigger决定是否触发监听函数      if (shouldTrigger) {        triggerListeners(this.location, from, {          direction,          delta,        })      }    },  }  Object.defineProperty(routerHistory, 'location', {    enumerable: true,    get: () => queue[position],  })  if (__TEST__) {    routerHistory.changeURL = function (url: string) {      const from = this.location      queue.splice(position++ + 1, queue.length, url)      triggerListeners(this.location, from, {        direction: NavigationDirection.unknown,        delta: 0,      })    }  }  return routerHistory}

createWebHistorycreateWebHashHistory一样,createMemoryHistory同样返回一个RouterHistory类型的对象。与后面两个办法不同的是,createMemoryHistory保护一个队列queue和一个position,来保障历史记录存储的正确性。

总结

createWebHistorycreateWebHashHistory中通过window.history.state来治理历史记录,;而createMemoryHistory是通过保护一个队列和一个地位来实现对路由记录的治理,这也保障了在SSR中vue-router可能失常进行。