vue-router 源码解读(第二出)【完】

前方

上一篇文章《vue-router 源码解读(第一出)》分析了 vue-rourter 的几个主要的文件的源码,这一篇文章则分析其它次要的文件里的源码。

util/path.js 源码

/* @flow */

export function resolvePath (
  relative: string,
  base: string,
  append?: boolean
): string {
  // 获取 url 的每一个字符
  const firstChar = relative.charAt(0)
  // 如果是斜杠直接返回
  if (firstChar === '/') {
    return relative
  }
  // 如果是 '?' 或者 '#' 直接 base + relative
  if (firstChar === '?' || firstChar === '#') {
    return base + relative
  }

  // 把基础 url 字符串以斜杠作为切点,打散成数组
  const stack = base.split('/')
  // remove trailing segment if:
  // - not appending
  // - appending to trailing slash (last segment is empty)
  // 如果 append 不为真,去掉 stack 最后一个,或者去掉 url 字符串最后一个斜杠(如果有)
  if (!append || !stack[stack.length - 1]) {
    stack.pop()
  }

  // resolve relative path
  // 如果 relative 有以斜杠开头的,先把斜杠替换换为空字符,再根据斜杠切开
  const segments = relative.replace(/^\//, '').split('/')
  for (let i = 0; i < segments.length; i++) {
    const segment = segments[i]
    // 如果是两个点
    if (segment === '..') {
      stack.pop()
    } else if (segment !== '.') {
      stack.push(segment)
    }
  }

  // ensure leading slash
  // 如果 stack 数组第一个元素不为空字符,则添加
  if (stack[0] !== '') {
    stack.unshift('')
  }
  // 把数组再拼回 url
  return stack.join('/')
}

// 分解 url(如果是 hash 模式,则为 # 后面的 url 字符串) ,返回一个包含有 path、query、hash 的对象
export function parsePath (path: string): {
  path: string;
  query: string;
  hash: string;
} {
  let hash = ''
  let query = ''
  // 查找 path中的 '#' 的下标
  const hashIndex = path.indexOf('#')
  // 如果存在
  if (hashIndex >= 0) {
    // 取包括 '#' 及其后面的字符串
    hash = path.slice(hashIndex)
    // 取 '#' 前面的 url 字符串
    path = path.slice(0, hashIndex)
  }
  // 查找参数标识 '?' 的下标
  const queryIndex = path.indexOf('?')
  // 如果找到
  if (queryIndex >= 0) {
    // 保存参数字符串(不带问号)
    query = path.slice(queryIndex + 1)
    // 保存路径字符中
    path = path.slice(0, queryIndex)
  }

  return {
    path,
    query,
    hash
  }
}

// 双斜杠全局替换成单斜杠
export function cleanPath (path: string): string {
  return path.replace(/\/\//g, '/')
}

这个文件主要是向外暴露三个用于处理 url 字符串的方法。

util/location.js 文件源码

/* @flow */

import type VueRouter from '../index'
import { parsePath, resolvePath } from './path'
import { resolveQuery } from './query'
import { fillParams } from './params'
import { warn } from './warn'
import { extend } from './misc'

export function normalizeLocation (
  raw: RawLocation,
  current: ?Route,
  append: ?boolean,
  router: ?VueRouter
): Location {
  // 如果 raw 的类型为 'string',则返回 {path:raw},否则直接返回 raw 作为 next 的值
  // next 是一个存储了将要跳转到的路由的相关信息的对象
  let next: Location = typeof raw === 'string' ? { path: raw } : raw
  // 如果 next 已经初始化,则直接返回 next
  if (next._normalized) {
    // 如果设置了 _normalized 为真,直接返回 next
    return next
  } else if (next.name) {
    // 如果 next.name 存在,复制 raw 属性到新对象
    next = extend({}, raw)
    const params = next.params
    // 如果 params 存在并且 params 为一个对象,复制 params 属性到 next.params 新对象
    if (params && typeof params === 'object') {
      next.params = extend({}, params)
    }
    // 返回 next
    return next
  }
  // 前面的判断都是基于我们在使用 RouterLink 组件时给 go 属性赋值为对象(对象含有 _normalized 或者 name 属性),
  // 如果不满足上面的两个判断条件,那么就继续往下执行
  // next.path 不存在,并且 params 参数和 current 有值,
  if (!next.path && next.params && current) {
    next = extend({}, next)
    next._normalized = true
    // 合并参数(当前路由参数和设置的参数)
    const params: any = extend(extend({}, current.params), next.params)
    if (current.name) {
      // 设置 next.name 和 next.params
      next.name = current.name
      next.params = params
    } else if (current.matched.length) {
      const rawPath = current.matched[current.matched.length - 1].path
      // 保存拼接好的 url (含参数字符串)
      next.path = fillParams(rawPath, params, `path ${current.path}`)
    } else if (process.env.NODE_ENV !== 'production') {
      warn(false, `relative params navigation requires a current route.`)
    }
    return next
  }

  const parsedPath = parsePath(next.path || '')
  const basePath = (current && current.path) || '/'
  const path = parsedPath.path
    ? resolvePath(parsedPath.path, basePath, append || next.append)
    : basePath

  const query = resolveQuery(
    parsedPath.query,
    next.query,
    router && router.options.parseQuery
  )

  let hash = next.hash || parsedPath.hash
  if (hash && hash.charAt(0) !== '#') {
    hash = `#${hash}`
  }

  return {
    _normalized: true,
    path,
    query,
    hash
  }
}

util/query.js文件源码

/* @flow */

import { warn } from './warn'

const encodeReserveRE = /[!'()*]/g
const encodeReserveReplacer = c => '%' + c.charCodeAt(0).toString(16)
// 匹配逗号encode() 后的字符串
const commaRE = /%2C/g

// fixed encodeURIComponent which is more conformant to RFC3986:
// - escapes [!'()*]
// - preserve commas
// encodeURIComponent() 该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ' ( ) 。
// 其他字符(比如 :;/?:@&=+$,# 这些用于分隔 URI 组件的标点符号),都是由一个或多个十六进制的转义序列替换的。
// 解释来源:https://www.w3school.com.cn/js/jsref_encodeURIComponent.asp
const encode = str => encodeURIComponent(str)
  .replace(encodeReserveRE, encodeReserveReplacer)
  .replace(commaRE, ',')

const decode = decodeURIComponent

// 合并参数对象(parseQuery()方法返回的对象 、extraQuery 对象)
export function resolveQuery (
  query: ?string,
  extraQuery: Dictionary<string> = {},
  _parseQuery: ?Function
): Dictionary<string> {
  // 如果没传 _parseQuery 则用 vue-router 预定义好的方法处理
  const parse = _parseQuery || parseQuery
  let parsedQuery
  try {
    parsedQuery = parse(query || '')
  } catch (e) {
    process.env.NODE_ENV !== 'production' && warn(false, e.message)
    parsedQuery = {}
  }
  // 遍历合并
  for (const key in extraQuery) {
    parsedQuery[key] = extraQuery[key]
  }
  return parsedQuery
}

// 返回一个参数名和值的对象,如果有多个同名的 key,那么在这个对象的此 key 的值为一个对象,存放所有的同名值
function parseQuery (query: string): Dictionary<string> {
  const res = {}
  // 去字符串前后空格,替换掉第一个?、# 或者 &
  query = query.trim().replace(/^(\?|#|&)/, '')
  // 如果 query 不为真,则直接返回空对象
  if (!query) {
    return res
  }

  query.split('&').forEach(param => {
    // 把参数名和参数值分开形成数组
    const parts = param.replace(/\+/g, ' ').split('=')
    // 对参数的 key 值进行 decode() 解码
    const key = decode(parts.shift())
    // 取出值
    const val = parts.length > 0
      ? decode(parts.join('='))
      : null
    // 如果不存在于 res 对象中,则添加到对象中
    if (res[key] === undefined) {
      res[key] = val
    } else if (Array.isArray(res[key])) { // 如果值是个数组,那把 val 放到数组的最后面
      res[key].push(val)
    } else { // 如果有值且还不是数组,那创建数组存放所有同 key 的值
      res[key] = [res[key], val]
    }
  })

  return res
}

// 返回参数字符串
export function stringifyQuery (obj: Dictionary<string>): string {
  // 下面的代码看起来有点复杂,但仔细分析也就那么回事。
  // 如果 obj 存在,则用 map() 方法遍历此对象上的 key,如果此 key 对应的值为数组,则用 forEach() 方法遍历这个值
  // 最后把 map() 方法返回来的数组用 filter() 方法过滤出字符串长度大于0的项。
  // 接着用 join('&') 方法把所有的参数连接起来
  const res = obj ? Object.keys(obj).map(key => {
    const val = obj[key]

    if (val === undefined) {
      return ''
    }

    if (val === null) {
      return encode(key)
    }

    if (Array.isArray(val)) {
      const result = []
      val.forEach(val2 => {
        if (val2 === undefined) {
          return
        }
        // 如果 val2 为 null
        if (val2 === null) {
          // 编码 encode(key) 并推入数组
          result.push(encode(key))
        } else {
          // 若值 val2 即不是 undefined 也不是 null,则用 '=' 作为连接符把编码后的 encode(key) 和 encode(val2)) 拼接起来
          result.push(encode(key) + '=' + encode(val2))
        }
      })
      // 值遍历完之后,用 '&' 把数组的所有项连接成字符串
      return result.join('&')
    }
    // 如果上面的三种情况都不满足(不是 undefined、null、数组),返回用 '=' 作为连接符把编码后的 encode(key) 和 encode(val2)) 拼接起来
    return encode(key) + '=' + encode(val)
  }).filter(x => x.length > 0).join('&') : null
  // 返回参数字符串
  return res ? `?${res}` : ''
}

util/params.js 文件源码

/* @flow */

import { warn } from './warn'
import Regexp from 'path-to-regexp'
// path-to-regexp 模块用法可以看这里:https://www.npmjs.com/package/path-to-regexp
// $flow-disable-line
const regexpCompileCache: {
  [key: string]: Function
} = Object.create(null)

// 这个方法主是拼接成一个带参数的 url 字符串
export function fillParams (
  path: string,
  params: ?Object,
  routeMsg: string
): string {
  params = params || {}
  try {
    const filler =
      regexpCompileCache[path] ||
      (regexpCompileCache[path] = Regexp.compile(path))

    // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }}
    if (params.pathMatch) params[0] = params.pathMatch
    // 返回 Regexp.compile() 的执行结果(依然是返回一个函数,这里形成了闭包)
    return filler(params, { pretty: true })
  } catch (e) {
    if (process.env.NODE_ENV !== 'production') {
      warn(false, `missing param for ${routeMsg}: ${e.message}`)
    }
    return ''
  } finally {
    // delete the 0 if it was added
    delete params[0]
  }
}

util/rourte.js 文件源码

/* @flow */

import type VueRouter from '../index'
import { stringifyQuery } from './query'

// 匹配零个或者一个斜杠
const trailingSlashRE = /\/?$/

// 创建路由对象
export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}
  try {
    query = clone(query)
  } catch (e) {}

  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  return Object.freeze(route)
}
// 深度拷贝数组/对象,或者直接返回值
function clone (value) {
  // 如果是数组
  if (Array.isArray(value)) {
    // 使用数组的 map 遍历并使用 close() 方法,递归处理
    return value.map(clone)
  } else if (value && typeof value === 'object') { // 如果是一个对象
    const res = {}
    // 通过 for in 遍历,并调用 clone() 递归处理
    for (const key in value) {
      res[key] = clone(value[key])
    }
    return res
  } else {
    // 如果不是数组也不是对象,则直接返回此值
    return value
  }
}

// the starting route that represents the initial state
export const START = createRoute(null, {
  path: '/'
})

// 生成一个由父子路由记录对象形成的数组
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
  const res = []
  while (record) {
    // 向 res 开头添加 record 路由记录
    res.unshift(record)
    // 赋值 record.parent 给 record(只有子路由记录 record.parent 才会有值)
    record = record.parent
  }
  return res
}

// 获取完整的 url
function getFullPath (
  { path, query = {}, hash = '' },
  _stringifyQuery
): string {
  const stringify = _stringifyQuery || stringifyQuery
  return (path || '/') + stringify(query) + hash
}

// 判断是否是相同的路由
export function isSameRoute (a: Route, b: ?Route): boolean {
  if (b === START) {
    return a === b
  } else if (!b) {
    return false
  } else if (a.path && b.path) {
    return (
      // 比对 path 去掉斜杠后值
      a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') &&
      // 两个路由的 hash 对象一样
      a.hash === b.hash &&
      // 两个路由的 query 对象一样
      isObjectEqual(a.query, b.query)
    )
  } else if (a.name && b.name) { // 两个路由的 name 对象一样
    return (
      a.name === b.name && // 这一行代码是不是有点多余了???
      // 两个路由的 hash 对象一样
      a.hash === b.hash &&
      // 两个路由的 query 对象一样
      isObjectEqual(a.query, b.query) &&
      // 两个路由的 params 对象一样
      isObjectEqual(a.params, b.params)
    )
  } else {
    return false
  }
}

function isObjectEqual (a = {}, b = {}): boolean {
  // handle null value #1566
  if (!a || !b) return a === b
  const aKeys = Object.keys(a)
  const bKeys = Object.keys(b)
  if (aKeys.length !== bKeys.length) {
    return false
  }
  return aKeys.every(key => {
    const aVal = a[key]
    const bVal = b[key]
    // 如果值是对象,递归遍历
    if (typeof aVal === 'object' && typeof bVal === 'object') {
      return isObjectEqual(aVal, bVal)
    }
    return String(aVal) === String(bVal)
  })
}

export function isIncludedRoute (current: Route, target: Route): boolean {
  return (
    current.path.replace(trailingSlashRE, '/').indexOf(
      target.path.replace(trailingSlashRE, '/')
    ) === 0 &&
    (!target.hash || current.hash === target.hash) &&
    queryIncludes(current.query, target.query)
  )
}

function queryIncludes (current: Dictionary<string>, target: Dictionary<string>): boolean {
  for (const key in target) {
    if (!(key in current)) {
      return false
    }
  }
  return true
}

history/hash.js 文件源码

/* @flow */

import type Router from '../index'
import { History } from './base'
import { cleanPath } from '../util/path'
import { getLocation } from './html5'
import { setupScroll, handleScroll } from '../util/scroll'
import { pushState, replaceState, supportsPushState } from '../util/push-state'

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }

  // this is delayed until the app mounts
  // to avoid the hashchange listener being fired too early
  // 监听这两个事件是为了解决直接在浏览器中输入 url 实现更新视图的问题
  setupListeners () {
    // 保存路由实例,监听 popstate 或者 hashchange 变化
    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      setupScroll()
    }

    window.addEventListener(
      supportsPushState ? 'popstate' : 'hashchange',
      () => {
        const current = this.current
        if (!ensureSlash()) {
          return
        }
        // 跳转路由
        this.transitionTo(getHash(), route => {
          if (supportsScroll) {
            handleScroll(this.router, route, current, true)
          }
          if (!supportsPushState) {
            replaceHash(route.fullPath)
          }
        })
      }
    )
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        pushHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        replaceHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

  go (n: number) {
    window.history.go(n)
  }

  ensureURL (push?: boolean) {
    const current = this.current.fullPath
    if (getHash() !== current) {
      push ? pushHash(current) : replaceHash(current)
    }
  }

  getCurrentLocation () {
    return getHash()
  }
}

