vue-router源码分析(二)

409 阅读6分钟

三、matcher

在组件根部vue实例,会在beforeCreate生命周期中执行router的init方法,在init方法中通过history.transitionTo进行路由的操作,transitionTo函数首先会通过this.router.match(location, this.current)拿到路由,那么match方法对应的是this.matcher.match方法,this.matcher在new VueRouter的时候通过createMatcher(options.routes || [], this)初始化,第一个参数传入用户写的routes,第二个参数传入当前的router实例,createMatcher函数返回一个Matcher对象。createMatcher函数首先会执行createRouteMap(routes)拿到pathList,pathMap,nameMap这三个变量。createRouteMap函数首先会定义这三个变量,然后通过forEach遍历用户传入的routes,对每一项执 行addRouteRecord(pathList, pathMap, nameMap, route)函数,并传入,第一次执行pathList是一个空数组,pathMap和nameMap是空对象,最后一个参数route是用户定义的routes,比如

{
  path: xxx,
  name: xxx,
  component: xxx,
  meta: xxx,
  children: xxx
}

addRouteRecord函数首先拿到route中的path和name,然后执行normalizePath函数。normalizePath函数接受三个参数,path传入route的path属性,第二个参数为parent(如果是嵌套路由的话,parent为父级),第三个参数是strict。首先判断strict如果是false,那么会通过正则,判断path的最后一位是否是/符号,如果是则去掉斜线。接着判断如果path的第0项是/或者parent为null那么会直接返回path,否则返回cleanPath(`${parent.path}/${path}`)cleanPath的目的是把连续的两个斜线替换为一个斜线,把当前的路径和parent.path进行拼接也就是说,在写子的path路径的时候,如果直接写detail,那么会对detail进行一个拼接,如果parent.path是category/category,那么就会拼接为category/detail。把处理过的路径赋值给normalizedPath变量后,定义了一个record对象,record.path就是刚才通过normalizePath函数计算出来的normalizedPathrecord.components是route定义的components,或{ default: route.component }record.name是route定义的name,还有meta,props等。定义record对象后,会判断当前route是否有children,如果有的话,对route.children进行遍历,把.children的每一项,作为route传入addRouteRecord函数,这样如果当前route有children就会递归的调用addRouteRecord函数,去把routes的每一个节点都会通过addRouteRecord进行处理。函数最后会判断if (!pathMap[record.path])也就是path是否被添加到了pathMap中,如果没有,那么会把record.pathpush到pathList中,并以path作为key,以record作为value,定义在pathMap中。name的处理和path相同,由于path是必须定义的,name可以不定义,所以name的处理,不会往pathList中添加。那么最终createRouteMap函数中的这三个参数,就会被赋值,执行完addRouteRecord函数,createRouteMap函数最后,会对pathList数组做一个处理,如果发现有通配符*那么会把他放到最后一位,最终createRouteMap函数返回pathListpathMap,nameMap这三个变量。createMatcher函数最终返回了值,其中一个是addRoutes方法,也就是createRouteMap(routes, pathList, pathMap, nameMap),这个提供给之后如果想要动态的修改routes,这样把之前表示路由映射关系的对象传入,也就是对旧的映射关系做修改

// src/create-matcher.js
export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  ...

  return {
    match,
    addRoutes
  }
}
// src/create-route-map.js
export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // the path list is used to control path matching priority
  const pathList: Array<string> = oldPathList || []
  // $flow-disable-line
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })

  // ensure wildcard routes are always at the end
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }

  ...

  return {
    pathList,
    pathMap,
    nameMap
  }
}

function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  const { path, name } = route
  ...
  const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)

  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }

  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    instances: {},
    enteredCbs: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
  }

  if (route.children) {
    // Warn if route is named, does not redirect and has a default child route.
    // If users navigate to this route by name, the default child will
    // not be rendered (GH Issue #629)
    ...
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

  ...

  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      ...
    }
  }
}

