preface

Picking up from the previous article, we saw what the Router initialization process does, from http://localhost:8080 to http://localhost:8080/#/, so how does the Router find the corresponding component to render when we enter a path? This article mainly introduces the matching process.

Take the route refresh at http://localhost:8080/#/foo/child/10000 as an example.

  • Vue Router: Import Router and new Router(
  • Vue Router Jump analysis (Part) : Route matcher logic analysis
  • Vue Router Jump analysis (Part 2) : Use the route matcher to find the route and jump

1. CreateMatcher function

When initializing the Router, we skipped the matcher-related: Enclosing the matcher = createMatcher (options. Routes | | [], this), the matcher by createMatcher method returns, it defined in SRC/create – the matcher. In js:

export function createMatcher (routes: Array
       
        , router: VueRouter
       ) :Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

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

  function match (raw: RawLocation, currentRoute? : Route, redirectedFrom? : Location) :Route { / *... * / }

  function redirect (record: RouteRecord, location: Location) :Route { / *... * / }

  function alias (record: RouteRecord, location: Location, matchAs: string) :Route { / *... * / }

  function _createRoute (record: ? RouteRecord, location: Location, redirectedFrom? : Location) :Route { / *... * / }

  return {
    match,
    addRoutes
  }
}
Copy the code

CreateMatcher takes two arguments, the first is the Routes array we passed in at initialization, and the second is the Router object, which essentially does a series of operations and returns an object containing the match method and the addRoutes method.

Const {pathList, pathMap, nameMap} = createRouteMap(routes) const {pathList, pathMap, nameMap} = createRouteMap(routes)

  • [‘/foo/child/:id’, ‘foo’, ‘bar’] [‘/foo/child/:id’, ‘foo’, ‘bar’]
  • PathMap: a route mapping object. The key is path and the value is a RouteRecord object
  • NameMap: Name mapping table where key is name and value is a RouteRecord object

The debugger:

2. CreateRouteMap function

The createRouteMap method is defined in SRC /create-route-map.js (removing code that does not affect the main logic) :

export function createRouteMap (routes: Array
       
        , oldPathList? : Array
        
         , oldPathMap? : Dictionary
         
          , oldNameMap? : Dictionary
          
         
        
       ) :{
  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 = [{ path: '/foo', ... }, { path: '/bar', ... }]
  routes.forEach(route= > {
    // With {path: '/foo',... } analysis
    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] === The '*') {
      pathList.push(pathList.splice(i, 1) [0])
      l--
      i--
    }
  }

  return {
    pathList,
    pathMap,
    nameMap
  }
}
Copy the code

CreateRouteMap = [] pathMap = {} nameMap = {} createRouteMap = {} If there is a wildcard in the path, it is put last. Finally, it returns the pathList, pathMap, and nameMap objects processed by addRouteRecord.

3. AddRouteRecord function

Let’s take a look at the addRouteRecord method. Since this method is a bit long, let’s remove some code and warning judgments that don’t affect the main logic.