function checkFallback (base) {
  const location = getLocation(base)
  // 如果是 '/#' 开头的
  if (!/^\/#/.test(location)) {
    window.location.replace(cleanPath(base + '/#' + location))
    return true
  }
}

// 确保 ptah 的第一个字符为斜杠
function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  // 如果没有就给 path 补一个
  replaceHash('/' + path)
  return false
}
// 获取
export function getHash (): string {
  // We can't use window.location.hash here because it's not
  // consistent across browsers - Firefox will pre-decode it!
  let href = window.location.href
  const index = href.indexOf('#')
  // empty path
  if (index < 0) return ''
  // 保存 href 字符串中 '#' 以后的字符
  href = href.slice(index + 1)
  // decode the hash but not the search or hash
  // as search(query) is already decoded
  // https://github.com/vuejs/vue-router/issues/2708
  // 保存 '?' 以后的字符
  const searchIndex = href.indexOf('?')
  // 如果没找到
  if (searchIndex < 0) {
    const hashIndex = href.indexOf('#')
    if (hashIndex > -1) {
      // 对 url 进行解码
      href = decodeURI(href.slice(0, hashIndex)) + href.slice(hashIndex)
    } else href = decodeURI(href)
  } else {
    // 如果找到
    if (searchIndex > -1) {
      // 生新组合 href
      href = decodeURI(href.slice(0, searchIndex)) + href.slice(searchIndex)
    }
  }

  return href
}

// 生成 '#' 号的 hash url
function getUrl (path) {
  // 获取当前完整的url
  const href = window.location.href
  const i = href.indexOf('#')
  // 保存主 url
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

// 判断当前环境是否支持 HTML5 的 pushState,支持则使用 pushState() 方法,否则使用 window.location.hash
function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

// 这个方法同上面的差不多
function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}

// 所有即使在创建路由实例时设置的是 hash 模式,hsah 中的最终实现还是优先使用 HTML5 的 pushState() 方法,如果不支持再降级到 window.location 的 hash() 方法 和 replace() 方法