另一个返回的是match函数,match函数的第一个参数raw的类型是RawLocation在flow文件夹下的declarations.jsdeclare type RawLocation = string | Location,也就是说raw的类型可以是Location类型的数据,他可以拥有name,path,hash,query,params等属性,也可以是一个字符串,match函数最终返回一个Route类型的数据,相比Location类型,他多出了fullPath,matched,meta等属性。首先他会执行 normalizeLocation(raw, currentRoute, false, router)来定义locationnormalizeLocation函数首先定义next变量,next的赋值会首先判断传入的raw的类型是否是string,如果是string那么把raw作为空对象中path的值,然后赋值给next,否则把raw直接赋值给next。接着判断如果next._normalized那么直接返回next本身,这也是做了一层缓存处理,否则的话如果next有name属性,那么会对raw做一层浅拷贝,如果next有params并且params的类型是object,那么会对next.params进行一层拷贝,重新赋值给next.params,这样就完成了对next的一个深层拷贝,然后返回next。之后会通过parsePath函数对next.path进行处理,并赋值给parsedPath变量,parsePath函数接受path,query,hash三个参数,经过一些逻辑处理之后再返回。然后定义变量basePath为current.path/current.path是调用match方法传入的第二个参数。之后通过调用resolveQuery定义了变量query,match函数最后会返回一个对象,其中包含_normalized为true,还有path,query,hash。match函数首先拿到loaction之后,从其中再获取name,如果有name,那么会到之前定义的nameMap,取到对应的record,如果没有取到,会调用 _createRoute(null, location)。如果取到了,那么会去对他的params做一些处理最后调用_createRoute(record, location, redirectedFrom),否则如果有path,那么会去pathMap取到对应的record,然后会判断matchRoute(record.regex, location.path, location.params),matchRoute是根据传入的record.regex正则,来匹配location.path,如果匹配到返回true,否则false,如果匹配到,那么 return _createRoute(record, location, redirectedFrom),如果没有匹配到,或者没有找到location的path和name,那么会执行 return _createRoute(record, location, redirectedFrom)

// src/create-matcher.js
export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  ...
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location

    if (name) {
      const record = nameMap[name]
      ...
      if (!record) return _createRoute(null, location)
      const paramNames = record.regex.keys
        .filter(key => !key.optional)
        .map(key => key.name)

      if (typeof location.params !== 'object') {
        location.params = {}
      }

      if (currentRoute && typeof currentRoute.params === 'object') {
        for (const key in currentRoute.params) {
          if (!(key in location.params) && paramNames.indexOf(key) > -1) {
            location.params[key] = currentRoute.params[key]
          }
        }
      }

      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      return _createRoute(record, location, redirectedFrom)
    } else if (location.path) {
      location.params = {}
      for (let i = 0; i < pathList.length; i++) {
        const path = pathList[i]
        const record = pathMap[path]
        if (matchRoute(record.regex, location.path, location.params)) {
          return _createRoute(record, location, redirectedFrom)
        }
      }
    }
    // no match
    return _createRoute(null, location)
  }
  ...
  return {
    match,
    addRoutes
  }
}

可以看到match函数最终都会return一个调用_createRoute函数的结果,如果通过path或者name匹配到了正确的路径,那么会调用_createRoute(record, location, redirectedFrom),否则会调用_createRoute(null, location)_createRoute函数中,如果传入的record有redirect,那么会调用redirect函数,redirect函数就是路由的重定向,他最终也会调用match方法或调用_createRoute(null, location),如果传入的record有matchAs他会执行alias函数也就是路由别名功能,最终他也会调用_createRoute方法,也就是说最终都会调用 return createRoute(record, location, redirectedFrom, router),createRoute函数最终返回一个route类型的数据,形成路由的对应关系

function _createRoute (
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    return createRoute(record, location, redirectedFrom, router)
  }

在看源码的过程中会遇到很多辅助函数,他会帮助你对参数进行一些处理,其中有的逻辑很复杂,在看源码的过程中,最重要的是对整体逻辑的把控,之后可以再去细致的看具体的函数是如何实现的,在很多项目中,会有一些单元测试的脚本,比如在vue-router中,test/unit/specs文件夹下就有很多辅助函数的测试,比如normalizeLocation函数,他会在单元测试中传入不同的参数,并通过expect函数来拿到相应的结果,遇到一些辅助函数,可以通过看单元测试的方式,快速了解一个函数的大概目的(传入什么参数,执行之后返回什么参数)

// test/unit/specs/location.spec.js
import { normalizeLocation } from '../../../src/util/location'

describe('Location utils', () => {
  describe('normalizeLocation', () => {
    it('string', () => {
      const loc = normalizeLocation('/abc?foo=bar&baz=qux#hello')
      expect(loc._normalized).toBe(true)
      expect(loc.path).toBe('/abc')
      expect(loc.hash).toBe('#hello')
      expect(JSON.stringify(loc.query)).toBe(JSON.stringify({
        foo: 'bar',
        baz: 'qux'
      }))
    })

    it('empty string', function () {
      const loc = normalizeLocation('', { path: '/abc' })
      expect(loc._normalized).toBe(true)
      expect(loc.path).toBe('/abc')
      expect(loc.hash).toBe('')
      expect(JSON.stringify(loc.query)).toBe(JSON.stringify({}))
    })
   ...
})