function addRouteRecord (
  pathList: Array<string>, // []
  pathMap: Dictionary<RouteRecord>, // {}
  nameMap: Dictionary<RouteRecord>, // {}
  route: RouteConfig, // { path: '/foo'. } parent? : RouteRecord,//undefined matchAs? : string// undefined
) {
  // Deconstruct path and name from the route object
  // route: {
  // path: '/foo',
  // name: 'Foo',
  // component: Foo,
  // meta: { permission: true },
  // children: [
  / / {
  // path: 'child/:id',
  // name: 'Child',
  // component: Child,
  / /},
  / /,
  // },
  const { path, name } = route // path = '/foo', name = 'Foo'

  // ('/foo', undefined, undefined) normalizedPath = 'foo'
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
Copy the code

function normalizePath ( path: string, parent? : RouteRecord, strict? : boolean ): string { if (! strict) path = path.replace(//$/, ”) if (path[0] === ‘/’) return path // returrn “/foo” if (parent == null) return path return cleanPath(`${parent.path}/${path}`)

}

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

  // Recursively iterate over the same operation for children
  if (route.children) {
    route.children.forEach(child= > {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  // Store the path of record and store the record in pathMap, nameMap, convenient for subsequent retrieval
  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) {
      warn(
        false.`Duplicate named routes definition: ` +
          `{ name: "${name}", path: "${record.path}" }`)}}}Copy the code

The RouteRecord object is similar to the route array object. It has path, name, component, and other attributes. It is a repository for routing attributes.

We then determine if RouteRecord has children, which it does in our example, so we recursively execute the addRouteRecord method again until we have traversed all children.

Then check whether the pathMap contains a value whose key is Path. If the pathList array pushes the current path, establish a mapping between Path and RouteRecord in the pathMap object.

Finally, judge whether there is a name attribute, establish the mapping between name and record.

In this way, we can correctly find the corresponding RouteRecord by path or name.

By executing addRouteRecord, we have a pathList, pathMap, nameMap that holds the data.

To clarify the previous logic:

  • Let’s start with the callcreateMatcherCreate the matcher
  • increateMatcherMethodcreateRouteMapThe method yields three values: pathList, pathMap, and nameMap
  • One was returnedmatchFunctions andaddRoutesfunction

4. Match function

So let’s look at what the match function does.

function match (
  raw: RawLocation, // raw = "/foo/child/10000"currentRoute? : Route,// currentRoute = { name: null, path: "/", hash: "". } redirectedFrom? : Location// undefined
) :Route {
  // location: {
  // hash: ""
  // path: "/foo/child/10000"
  // query: {}
  // }
  const location = normalizeLocation(raw, currentRoute, false, router)
  const { name } = location // name = undefined

  if (name) {
    const record = nameMap[name]
    if(process.env.NODE_ENV ! = ='production') {
      warn(record, `Route with name '${name}' does not exist`)}if(! record)return _createRoute(null, location)
    const paramNames = record.regex.keys
      .filter(key= >! key.optional) .map(key= > key.name)

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

    if (currentRoute && typeof currentRoute.params === 'object') {
      for (const key in currentRoute.params) {
        if(! (keyin 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 a path is matched, return the path created with the related attributes in the record, location
      if (matchRoute(record.regex, location.path, location.params)) {
        return _createRoute(record, location, redirectedFrom)
      }
    }
  }
  // no match
  return _createRoute(null, location)
}
Copy the code

There are two objects Rawlocation and Location. Let’s look at them briefly:

declare typeLocation = { _normalized? :boolean; name? :string; path? :string; hash? :string; query? : Dictionary<string>; params? : Dictionary<string>; append? :boolean; replace? :boolean;
}

declare type RawLocation = string | Location
Copy the code

A Location object holds some property value on a path, and RawLocation is a string or Location object.

Then the first step in the function body is to implement a normalizeLocation function, which formats raw of different formats into a Location object. Logic is quite complicated and we don’t need to clarify this, but we can open the test/unit/specs/location. Spec. Js unit test file and see what he would do some operation:

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'}}})))Copy the code

When our path is/ABC? When foo=bar&baz=qux#hello, the normalizeLocation function returns an object like this:

location = {
  _normalized: true.path: '/abc'.hash: 'hello'.query: {
    foo: 'bar'.baz: 'qux',}}Copy the code

Then we need to understand the internal implementation of normalizeLocation and know what it does, which is a lazy way to read the source code.

Continuing our main thread, once we get location, we first fetch name from it. If there is a name, we fetch the Record object directly from nameMap, and when this object does not exist, we call the _createRoute method to create an empty path to return.

function _createRoute (record: ? RouteRecord,// record = { path: "/foo/child/:id", regex: xxxx, components: xxx, ... }
  location: Location, // location = { _normalized: true, path: "/foo/child/10000", xxx } redirectedFrom? : Location// undefined
) :Route {
  if (record && record.redirect) {
    return redirect(record, redirectedFrom || location)
  }
  if (record && record.matchAs) {
    return alias(record, location, record.matchAs)
  }
  // Redirect and matchAs do not exist, so createRoute is implemented
  return createRoute(record, location, redirectedFrom, router)
}

// createRoute is defined in SRC /util/route.js
export function createRoute (record: ? RouteRecord, location: Location, redirectedFrom? :? Location, router? : VueRouter) :Route {
  // String query parameters
  const stringifyQuery = router && router.options.stringifyQuery // stringifyQuery = undefined

  // Get query parameters
  let query: any = location.query || {} // query = {}
  / / copy the query
  try {
    query = clone(query) // query = {}
  } catch (e) {}

  // Create a Route object
  const route: Route = {
    name: location.name || (record && record.name), // name = "Child"
    meta: (record && record.meta) || {}, // meta = {}
    path: location.path || '/'.// path = "/foo/child/10000"
    hash: location.hash || ' '.// hash = ""
    query, // query = {}
    params: location.params || {}, // params = { id: 10000 }
    fullPath: getFullPath(location, stringifyQuery), // fullPath = "/foo/child/10000"
    // matched = [{ path: '/foo', ... }, { path: '/foo/child/:id', ... }]
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  / / return route
  return Object.freeze(route)
}
Copy the code

Now that the work of the match method is done, let’s review that it does three main things:

  1. Format the route and the current route as Location objects
  2. Find the Record by name or path in location
  3. Create Route objects from record and Location

5. AddRoutes function

We have analyzed the following addRoutes method:

function addRoutes (routes) {
  createRouteMap(routes, pathList, pathMap, nameMap)
}
Copy the code

Create new routes and pathList, pathMap, nameMap values. When createRouteMap is executed, modify them from the original base. Ensure that the path we added is initialized correctly.

conclusion

Down like this, we finally finished analyzing the matcher related logic, you can see, the internal logic is very complex, we want to think about, don’t need to scrutinize some logic or the auxiliary function, only need to know what he is, as previously mentioned by looking at the unit test method is a good place to start.

We analyzed the Matcher to see how we correctly rendered to the components we defined when we first entered, and also to lay a foundation for analyzing what the Vue Router was doing when we switched routes.