Vue – the router source code parsing words | | 1.5 w multiple warning – 【 in 】

  • Hello everyone, I am Guanghui 😎
  • This isvue-routerSource code analysis of the middle, is also the core
  • This article mainly introduces the following points
    • This paper introduces thevue-routerHow is route matching done
    • How do guards and hooks trigger
    • How are asynchronous components handled, and so on
  • Although it looks like a little knowledge, but the content is not a lot
  • Another thing to say
    • The first time to do source code analysis, there must be a lot of mistakes or understanding is not in place, welcome to correct 🤞
  • The project address
    • https://github.com/BryanAdamss/vue-router-for-analysis
    • Give me one if you think it’ll helpstar
  • Uml diagram source file
    • https://github.com/BryanAdamss/vue-router-for-analysis/blob/dev/vue-router.EAP
  • Associated article link
    • Vue – the router source code parsing words | | 1.3 w multiple warning – [on]
    • Vue – the router source code parsing words | | 1.5 w multiple warning – 【 in 】
    • Vue – the router source code parsing | 6 k word – 【 the 】

Routing hop

  • As mentioned before, to realize the hop of a route, the route object matching the address must be found in the route mapping table first. This process is called route matching. After finding the matched route, the hop is resolved
  • Therefore, there are two key steps to realize route redirection: route matching and navigation resolution
  • VueRouterEncapsulate the two key steps described above intotransitionToThe method of
  • Next, let’s take a looktransitionToThe implementation of the

transitionTo

  • As I mentioned earlier,transitionToMethods are defined in the base classHistoryOn the
// src/history/base.js./ / parent class
export class History {...// Route jump
  transitionTo (
    location: RawLocation, // Primitive location, either a URL or a location interface(custom shape, defined in types/router.d.ts)onComplete? :Function.// The jump successfully callbackonAbort? :Function// Jump failed callback
  ) {
    const route = this.router.match(location, this.current) // Pass in the location to jump to and the current Route object, and return the Route to

    // Confirm the jump
    this.confirmTransition(
      route,
      () = > { // onComplete
        this.updateRoute(route) AfterEach hook is triggered when a route is updated
        onComplete && onComplete(route) // Call the onComplete callback
        this.ensureURL()
        // fire ready cbs once
        // Trigger the ready callback
        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb= > {
            cb(route)
          })
        }
      },
      err= > { // onAbort
        if (onAbort) {
          onAbort(err)
        }
        // Raise the error callback
        if (err && !this.ready) {
          this.ready = true
          this.readyErrorCbs.forEach(cb= > {
            cb(err)
          })
        }
      }
    )
  }
}
Copy the code
  • insetupListenersThe chapter also introducestransitionToMethod signature of
  • Receive three parameters
    • locationforRawLocationType, which represents the address to resolve
    • onCompleteThe jump success callback is invoked when the route jump succeeds
    • onAbortIs a jump failure (cancel) callback, called when the route is cancelled
  • Look at the internal logic
  • callThe router instancesthematchMethod to get the route object to jump to from the route mapping tableroute; This is essentiallyRouting matchingProcess;
  • Get what you want to jump torouteAfter the callconfirmTransitioncompleterouteParse the jump, and call the corresponding callback method when the jump is successful and canceled; This is aNavigation parsingprocess
    • Call on successupdateRouteTrigger rerender, and then trigger the related callback; We’ll cover rendering in a later chapter
    • When cancel (failed), the related callback is triggered
  • So let’s look at the route matching process

Routing matching

  • transitionToWill callThe router instancesthematchMethod to implement route matching
// src/index.js

export default class VueRouter {...constructor (options: RouterOptions = {}) {

    this.matcher = createMatcher(options.routes || [], this)... }// Get the matching routing objectmatch ( raw: RawLocation, current? : Route, redirectedFrom? : Location ): Route {return this.matcher.match(raw, current, redirectedFrom)
  }
}
Copy the code
  • Match of the Router instanceMethod, also called by the matchermatchMethod, pass the parameters through directly
    • For the creation of a match, see the previous oneCreate matcherchapter
  • Let’s move on to the matchermatchmethods
// src/create-matcher.js.export function createMatcher (
  routes: Array<RouteConfig>, // Route configuration list
  router: VueRouter / / VueRouter instance
) :Matcher {...// Pass location to return a matching Route object
  function match (raw: RawLocation, currentRoute? : Route, redirectedFrom? : Location) :Route {
    // Get the formatted location where the router instance can be accessed due to closure features
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location

    // Match by name
    if (name) {
      const record = nameMap[name]
      if(process.env.NODE_ENV ! = ='production') {
        // No warning found
        warn(record, `Route with name '${name}' does not exist`)}// If no Route record is found, an empty Route is created
      if(! record)return _createRoute(null, location)

      // Get the dynamic route parameter name
      const paramNames = record.regex.keys
        .filter(key= >! key.optional) .map(key= > key.name)
      if (typeoflocation.params ! = ='object') {
        location.params = {}
      }

      // Extract the value of the current Route that matches the dynamic Route parameter name and assign it to location
      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]
          }
        }
      }

      / / fill the params
      location.path = fillParams(record.path, location.params, `named route "${name}"`)

      / / create the route
      return _createRoute(record, location, redirectedFrom)
    } else if (location.path) {
      location.params = {}

      // Iterate through the pathList to find matching records and generate a Route
      for (let i = 0; i < pathList.length; i++) {
        const path = pathList[i]
        const record = pathMap[path]
        if (matchRoute(record.regex, location.path, location.params)) {

          // After finding the matching Route record, generate the corresponding Route
          return _createRoute(record, location, redirectedFrom)
        }
      }
    }

    // no match
    return _createRoute(null, location)
  }
   
}
Copy the code
  • Due to therouter.matchThe parameters are passed through, so the signatures are exactly the same
    • rawisRawLocationType: indicates the address to be matched
    • currentRouteIs the current routing object
    • redirectedFromIt means what address it was redirected from
  • We look at thematchMethod logic
    • First of all, it’s against the incomingrawAddress, formatted (normalized)
    • Then fetch the formatted addressname
    • nameIf yes, check whether it can passnameinnameMapTo find the corresponding route recordRouteRecord
      • If not, create a new onerouteThe object returned
      • If it can be found, fill itparamsAnd uses this routing record to create a new oneRouteThe object returned
    • nameIf it does not exist, judgepathIf there is a
      • Exist, then usePathList, pathMapcallmatchRouteCheck whether a route is matched, and then find the matched route record. Then use this route record to create a new onerouteThe object returned
    • name,pathAren’t there
      • Create a new onerouteThe object returned
  • The activity diagram is as follows
  • match.png
  • Let’s extract the keywords of the above process
    • Address formatting normalizeLocation,Check whether the address matches matchRoute,Fill the parameter fillParams,Create the route object _createRoute

Address formatting normalizeLocation

  • Let’s see why we need to format the address. Okay
  • We know thatVueRouteThe address defined isRawLocationType, which is union type, supportedstringandLocationtype
// flow/declarations.js

declare typeLocation = { _normalized? :booleanname? :stringpath? :stringhash? :stringquery? : Dictionary<string> params? : Dictionary<string> append? :booleanreplace? :boolean
}

declare type RawLocation = string | Location
Copy the code
  • So the following addresses are all valid
  • $router.pushSo are the parameters of the methodRawLocationType, so use$router.pushFor example
// It is a string of characters
this.$router.push('home') / / relative
this.$router.push('/home') / / absolute

// Location as an object
this.$router.push({ path: 'home' })
this.$router.push({ path: '/home' })

this.$router.push({ path: '/home'.query: { test: 3}})/ / qs to carry

this.$router.push({ name: 'home' }) // Name the route
this.$router.push({ name: 'detail'.params: { id: 1}})// Name + parameter

this.$router.push({ params: { id: 1}})// Only with parameters, for the relative jump only parameter change; Relative parameter jump
Copy the code
  • You can seeVueRouterAll the above conditions need to be compatible, and the address needs to be formatted for ease of handling
  • Look at the implementation logic
// src/util/location.js

// Format location
export function normalizeLocation(
  raw: RawLocation, // The original location, a string, or a formatted locationcurrent: ? Route,// The current route object
  append: ?boolean.// Whether it is append moderouter: ? VueRouter/ / VueRouter instance
) :Location {
  let next: Location = typeof raw === 'string' ? { path: raw } : raw // named target // formatted

  if (next._normalized) {
    return next
  } else if (next.name) {
    // Handle named forms, such as {name:'Home',params:{id:3}}
    next = extend({}, raw)
    const params = next.params
    if (params && typeof params === 'object') {
      next.params = extend({}, params)
    }
    return next
  } // relative params // process {params:{id:1}} relative parameter form jump

  if(! next.path && next.params && current) { next = extend({}, next) next._normalized =true
    const params: any = extend(extend({}, current.params), next.params) // Extract the current route field as the next field, because the relative parameter form, only params, must use current to extract some fields

    if (current.name) {
      // Name the form
      next.name = current.name
      next.params = params
    } else if (current.matched.length) {
      // In the form of path, the current path is extracted from the matching record and filled with parameters
      const rawPath = current.matched[current.matched.length - 1].path
      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
  } // Handle path jumps, such as {path:'/test',query:{test:3}} // parse path

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

  const query = resolveQuery(
    parsedPath.query,
    next.query, // Additional qs is required
    router && router.options.parseQuery // Support passing in custom methods to parse query
  ) / / parse the hash

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

  return {
    _normalized: true.// The flag has been formatted
    path,
    query,
    hash,
  }
}
Copy the code
  • First of all tostringType is converted to object form for later unified processing
  • If the address has been formatted, return it directly
  • Then check whether it is a named route
    • If so, copy the original addressrawCopy,params, return directly
  • Handles relative route (relative parameter) jumps that carry only parameters, namelythis.$router.push({params:{id:1}})In the form of
    • The definition of such an address isThere is no path,Only paramsandThe current route object exists
    • The main processing logic is
      • To mergeparams
      • If the route is named, this parameter is usedcurrent.nameAs anext.nameAnd assignmentparams
      • Non-named route: Finds the matched route record from the current route object and retrieves the route recordpathAs anext.pathAnd fill itparams
      • Returns the processed address
    • Due to this jump mode, onlyparams, so the object must be routed from the currentcurrentTo get the available fields (path,name), as its own value, and then jump
  • Processing bypathWay to jump
    • callparsePathfrompathParsing outPath, Query, hash
    • And then tocurrent.pathforbasePath, the resolution (resolve)The final path
    • rightqueryMerge operation
    • righthashPre-append#operation
    • Returns a_normalized:trueIdentification of theLocationobject
  • After the above processing, no matter what address is passed in, return a string with_normalized:trueIdentification of theThe Location typeThe object of
  • normalize-location.png

Check whether the address matches matchRoute

  • We know thatVueRouterDynamic route matching is supported, as shown in the following figure
  • dynamic-route.png
  • What we did in the last videoGenerating Routing RecordsThe chapter also introduces,VueRouterWhen routing records are generated, the route passespath-to-regexpThe package generates a regular extension object and assigns a value to the routing recordregexField, used to obtain subsequent dynamic route parameters
    • The main logic is to provide a dynamic routeuser/:idAnd an address/user/345Through thepath-to-regexpI can generate an object{id:345}To express the mapping of parameters
    • Is a process of extracting parameters from URL by means of dynamic routing./user/345->{id:345}
    • You can view specific examplesGenerating Routing Recordschapter
  • The above logic for extracting parameters is inmatchRouteImplementation of the
  • matchRouteLocated in thesrc/create-matcher.js
// src/create-matcher.js

// Check that path passes the regex match and that the params object is correctly assigned
function matchRoute(regex: RouteRegExp, path: string, params: Object) :boolean {
  const m = path.match(regex)

  if(! m) {// Failed to match
    return false
  } else if(! params) {// If && params does not exist, it can be matched
    return true
  } // Params that match the regulars exist and need to be correctly assigned to params // path-to-regexp will process each dynamic routing marker into a group of regulars, So I start from 1 / / https://www.npmjs.com/package/path-to-regexp / / const keys = []; // const regexp = pathToRegexp("/foo/:bar", keys); // regexp = /^\/foo\/([^\/]+?) / /? $/ I / / : bar was treated to a regular group / / keys = [{name: "bar", the prefix: '/', suffix: "', the pattern: '[# ^ \ \ / \ \?] +?', modifier: '}]

  for (let i = 1, len = m.length; i < len; ++i) {
    const key = regex.keys[i - 1] // regex.keys returns the matched
    const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i]
    if (key) {
      // Fix #1994: using * with props: true generates a param named 0
      params[key.name || 'pathMatch'] = val
    }
  }

  return truee
}
Copy the code
  • The method signature tells you that it returns onebooleanValue, which represents the value passed inpathWhether or not it passesThe regex matching; Although returning onebooleanValue, but inside it does something very important frompathTo extract dynamic routing parameter values, we look at the complete logic
  • First callpath.match(regex)
  • Cannot match direct returnfalse
  • Yes and noneparamsTo return totrue
  • All that’s left is one case where it matches andparamsYes, this is the caseparamsDo the correct assignment
    • The whole assignment is mostly traversalpath.match(regex)Returns the value and retrievesregexStored in thekey, and then assign the values in turn, refer to the comments above for details;
    • aboutregex,path-to-regexp, you can refer toGenerating Routing RecordsChapters andhttps://www.npmjs.com/package/path-to-regexp
  • And then there’s the point, the point of assignmentpathMatchWhat is?
    • This is essentially the same thing as the wildcard*Related to the
    • VueRouterThe special treatment of wildcards can be seenRouter.vuejs.org/zh/guide/es…
      • namelypathMatchRepresents the path to which the wildcard is matched
  • The official example is as follows
{
  // Will match all paths
  path: The '*'
}
{
  // will match any path starting with '/user-'
  path: '/user-*'
}

{path: '/user-*'}
this.$router.push('/user-admin')
this.$route.params.pathMatch // 'admin'
// Give a route {path: '*'}
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'
Copy the code
  • match-route.png

Fill the parameter fillParams

  • fillParamsYou can view it asmatchRouteThe inverse operation is a process of using parameters to generate URL with the help of dynamic path; namely/user/:id+{id:345}->/user/345
  • You can look at the implementation
// src/util/params.js

/ / cache
const regexpCompileCache: {
  [key: string] :Function
} = Object.create(null)

// Fill in the dynamic routing parameters
export function fillParams(
  path: string,
  params: ?Object,
  routeMsg: string
) :string {
  params = params || {}
  try {
    / / the compile is mainly used to inverse resolution, https://www.npmjs.com/package/path-to-regexp#compile-reverse-path-to-regexp
    const filler =
      regexpCompileCache[path] ||
      (regexpCompileCache[path] = Regexp.compile(path)) / / repair/https://github.com/vuejs/vue-router/issues/2505#issuecomment-442353151 / Fix # 2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }} // and fix #3106 so that you can work with location descriptor object having params.pathMatch equal to empty string
    if (typeof params.pathMatch === 'string') params[0] = params.pathMatch // Return the path after the inverse parsing

    return filler(params, { pretty: true})}catch (e) {
    if(process.env.NODE_ENV ! = ='production') {
      // Fix #3072 no warn if `pathMatch` is string
      warn(
        typeof params.pathMatch === 'string'.`missing param for ${routeMsg}${e.message}`)}return ' '
  } finally {
    // delete the 0 if it was added
    delete params[0]}}Copy the code
  • You can see that the whole inverse analytic logic is aided byRegexp.compileIn combination withregexpCompileCacheImplementation of the
  • Regexp.compileTo receive aDynamic Routing path, returns a function that can be used to do inverse parsing;
  • Regexp.compileExamples are as follows
// https://www.npmjs.com/package/path-to-regexp#compile-reverse-path-to-regexp

const toPath = compile('/user/:id')
toPath({ id: 123 }) //=> "/user/123"
Copy the code
  • You can see first of all, yesRegexp.compileThe returned function is cached
  • thenmatchRouteAdd thepathMatchAssigned toparams[0]
  • callRegexp.compileReturns the function toparamsIs the input parameter, inverse parsingurlAnd return
  • Delete addedparams[0]
  • fill-params.png

Create the route object _createRoute

  • No matter on the topnormalizeLocation,matchRoute,fillParamsIt’s all for incomingaddressDo something;
  • whilematchThe route object () method is used to find the route object that matches the address_createRouteMethod implementation
  • As the naming suggests, this is an internal method
// SRC /create-matcher. Js createMatcher

function _createRoute(record: ? RouteRecord, location: Location, redirectedFrom? : Location) :Route {
  // Route records are marked for redirection
  if (record && record.redirect) {
    return redirect(record, redirectedFrom || location)
  } // Route records are marked as alias routes, as shown in create-route-map.js

  if (record && record.matchAs) {
    return alias(record, location, record.matchAs)
  } // Normal route record

  return createRoute(record, location, redirectedFrom, router)
}
Copy the code
  • You can see that it takes three arguments
    • recordUsed to generateThe Route objectsDestination route record of
    • locationThe target address
    • redirectedFromThe source address of the redirect. This parameter has a value only when the redirect occurs
  • As we know, different tag fields will be added to different types of records when routing records are added
    • If there are records added to the redirectredirectfield
    • Add an alias routematchAsfield
      • You can see in the frontGenerating Routing Recordschapter
  • You can see that different methods are called for different routing record types
    • Redirect a route callredirectmethods
    • Alias routing callaliasmethods
    • Rest of the callscreateRoutemethods
  • The activity diagram is as follows
  • create-route-inner.png
  • Actually,redirect,aliasMethod is also called internallycreateRoutemethods
  • So let’s seecreateRouteMethod implementation

createRoute

  • createRouteLocated in thesrc/util/route.js
// src/util/route.js

/ / generates the Route
export function createRoute(record: ? RouteRecord, location: Location, redirectedFrom? :? Location, router? : VueRouter) :Route {
  const stringifyQuery = router && router.options.stringifyQuery // Support passing in custom serialized QS methods
  let query: any = location.query || {}

  try {
    query = clone(query) // location.query is a reference value to avoid mutual influence and carry out deep copy
  } catch (e) {} / / generates the Route

  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), / / a complete path
    matched: record ? formatMatch(record) : [], // Obtain all matched routing records
  } // If the route is redirected from another pair, record the address before the redirection

  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  } // Prevent tampering

  return Object.freeze(route)
}
Copy the code
  • Due to theVueRouterSupport for incoming customizationSerialize the queryStringMethod, so the first step is to get serializationqueryStringThe method of
  • Then thequeryMade a deep copy to avoid interaction
  • The next step is to make newRouteobject
  • If it is redirected from another route, the full redirected source address is generated and assigned to the newly generated oneRouteobject
  • The last callObject.freezeA freeze on newRouteObject, becauseRouteThe object isimmutablethe
  • The whole process is as follows
  • create-route.png
  • You can see the generationRouteIs calledgetFullPathTo generate a completefullPath
// src/util/route.js

// Get the full path
function getFullPath({ path, query = {}, hash = ' ' }, _stringifyQuery) :string {
  const stringify = _stringifyQuery || stringifyQuery
  return (path || '/') + stringify(query) + hash
}
Copy the code
  • You can seegetFullPathIs in thepathAppendqsandhash
  • In addition to generateRouteIs calledformatMatchIn order to getallThe associatedRouting record
  • You can search up all associated routing records
// src/util/route.js

// Format the matched route record. If a route record matches, the parent route record must also match
// /foo/bar matches, then its parent route object /foo must also match
function formatMatch(record: ? RouteRecord) :Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record) // The queue header is added, so the parent record is always in front and the current record is always last; Used in the router-view component to get a matching Route record
    record = record.parent
  }
  return res
}
Copy the code
  • Don’t aRouteIt doesn’t correspond to oneRoute objects?
  • Actually in the glossary introductionRoute Routing objectWhen, also mentioned, aRoute Routing objectThere may be multiple associationsRouteRecord indicates the RouteRecord object
  • This is because there are nested routes. When the child route record is matched, it means that the parent route record must also be matched. See the following example
// When there are the following routing rules
routes: [{path: '/parent'.component: Parent,
    children: [{ path: 'foo'.component: Foo }],
  },
]
Copy the code
  • access/parent/foo“, two route records are matched
  • match-demo-record.png
  • And the exact match to the routing record must be the last one, so we’ll see laterroute.matched[route.matched.length - 1]To get the currentrouteCorresponding to an exact matchRouteRecord
  • After watchingcreateRouteLet’s seealiasThe implementation of the

Create an alias routing object alias

  • aliasLocated in thesrc/create-matcher.js
// src/create-matcher.js

// Create an alias Route
function alias(
  record: RouteRecord,
  location: Location,
  matchAs: string
) :Route {
  // Get the full path to the alias
  const aliasedPath = fillParams(
    matchAs,
    location.params,
    `aliased route with path "${matchAs}"`
  ) // Get the original Route matching the alias

  const aliasedMatch = match({
    _normalized: true.path: aliasedPath,
  })
  if (aliasedMatch) {
    const matched = aliasedMatch.matched
    const aliasedRecord = matched[matched.length - 1] // Find the last matched route record, that is, the current matched route record, as shown in the route.js formatMatch method
    location.params = aliasedMatch.params
    return _createRoute(aliasedRecord, location)
  }

  return _createRoute(null, location)
}
Copy the code
  • Logic is as follows
  • Take firstmatchAsgetaliasedPath.
  • Then pickaliasedPathWalk againmatchgetaliasedMatchRoute objects
  • aliasedMatchIf so, takealiasedMatchPrecisely matchedRoute record objectandlocationTo generate theRoute objectsreturn
  • If it does not exist, create a new oneRoute objectsreturn
  • It might be a little convoluted, but let’s do an example
    • And as we already know,/aAlias set/b, two route records are generated, and/bOn the routing record ofmatchAsfor/a
      • The ones you forget can look aheadGenerate alias routing recordschapter
    • Incoming herealiasthematchAsIs equivalent to/aFirst, takematchAsnamely/aGet filled inparamsThe path of the
    • Call it from this pathmatchFind a matchRoute objectsAnd remember torouteA
    • As mentioned earlier, routing objects are associated with routing records, so fromrouteACan obtain accurate matching routing recordsrouteRecordA
    • Take this routing record and/bthelocationTo generate the routing object and return
    • This is what the official website saysThe alias for /a is /b, which means that when a user accesses /b, the URL remains /b, but the route matches/A, just as if the user accesses /a.The effect
  • The activity diagram is as follows
  • alias.png
  • Let’s seeredirectThe implementation of the

Create a redirect for a redirected route object

  • So let’s take a lookrecord.redirectSeveral possible scenarios;https://router.vuejs.org/zh/guide/essentials/redirect-and-alias.html# redirect
    • string{redirect:'/'}
    • object{redirect:{path:'/test'}},{redirect:{name:'Test'}}
    • Passing in functions is also supported{redirect:to=>{ return {name:'Test'}}}
  • Let’s look at this firstredirectMethod entry
// SRC /create-matcher.js _createRoute inside the method

if (record && record.redirect) {
  return redirect(record, redirectedFrom || location)
}
Copy the code
  • Because multiple redirection scenarios exist, retain the address that triggers redirection for the first timeredirectedFrom
    • /a -> /b -> /cIn the/cTo retain the address that triggers redirection for the first time/a
  • How to retain the address that triggered redirection for the first time?
    • On the first redirect,redirectedFromThere is no value
    • inredirectThe method inside willlocationAs aredirectedFromParameters of the callmatchMethod,matchIf it is found that the redirection is still needed, the call continuesredirectAt this time,redirectedFromIt has a value. It’s the first one passed inlocation, in order to complete the transfer of the initial address
  • Look at the example below
;[
  { path: '/foo'.component: Foo },
  { path: '/baz'.component: Baz, redirect: '/foo' }, // named redirect
  { path: '/named-redirect'.redirect: '/baz'},]// SRC /create-matcher.js _createRoute inside the method
if (record && record.redirect) {
  console.count('redirect count:') // Count the number of calls
  console.log('record:', record, 'redirectedFrom:', redirectedFrom) // Prints the initial source address for redirection
  return redirect(record, redirectedFrom || location)
}
Copy the code
  • When we visit/named-redirectRoute (trigger route forward) is redirected to/baz./bazIt’s going to redirect to/foo, the final displayFooComponents; So,redirectThe method should be called twice;
  • We can look at the output of the example above
  • redirect-demo.png
  • Will findredirectThe method is called four times, the first two times due to route jumpsredirectThe last two are triggered by the need to resolve the route while the component is renderingredirectCall;
  • You can compare the call stack
    • redirect-stack-1.png
    • redirect-stack-2.png
    • You can see the first and second onesredirectIs made up oftransitionToThe trigger
    • redirect-stack-3.png
    • redirect-stack-4.png
    • The third and fourth are component renderingsrendercallresolveThe trigger
      • Why does component rendering need to parse routes, as explained in the component section below
  • You can see the first callredirectfrom/named-redirectRedirected to/bazAt this time,redirectFromIt has no value
  • And the second call is from/bazRedirected to/fooAt this time,redirectFromThat’s the address that triggered the first redirect/named-redirect
  • And ultimately$routeThere will also be one onredirectFromThe address that triggered the first redirect is preserved
  • We just looked at itredirectFromLet’s seeredirectConcrete implementation of
// SRC /create-matcher. Js createMatcher
// Create a redirect Route
function redirect(
  record: RouteRecord, // Records of routes that trigger redirection (including redirect)
  location: Location // The initial address to trigger the redirection ()
) :Route {
  const originalRedirect = record.redirect
  let redirect =
    typeof originalRedirect === 'function' // redirect supports incoming functions; https://router.vuejs.org/zh/guide/essentials/redirect-and-alias.html# redirect
      ? originalRedirect(createRoute(record, location, null, router))
      : originalRedirect // redirect returns a path, such as '/bar'

  if (typeof redirect === 'string') {
    redirect = { path: redirect }
  } // The originalRedirect function returns a non-string, non-object value with a warning and creates an empty Route

  if(! redirect ||typeofredirect ! = ='object') {
    if(process.env.NODE_ENV ! = ='production') {
      warn(false.`invalid redirect option: The ${JSON.stringify(redirect)}`)}return _createRoute(null, location)
  } // Redirect must be an object at this point

  const re: Object = redirect
  const { name, path } = re
  let { query, hash, params } = location

  query = re.hasOwnProperty('query')? re.query : query hash = re.hasOwnProperty('hash')? re.hash : hash params = re.hasOwnProperty('params')? re.params : params// Redirection is a named route

  if (name) {
    // resolved named direct
    const targetRecord = nameMap[name] // No named route warning found

    if(process.env.NODE_ENV ! = ='production') {
      assert(targetRecord, `redirect failed: named route "${name}" not found.`)}return match(
      {
        _normalized: true,
        name,
        query,
        hash,
        params,
      },
      undefined,
      location
    )
  } else if (path) {
    // Redirects are in the form of path
    // 1. Resolve relative redirect
    const rawPath = resolveRecordPath(path, record) // 2. Resolve params, populate params
    const resolvedPath = fillParams(
      rawPath,
      params,
      `redirect route with path "${rawPath}"`
    ) // 3. Rematch with existing query and hash
    return match(
      {
        _normalized: true.path: resolvedPath,
        query,
        hash,
      },
      undefined,
      location
    )
  } else {
    if(process.env.NODE_ENV ! = ='production') {
      warn(false.`invalid redirect option: The ${JSON.stringify(redirect)}`)}return _createRoute(null, location)
  }
}
Copy the code
  • You can see first of all, yesrecord.redirectStandardized, unified generation of oneredirectObject (redirect target)
    • Now, why do we normalize, as I mentioned earlier,redirectSupports string, object, and function types, so you need to standardize them for unified processing
  • And then it takes precedenceredirectthequery hash paramsValue to domatch, will only be taken if it does not existInitial address locationthequery hash params
  • And then it decides what the redirection target isNamed formorPath form
  • Named form
    • To determinenameMapIf there is no target routing record, interrupt and give a hint;
    • Then retracematchProcess, and willlocationAs aredirectedFromPass in, and you’re doneredirectedFromTransfer closed loop of
    • matchIt will continue to determine if there is a redirect, so that multiple multiple redirect scenarios are covered
  • Path form
    • takepathMatch, need to get the full path, so first fromrecordPull out the original pathrawPathAnd fill in what was resolved earlierparamsGet the full address
    • Take the full address and go backmatchThe process will also belocationAs aredirectedFromPass in, doneredirectedFromTransfer closed loop of
    • matchIt will continue to determine if there is a redirect, so that multiple multiple redirect scenarios are covered
  • If neitherNamed formIs notPath formDirectly create a new routing object to return
  • The process is as follows
  • redirect-full.png

summary

  • The process of route matching is actually takingAddress RawLocationgenerateRoute object RouteIn betweenRoute records RouteRecordIt acts as an intermediate bridge because the routing record contains important information about the generated routing object. So the process should be to takeaddressFind the corresponding route in the routing mapping tableRouting recordAnd then takeRouting recordgenerateRoute objects
  • The above matching logic is mainly composed ofmatchFunction implementation, the key logic containsAddress formatting normalizeLocation,Check whether the address matches matchRoute,Fill the parameter fillParams,Create the route object _createRoute
  • innormalizeLocation, torawLocationTo standardize and facilitate subsequent processing
  • inmatchRouteWith the help ofpath-to-regexpCheck if the address matches and extractparams
  • fillParamsYou can view it asExtract the paramsReverse operation, mainly used to fill the dynamic part of the address
  • in_createRoute, alias, redirection, multiple direction, and other scenarios are handled separately
  • Through the above process, you can getRawLocationThe correspondingRoute
  • getRoute, we can parse the navigation

Navigate the parse (validation) process

  • I mentioned it beforetransitionToMethod is calledmatchMethod to get the targetRouteAfter that, it will be calledconfirmTransitionMethod to do navigation parsing
  • We know thatvue-routerVarious hooks, guard functions are triggered sequentially when routing jumps, for exampleBeforeRouteLeave, beforeRouteEnter, etc;
  • First of all, these hooks and guards are defined invue-routerOn the instance, some are route exclusive, some are located.vueComponent, so the first step must be to extract the hooks and guard functions together
  • Secondly, the hooks and guards are executed sequentially, so you need to design a queue and iterator to ensure that they are executed sequentially
  • Finally, there are special scenarios to deal with, such as how do asynchronous routing components ensure sequential execution
  • The relevant logic above is encapsulated inconfirmTransitionIn the
  • confirmTransitionMethods are defined insrc/base.jsIn the
// SRC /base.js History class

// Confirm the route redirect
confirmTransition (/* to*/route: Route, onCompleteFunction, onAbort? :Function) {
  const current = this.current /* from */

  / / cancel
  const abort = err= > {
    // after merging https://github.com/vuejs/vue-router/pull/2771 we
    // When the user navigates through history through back/forward buttons
    // we do not want to throw the error. We only throw it if directly calling
    // push/replace. That's why it's not included in isError
    if(! isExtendedError(NavigationDuplicated, err) && isError(err)) {if (this.errorCbs.length) {
        this.errorCbs.forEach(cb= > {
          cb(err)
        })
      } else {
        warn(false.'uncaught error during route navigation:')
        console.error(err)
      }
    }
    onAbort && onAbort(err)
  }

  // Same Route, a duplicate error is reported
  if (
    isSameRoute(route, current) &&
    // in the case the route map has been dynamically appended to
    // Prevent the route map from being dynamically changed
    route.matched.length === current.matched.length
  ) {
    // ensureURL is implemented by subclasses that determine whether to add or replace a record based on passing parameters
    this.ensureURL() // Replace the current history
    return abort(new NavigationDuplicated(route))
  }

  // Compare the route RouteRecord to find the route records that need to be updated, deactivated, or activated
  const { updated, deactivated, activated } = resolveQueue(
    this.current.matched,
    route.matched
  )

  // Generate a queue of guards and hooks to execute
  const queue: Array<? NavigationGuard> = [].concat(// in-component leave guards
    extractLeaveGuards(deactivated), // Extract all beforeRouteLeave guards in the routing component
    // global before hooks
    this.router.beforeHooks, // Global beforeEach guard
    // in-component update hooks
    extractUpdateHooks(updated), // Extract all beforeRouteUpdate guards in the routing component
    // in-config enter guards
    activated.map(m= > m.beforeEnter), // Exclusive beforeEnter guard for routing
    // async components
    resolveAsyncComponents(activated)// Parse asynchronous components
  )

  this.pending = route // Record the route to jump to, easy to cancel the comparison

  // Iterate over the function
  const iterator = (hook: NavigationGuard, next) = > {
    if (this.pending ! == route) {// When to is found to have changed, it needs to be cancelled
      return abort()
    }
    try {
      hook(/* to*/route, /* from*/current, /* next*/(to: any) = > {
        if (to === false || isError(to)) {
          // next(false) -> abort navigation, ensure current URL
          // next(false) -> Cancel the jump and add a new record (but not add record because the URL has not changed)
          this.ensureURL(true)
          abort(to)
        } else if (
          typeof to === 'string' || // next('/')
          (typeof to === 'object' &&
            (typeof to.path === 'string' || typeof to.name === 'string')) / / next ({path: '/'}) or next ({name: 'Home'})
        ) {
          // next('/') or next({ path: '/' }) -> redirect
          abort() // Cancel current
          if (typeof to === 'object' && to.replace) {
            // Call the replacement record of the subclass method
            this.replace(to)
          } else {
            // Call the subclass method to add records
            this.push(to)
          }
        } else {
          // confirm transition and pass on the value
          // next()
          next(to)
        }
      })
    } catch (e) {
      abort(e)
    }
  }

  // Execute queue
  runQueue(queue, iterator, /* Execute the end callback */() = > {
    const postEnterCbs = [] // Save the callback passed to Next in beforeRouteEnter
    const isValid = () = > this.current === route // Indicates that the jump ends

    // wait until async components are resolved before
    // extracting in-component enter guards
    const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) // Wait for the asynchronous component to finish parsing, then extract the beforeRouteEnter guard within the component
    const queue = enterGuards.concat(this.router.resolveHooks)// beforeResolve hooks
    runQueue(queue, iterator, /* Execute the end callback */() = > {
      if (this.pending ! == route) {return abort()
      }

      this.pending = null

      onComplete(route) // Execute the onComplete callback, which calls the updateRoute method and internally fires the afterEach hook

      if (this.router.app) {
        this.router.app.$nextTick(() = > {
          // Call the callback passed to Next in the beforeRouteEnter guard
          // next(vm=>xxx)
          postEnterCbs.forEach(cb= > {
            cb()
          })
        })
      }
    })
  })
}
Copy the code
  • We can look at the method signature first
    • routeDestination route object. The destination to be resolvedtoObject, andcurrentCan be understood asfromObject.
    • onCompleteThe jump completes the callback
    • onAbortCancel, wrong callback
  • Let’s look at the main logic
    • The problem of repeated jumps is dealt with first
    • Then, the route records that need to be updated, deactivated, or activated are found by comparison
    • The corresponding hook and guard functions are extracted from the above three routing records
    • Queues and executes the hook and guard functions
  • Next, let’s look at the logic in turn

Judge repeated jumps

  • Defined before determining duplicate jumpsabortMethod, it’s mainly rightonAbortMethod made a layer of packaging; This method is called when the navigation is cancelled
    • It receives aerrParameter if there is a registration error callback anderrFor theNavigationDuplicatedErrors are traversederrorCbsList performs the error callback within it
    • The last callonAbortCall back and pass inerrThe parameters are handled externally
  • Next, determine whether to repeat the jump, mainly usingisSameRouteCheck whether the current route object and the destination route object are the same. If they are the same and the number of matching route records is the same, they are regarded as repeated hopsabortMethod and pass inNavigationDuplicatedError and terminate the process
  • isSameRouteMainly judgePath, name, Hash, Query, paramsIf the key information is the same, the route object is the same
// src/util/route.js

// Whether the route is the same
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) {
    // If path, hash, and query are the same, check whether they are the same
    return (
      a.path.replace(trailingSlashRE, ' ') ===
        b.path.replace(trailingSlashRE, ' ') &&
      a.hash === b.hash &&
      isObjectEqual(a.query, b.query)
    )
  } else if (a.name && b.name) {
    // If name, hash, query, and params are the same
    return (
      a.name === b.name &&
      a.hash === b.hash &&
      isObjectEqual(a.query, b.query) &&
      isObjectEqual(a.params, b.params)
    )
  } else {
    return false}}Copy the code
  • Note that subclasses are still called after a repeat jump is determinedensureURLMethod to updateurl

Compare the route records that need to be updated, deactivated, or activated

  • After determining the repeat jump, you need to comparefrom,toRoute object to find out which route records need to be updated, which need to be deactivated, and which need to be activated for subsequent extraction of hooks and guard functions
// SRC /history/base.js confirmTransition method
// Compare the route RouteRecord to find the route records that need to be updated, deactivated, or activated
const { updated, deactivated, activated } = resolveQueue(
  this.current.matched,
  route.matched
)
Copy the code
  • You can see that the logic is wrapped inresolveQueueMethod, a record list of the current and destination routing objects is passed in, deconstructed from the return valueupdated, deactivated, activated
  • Look at theresolveQueueimplementation
// Compare the curren and next routing records lists to find the routing records that need to be updated, deactivated, or activated
function resolveQueue(
  current: Array<RouteRecord>,
  next: Array<RouteRecord>
) :{
  updated: Array<RouteRecord>
  activated: Array<RouteRecord>
  deactivated: Array<RouteRecord>
} {
  let i
  const max = Math.max(current.length, next.length) // Find the first unequal route record index
  for (i = 0; i < max; i++) {
    if(current[i] ! == next[i]) {break}}/ / eg / / current: [1, 2, 3] / / next: [1, 2, 3, 4, 5] / / I for 3 / / need to be updated for [1, 2, 3] / / need to activate for inactivation (4, 5) / / need to []

  return {
    updated: next.slice(0, i), // The left side of the index needs to be updated
    activated: next.slice(i), // The right side of the index needs to be activated
    deactivated: current.slice(i), // The right side of the index needs to be deactivated}}Copy the code
  • The logic is simple
    • First of all find outcurrentandnextThe maximum length of the list,
    • Then the maximum number of loops is used to find the first unequal routing record index
    • With this index as the dividing line,Next listThe route record to be updated is on the left of the index, and the route record to be activated is on the right of the index
    • The current listThe index and the right side are the route records to be deactivated
  • For example,
  • currentTo:[1, 2, 3],nextfor[1, 2, 3, 4, 5], the current route object containsOne, two, threeThe destination routing object contains three routing recordsOne, two, three, four, fiveFive Routing Records
  • After the calculationmaxfor5
  • Loop, find the first unequal index is3
  • So what needs to be updated isNext, slice (0, 3)namelyOne, two, three
  • What needs to be activated isnext.slice(3)namely4, 5,
  • What needs to be inactivated iscurrent.slice(3)Nothing to inactivate
  • By identifying the route records that need to be updated, activated, and deactivated, we can extract the corresponding hook and guard functions from them

Extract hooks, guard functions, and parse asynchronous components

  • So first of all, let’s sort this outvue-routerWhat are the hooks and guard functions
    • router.beforeEachGlobal front guard
    • router.beforeResolveGlobal Parsing Guard (new in V2.5.0)
    • router.afterEachGlobal post-hook
    • RouteConfig.beforeEnterRoute exclusive guard
    • vm.beforeRouteEnterVue component internal route entry guard
    • vm.beforeRouteUpdateVue Component Routing Update Guard (new in V2.2)
    • vm.beforeRouteLeaveVue component routing away guard
  • You can see some of them are definitionsVueRouterSome are defined in configuration rulesRouteConfigSome are defined inRouteComponent Routing componentOn the
    • The hooks and guards of the first two are easy to get because we are inThe History classHolding theVueRouterInstance, easily accessible to the guards and hooks and executed with little extra processing;
    • The only thing that’s hard to deal with is the definitionRouteComponent Routing componentThe guard function inRouteRecordGet all theRouteComponent Routing componentAnd extract the corresponding guard from it, and finally bind its context to ensure that the execution result is correct;
  • In the last video, we got the ones that need to be updated, activated, and deactivatedRouteRecord Indicates route recordsLet’s take a look at which guards to extract
  • deactivatedExtract thebeforeRouteLeave
  • updatedExtract thebeforeRouteUpdate
  • activatedExtract thebeforeRouteEnter, there is a special scenario where the asynchronous route component can be extracted only after the parsing of the asynchronous route component is completebeforeRouteEnterGuards, we’ll talk about that later
  • Let’s take a look at the extracted entry code
// SRC /history/base.js confirmTransition method
// Generate a queue of guards and hooks to execute
const queue: Array<? NavigationGuard> = [].concat(// in-component leave guards
  extractLeaveGuards(deactivated), // Extract all beforeRouteLeave guards from the routing component // global before hooks
  this.router.beforeHooks, // Global beforeEach guard // in-Component update hooks
  extractUpdateHooks(updated), // Extract all beforeRouteUpdate guards in the routing component // in-config Enter Guards
  activated.map((m) = > m.beforeEnter), // Route exclusive beforeEnter guard // async Components
  resolveAsyncComponents(activated) // Parse asynchronous components
)
Copy the code
  • You can see that a queue is definedqueue
  • I did the following in turn
    • Extract thedeactivatedIn thebeforeRouteLeaveThe guards
    • To obtain theVueRouterDefined on the instancebeforeEachThe guards
      • beforeEachGuards are defined directly inVueRouterThe instance
    • fromupdatedExtract thebeforeRouteUpdateThe guards
    • fromactivatedTo obtain the exclusive routebeforeEnterThe guards
      • beforeEnterGuards were originally defined inRouteConfigIs passed to the routing record, so it can be directly obtained from the routing record
    • parsingactivatedAsynchronous routing component in
      • Routing Component Supportimport()Dynamic import, so it’s going to be handled here
  • So let’s start with a method with a very similar nameextractLeaveGuardsandextractUpdateHooks

ExtractLeaveGuards, extractUpdateHooks

  • Both are locatedsrc/base.js
// src/base.js

// Pass in the list of route records, extract the beforeRouteLeave guard and output it in reverse order
function extractLeaveGuards(deactivated: Array<RouteRecord>) :Array<?Function> {
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)}// Pass in the list of routing records and extract the beforeRouteUpdate hook
function extractUpdateHooks(updated: Array<RouteRecord>) :Array<?Function> {
  return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}
Copy the code
  • You can see that both are called internallyextractGuards, the former passes an extra parametertrue
  • Let’s seeextractGuards
// src/base.js

// Extract the guard
function extractGuards(
  records: Array<RouteRecord>,
  name: string.// The name of the guard to extract
  bind: Function.// Bind the guard context functionreverse? :boolean // Whether to reverse the sequence
) :Array<?Function> {
  const guards = flatMapComponents(records, (
    def /* Routing component definition */,
    instance / * * / router - the view instance,
    match /* Route record */,
    key /* View name */
  ) = > {
    const guard = extractGuard(def, name) // Extract the way out by the guard function in the component // bind the context for the guard
    if (guard) {
      return Array.isArray(guard)
        ? guard.map((guard) = > bind(guard, instance, match, key))
        : bind(guard, instance, match, key)
    }
  }) // Flat + reverse
  return flatten(reverse ? guards.reverse() : guards)
}
Copy the code
  • Take a look at the method signature
    • Receives an array of routing recordsrecords
      • namelyextractLeaveGuardsIn the incomingdeactivatedRouting record array;extractUpdateHooksIn the incomingupdatedRouting record array
    • Receives a guard name to extractname
      • beforeRouteLeaveandbeforeRouteUpdatestring
    • A function that binds above and below a guardbind
      • extractLeaveGuards,extractUpdateHooksIt’s all passingbindGuardMethod, which we parse below
    • And whether one needs to output in reverse orderreverseBoolean value; Optional parameter;
      • extractLeaveGuardsThe relay will betrue, represents that the returned array (guard function array) needs to be output in reverse order;
    • Returns aitemisFunctionAn array of
  • Look at the internal logic
    • callflatMapComponentsThe incomingrecordsAnd a receiverdef, instance, match, keyParameter arrow function, returns oneguardsThe guards array
    • Then according to thereverseTo decide if it’s rightguardsThe array is reversed
      • Why reverse order?
      • increateRouteAs mentioned in the chapter, routing records are stored in reverse order. The accurately matched route records are placed at the end of the array (length - 1Location), parent record first
      • Some guard functions need to be executed in reverse order, for examplebeforeRouteLeave, it needs to be called on the accurately matched routing component first, and then on the parent component
    • The last callflattenwillguardsflat
  • Look at the firstflatMapComponentsimplementation
// src/util/resolve-components.js

// Flatten the routing component in the routing record
export function flatMapComponents(
  matched: Array<RouteRecord>, // Route record array
  fn: Function // The callback function
) :Array<?Function> {
  return flatten(
    matched.map((m) = > {
      return Object.keys(m.components).map((key) = >
        fn(
          m.components[key], // The routing component definition corresponding to the named view; Generally corresponding to fn's entry argument def
          m.instances[key], // router-view instance; Generally, it corresponds to the input parameter _ or instance of fn
          m, // Matched route record; Generally, it corresponds to the match of fn's input parameter
          key // Name the view key; Generally, it corresponds to the input key of FN))}))}Copy the code
  • You can see that it receives an array of routing recordsmatchedAnd a functionfn, returns a passflattenArray to process
    • matchedThat’s what we passed inrecords
    • fnIs to receivedef, instance, match, keyParameter arrow function
  • This method essentially iterates through each routing component in the routing record and calls the external functions in turn as an input parameterfn, returns the result fromfnFunction determines, and finally outputs the result array flat
    • This method is also used when parsing asynchronous components
  • It’s going to be on the incomingrecordscallmapMethod, and iterate through eachrecordThe definition ofcomponentsField and paircomponentsagainmapTraversal, and then call the incomingfn.mapAs a result,fnReturn result
  • componentsFields are used to define named views, like the following, where key is the view name and value is the routing component
components: {
        default: Foo,
        a: Bar,
        b: Baz
}
Copy the code
  • So the incoming fn, the incoming fndef, instance, match, keyThe arrow of the argument function has four arguments
    • defThe correspondingm.components[key]The routing component definition (Foo, Bar, Baz)
    • instanceThe correspondingm.instances[key]isrouter-viewComponent instance, about routing records androute-viewHow do they relate to each otherThe view componentsWhen parsing
    • mThis corresponds to the current route record traversed
    • keyIs the name of the view currently traversed
  • The general logic is as follows
  • flat-map-components.png
  • Let’s look at the logic inside the arrow function
const guards = flatMapComponents(records, (
  def /* Routing component definition */,
  instance / * * / router - the view instance,
  match /* Route record */,
  key /* View name */
) = > {
  const guard = extractGuard(def, name) // Extract the way out by the guard function in the component // bind the context for the guard
  if (guard) {
    return Array.isArray(guard)
      ? guard.map((guard) = > bind(guard, instance, match, key))
      : bind(guard, instance, match, key)
  }
})
Copy the code
  • First callextractGuardThe mapping is extracted directly from the routing component definitionnameGuard function of
  • And then we call passextractGuardsthebindMethod is to guard the binding context
  • We look at theextractGuardimplementation
// src/base.js

// Extract a single guard
function extractGuard(
  def: Object | Function,
  key: string
) :NavigationGuard | Array<NavigationGuard> {
  if (typeofdef ! = ='function') {
    // extend now so that global mixins are applied.
    def = _Vue.extend(def)
  }
  return def.options[key]
}
Copy the code
  • There are two main pieces of logic
    • callextendTo apply global mixins
    • Returns the corresponding guard function
  • After extracting a single guard, you need to call the incoming onebindMethod to which the context is bound;
  • bindAnd the way to do that isbindGuard
// src/history/base.js

// Bind the guard context to the vue instance (routing component)
function bindGuard(guard: NavigationGuard, instance: ? _Vue): ?NavigationGuard {
  if (instance) {
    return function/* Already bound context guard function */boundRouteGuard() {
      return guard.apply(instance, arguments)}}}Copy the code
  • With the context binding above, the guard function extracted from the routing component is executed back in the routing component context, ensuring that the guard function returns the correct result no matter where it is called
  • summary
  • extractGuardsThe main work is to extract the guard function from the routing component and bind the context to it
  • extract-guards.png
  • Next we parse the active routing record for the asynchronous component
  • Mainly throughresolveAsyncComponentsMethod implemented

resolveAsyncComponents

  • Before we look at how to parse asynchronous components, let’s take a lookvueWhat does an asynchronous component look like in?
/ / https://cn.vuejs.org/v2/guide/components-dynamic-async.html# asynchronous components

/ / receive the resolve, reject
Vue.component('async-example'.function (resolve, reject) {
  setTimeout(function () {
    // Pass the component definition to the 'resolve' callback
    resolve({
      template: '
      
I am async!
'
,}}),1000)})// use in conjunction with require Vue.component('async-webpack-example'.function (resolve) { // This particular 'require' syntax will be told to Webpack // Automatically splits your build code into multiple packages // Will be loaded via Ajax request require(['./my-async-component'], resolve) }) // used in conjunction with import() Vue.component( 'async-webpack-example'.// This dynamic import returns a 'Promise' object. () = > import('./my-async-component'))// Local registration new Vue({ // ... components: { 'my-component': () = > import('./my-async-component'),}})// with load status const AsyncComponent = () = > ({ // The component to load (should be a 'Promise' object) component: import('./MyComponent.vue'), // The component used when the asynchronous component is loaded loading: LoadingComponent, // The component used when loading failed error: ErrorComponent, // Display the component delay time when loading. The default is 200 (ms) delay: 200.// If a timeout is provided and the component load times out, // the component used when the load failed is used. The default value is' Infinity ' timeout: 3000,})Copy the code
  • The documentation describes asynchronous components asVue allows you to define your component as a factory function that asynchronously parses your component definition
  • An asynchronous component is a factory function, within a functionResolve, rejectComponent definition, return onePromiseReturns an object with a specific identification field
  • The asynchronous component code in the parse routing record is locatedsrc/util/resolve-components.js
// src/util/resolve-components.js

// Parse the asynchronous component and return a function that takes to, from, and next parameters
export function resolveAsyncComponents(matched: Array<RouteRecord>) :Function {
  return (to, from, next) = > {
    let hasAsync = false
    let pending = 0
    let error = null
    flatMapComponents(matched, (
      /* Routing component definition */ def,
      / * * / router - the view instance _,
      /* Route record */ match,
      /* View name */ key
    ) = > {
      // if it's a function and doesn't have cid attached,
      // assume it's an async component resolve function.
      // we are not using Vue's default async resolving mechanism because
      // we want to halt the navigation until the incoming component has been
      // resolved.
      // def.cid identifies the instance constructor; https://github.com/vuejs/vue/search?q=cid&unscoped_q=cid
      // If the component is defined as a function and the component CID is not set, it is considered an asynchronous component
      if (typeof def === 'function' && def.cid === undefined) {
        hasAsync = true
        pending++ / / parsing

        const resolve = once((resolvedDef) = > {
          // The loaded component definition is an ESM
          if (isESModule(resolvedDef)) {
            resolvedDef = resolvedDef.default
          } // Save resolved on async factory in case it's used elsewhere // Keep async factory functions for later use
          def.resolved =
            typeof resolvedDef === 'function'
              ? resolvedDef
              : _Vue.extend(resolvedDef)
          match.components[key] = resolvedDef // Replace the component in the named view of the routing record
          pending--
          if (pending <= 0) {
            // All asynchronous components are loaded
            next()
          }
        }) / / an error
        const reject = once((reason) = > {
          const msg = `Failed to resolve async component ${key}${reason}`process.env.NODE_ENV ! = ='production' && warn(false, msg)
          if(! error) { error = isError(reason) ? reason :new Error(msg)
            next(error)
          }
        }) / / asynchronous components, https://cn.vuejs.org/v2/guide/components-dynamic-async.html# asynchronous components

        let res
        try {
          res = def(resolve, reject) / / return promise
        } catch (e) {
          reject(e)
        }
        if (res) {
          if (typeof res.then === 'function') {
            res.then(resolve, reject)
          } else {
            // new syntax in Vue 2.3
            // Handle the load state, return a package object; https://cn.vuejs.org/v2/guide/components-dynamic-async.html# processing load condition
            const comp = res.component // is loaded with import() and returns a promise
            if (comp && typeof comp.then === 'function') {
              comp.then(resolve, reject)
            }
          }
        }
      }
    }) // There is no asynchronous component, just next

    if(! hasAsync) next() } }Copy the code
  • You can see that it receives an array of routing recordsmatched, returns a receiveFrom, to, nextInside is the parsing logic for asynchronous components
  • resolveAsyncComponentsIt does not perform the logic to parse the asynchronous component because it only returns a function that will be called when the queue is run and the asynchronous component is parsed
  • So the execution of the queue, we’ll look at that later, but what’s the logic inside the function that’s returned
    • Defines an identity field for asynchronous componentshasAsync, and the number of asynchronous components currently waiting to be resolvedpending
    • And then calledflatMapComponentsgetrecordsAnd calls the incoming callback methods in turn
    • The callback method will receive the route component that is traversed. In this case, you need to check whether the route component is asynchronous. If yes, start parsing the asynchronous component
    • If I end up walking and I findhasAsyncIs stillfalse, which means no asynchronous component directlynext()Go to the next step
  • How do you determine if a component is asynchronous?
    • As we said earlier, invueThe asynchronous component must be a factory function called internallyResove, rejectOr returnPromiseOr return a particular format object, but it’s definitely a function
    • The secondvueEach instance in thecid, if you havecidMeans that the corresponding instance has been generated, so the asynchronous componentcidMust be forundefined
    • So the way to tell if it’s an asynchronous component isFunction &&cid === 'undefined'
  • If it is judged to be an asynchronous componenthasAsyncSet totrueAnd let thependingAutomatic increment: indicates that asynchronous components have been discovered. After the components are parsedpendingThe decrement, and whenpending<=0It indicates that the asynchronous component parsing is complete and can be callednextProceed to the next step
  • As I mentioned earlier,vueThe asynchronous component factory function ofResolve, rejectBoth methods are called after getting the component definition from the server;
  • These two methods are passed to the asynchronous component factory function on receipt of the asynchronous component definition returned by the server
  • Because the asynchronous component factory function returns onePromiseFunctions or objects of a particular format, so the following can happen
    • If it is returnedPromise, the two methods are passed to the returnedPromise.thenIn the
    • If a particular format object is returnedcomponentField and pass the two methods in againcomponent.thenIn the
  • Due to theResolve, rejectHas beenonceA wrapper, even if passed multiple times, will only be executed once
  • We look at theResolve, rejectmethods
  • They were all taken by oneonceMethod to ensure that it is executed only once
  • reject
    • Throw an error and callnextPass to the next process
  • resolve
    • Let’s see if it isesmIf, take it.defaultField to get the component definition
    • After getting the component definition, the asynchronous component factory function is reserved for later use
    • The corresponding component in the named view of the routing record is then replaced, which completes the resolution of the component and binds it to the routing record
  • Again, all of the above logic isresolveAsyncComponentsReturn function logic that will not actually be called until the queue is executed
  • At this point, our queuequeueIt already contains the extracted onesGuards, hooks, functions that contain the logic to parse asynchronous components
  • Now that the queue is built, how does it work

Execution of guard queues

  • Queue execution is throughRunQueue, iteratorIt works with each other

runQueue

  • runQueueMethods insrc/util/async.js
// src/util/async.js

/* @flow */
// The queue executes the function
// queue Specifies the queue to execute
// fn iterates
// cb callback function
export function runQueue(
  queue: Array<? NavigationGuard>, fn:Function,
  cb: Function
) {
  const step = (index) = > {
    // Execute the callback
    if (index >= queue.length) {
      cb()
    } else {
      // If yes, execute the iteration function
      if (queue[index]) {
        fn(
          queue[index],
          /* next*/ () = > {
            step(index + 1)})}else {
        // Otherwise, skip to the next execution
        step(index + 1)
      }
    }
  }
  step(0)}Copy the code
  • You can see that it receives a queuequeue, an iterative functionfn, a callback function that completes executioncb
  • Inside is a recursive implementation
  • Defines astepFunction and receives an object that identifies the queue execution stepindex
  • It must be called manuallystepTo jump to the execution of the next queue item
    • This is used when parsing components
  • whenindexIf the value is greater than or equal to the length of the queue (the end condition of recursion), it means that all queue items have been executed and can be calledcb
  • Otherwise, if there are queue items, the iterative function is calledfnAnd pass in the queue item and jump to the next queue itemstep(index + 1)function
  • If there are no queue items, the execution of the next queue item is skipped directly
  • Recursion throughstep(0)To activate the

iterator

  • Iterator related code good
// src/history/base.js

// Iterate over the function
const iterator = (hook: NavigationGuard, next) = > {
  if (this.pending ! == route) {// When to is found to have changed, it needs to be cancelled
    return abort()
  }
  try {
    hook(
      /* to*/ route,
      /* from*/ current,
      /* next*/ (to: any) = > {
        if (to === false || isError(to)) {
          // next(false) -> abort navigation, ensure current URL
          // next(false) -> Cancel the jump and add a new record (but not add record because the URL has not changed)
          this.ensureURL(true)
          abort(to)
        } else if (
          typeof to === 'string' || // next('/')
          (typeof to === 'object' &&
            (typeof to.path === 'string' || typeof to.name === 'string'))) {/ / next ({path: '/'}) or next ({name: 'Home'})
          // next('/') or next({ path: '/' }) -> redirect
          abort() // Cancel current
          if (typeof to === 'object' && to.replace) {
            // Call the replacement record of the subclass method
            this.replace(to)
          } else {
            // Call the subclass method to add records
            this.push(to)
          }
        } else {
          // confirm transition and pass on the value
          // next()
          next(to)
        }
      }
    )
  } catch (e) {
    abort(e)
  }
}
Copy the code
  • You can see it receives onehookThat is, guards in guard queues, hook functions andnextFunction (The step function passed in by runQueue)
  • If a route changes during the execution, it is cancelled immediately
  • And then try to callhookAnd passes in the destination route object, the current route object, and a receivetoArrow function of
  • In fact, these three parameters correspond to what the guard will receiveFrom, to, nextThree parameters
router.beforeEach((to, from, next) = > {
  // ...
})
Copy the code
  • We know the guardsnextIs afunctionAnd can receive the following parameters to meet different route forward requirements
    • next(): Goes to the next hook in the pipe.
    • next(false): interrupts current navigation.
    • next('/')ornext({ path: '/' }): Jumps to a different address. The current navigation is interrupted and a new navigation is performed. You can pass any location object to next, and you can set options like replace: True, name: ‘home’, and any options used in router-link’s to prop or router.push.
    • next(error): (2.4.0+) If the argument passed to Next is an Error instance, the navigation is terminated and the Error is passed to the callback registered with router.onerror ().
  • The receivingtoThe arrow function handles these scenarios
  • Every item in the queue will be thereiteratorIs called once and passesnext()To jump to the execution of the next queue item
  • To understand theRunQueue, iteratorNow, what does the queue actually look like

The queue

  • The complete code for queue execution is as follows
// src/history/base.js

// Execute queue
runQueue(
  queue,
  iterator,
  /* Execute the end callback */ () = > {
    const postEnterCbs = [] // Save the callback passed to Next in beforeRouteEnter
    const isValid = () = > this.current === route // Wait until async components are resolved before // Fully in-component Enter Guards
    const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) // Wait for the asynchronous component to finish parsing, then extract the beforeRouteEnter guard within the component
    const queue = enterGuards.concat(this.router.resolveHooks) // beforeResolve hooks
    runQueue(
      queue,
      iterator,
      /* Execute the end callback */ () = > {
        if (this.pending ! == route) {return abort()
        }
        this.pending = null
        onComplete(route) // Execute the onComplete callback, which calls the updateRoute method and internally fires the afterEach hook
        if (this.router.app) {
          this.router.app.$nextTick(() = > {
            // Call the callback passed to Next in the beforeRouteEnter guard
            // next(vm=>xxx)
            postEnterCbs.forEach((cb) = > {
              cb()
            })
          })
        }
      }
    )
  }
)
Copy the code
  • As you can see, the queue is passed inqueue, the iteratoriteratorAnd a fully executed callback function
  • Under the first reviewqueueWhat elements are in the queue
    • BeforeRouteLeave guard
    • globalBeforeEach guard
    • BeforeRouteUpdate guard
    • BeforeEnter guard
    • And a higher-order function that returns the function that parses the asynchronous component
  • The functions in the queue will be in sequence when the queue executesiteratorIn the called
  • The first few guard functions are extracted and can be executed synchronously
  • But when the last higher-order function executes, it returns a function that parses the asynchronous component
  • By virtue of the closure feature, it can access data fromiteratorIn the incomingFrom, to, next
  • It is then called after parsing the asynchronous componentnextTo enter the execution of the next item in the queue
  • This ensures that the queue will be executed sequentially, even if there are asynchronous functions in the queue
  • The end callback is executed after the entire guard queue is executed
  • When the end callback is executed, the asynchronous component is fully parsed and can be extractedbeforeRouteEnterthe

Extracting beforeRouteEnter

  • extractingbeforeRouteEnterSlightly different from the other guards
    1. becausebeforeRouteEnterThe component may be asynchronous, sobeforeRouteEnterThe extraction cannot begin until the asynchronous component has been resolved
    1. There is also a difference in the route transition animation forout-inThe asynchronous component may have been parsed, butrouter-viewThe instance may not be registered and cannot be called at this timebeforeRouteEnter; seeissue #750
  • becausebeforeRouteEnterSupports passing a callback tonextTo access the component instance, as shown below
beforeRouteEnter (to, from, next) {
  next(/*postEnterCb*/vm= > {
    // Access component instances through 'VM'
  })}
Copy the code
  • And thisvmIs stored in therouter-viewOn the instance, so you have to waitrouter-viewThe callback can only be invoked if the instance exists
  • Let’s look at the code implementation
//src/history/base.js.const postEnterCbs = [] // Save the callback passed to Next in beforeRouteEnter
const isValid = () = > this.current === route // Indicates that the jump ends
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) // Wait for the asynchronous component to finish parsing, then extract the beforeRouteEnter guard within the component.// Extract the component's beforeRouteEnter guard
function extractEnterGuards (
  activated: Array<RouteRecord>,
  cbs: Array<Function>, // postEnterCbs
  isValid: () => boolean
) :Array<?Function{
  return extractGuards(
    activated,
    'beforeRouteEnter'.(guard, _, match, key) = > { /* Bind the execution context of beforeRouteEnter */
      return bindEnterGuard(guard, match, key, cbs, isValid)
    }
  )
}

// Bind the execution context of beforeRouteEnter
function bindEnterGuard (
  guard: NavigationGuard,
  match: RouteRecord,
  key: string,
  cbs: Array<Function>, // postEnterCbs
  isValid: () => boolean
) :NavigationGuard {
  // Wrap the beforeRouteEnter inside the component
  return function routeEnterGuard (to, from, next{
    // Invoke the beforeRouteEnter guard within the component
    return guard(to, from./* beforeRouteEnter next; Cb is the next callback */cb= > {
      if (typeof cb === 'function') {
        cbs.push(() = > {
          / / # 750
          // if a router-view is wrapped with an out-in transition,
          // the instance may not have been registered at this time.
          // we will need to poll for registration until current route
          // is no longer valid.
          // If router-view is wrapped by out-of-in transition
          // The router-view instance may not yet exist when the beforeRouteEnter guard is called
          // But this.current is already to
          // So you must poll the cb call until instance exists
          poll(cb, match.instances, key, isValid)
        })
      }
      // Iterator next step
      next(cb)
    })
  }
}

// Polling calls cb
function poll (
  cb: any./* cb for beforeRouteEnter next callback */ // somehow flow cannot infer this is a function
  instances: Object,
  key: string,
  isValid: () => boolean
{
  if( instances[key] && ! instances[key]._isBeingDestroyed// do not reuse being destroyed instance
  ) {
    cb(instances[key])
  } else if (isValid()) {
    setTimeout(() = > {
      poll(cb, instances, key, isValid)
    }, 16)}}Copy the code
  • You can see it’s callingextractEnterGuardsbefore
  • One is declared in the outer layerpostEnterCbsAn array of
    • Used to storeThe callback function passed to Next in beforeRouteEnter, we callpostEnterCbThat is, the pullback after entry
  • And one to determine whether the jump is overisValidfunction
  • isValidThe function is passed inextractEnterGuardsIn the
  • extractEnterGuardsReturns a wrapper through the higher-order function formbeforeRouteEnterNamed function ofrouteEnterGuard, which is called when the queue is executed and executes the realbeforeRouteEnterThe guardsguard
  • guardIs received when executedThe from and toAnd a modified onenext, it receives onepostEnterCb
  • thispostEnterCbAccess may be required in the futurevm
  • So willpostEnterCbwithpollMethod wrapping is inserted as defined on the outsidepostEnterCbsIn the array
  • pollThe method is mainly used to solve the above mentionedissue #750And it keeps polling untilrouter-viewWhen the instance existspostEnterCbAnd pass mount torouter-viewComponent instance on
  • So that’s itnextAccess to the logic of the component instance
  • Extract thebeforeRouteEnterThe guards and the ones in thempostEnterCbsThen inqueueAfter joining together thebeforeResolveThe guards
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) // Wait for the asynchronous component to finish parsing, then extract the beforeRouteEnter guard within the component
const queue = enterGuards.concat(this.router.resolveHooks) // beforeResolve hooks
Copy the code
  • At this timequeueIs in therouteEnterGuardThe function andresolveHook
  • And then execute the queue, in the queuerouterEnterGuardandresolveHookWill perform
runQueue(
  queue,
  iterator,
  /* Execute the end callback */ () = > {
    if (this.pending ! == route) {return abort()
    }
    this.pending = null
    onComplete(route) // Execute the onComplete callback, which calls the updateRoute method and internally fires the afterEach hook
    if (this.router.app) {
      this.router.app.$nextTick(() = > {
        // Call the callback passed to Next in the beforeRouteEnter guard
        // next(vm=>xxx)
        postEnterCbs.forEach((cb) = > {
          cb()
        })
      })
    }
  }
)
Copy the code
  • The logic is the same as before,beforeRouteEnterandbeforeResolveIs called in sequence, and the end-of-queue callback is executed
  • Is called in the end-of-queue callbackonCompleteAnd the incomingObjective the RouteAnd in the$nextTickSaved before traversingpostEnterCbs, that is, the incomingnextThe callback
  • Here theonCompleteYes When confirming routes (confirmTransition) of the incoming
// SRC /history/base.js transitionTo method

   this.confirmTransition(
      route,
      () = > { // onComplete
        this.updateRoute(route) AfterEach hook is triggered when a route is updated
        onComplete && onComplete(route) // Call the onComplete callback
        this.ensureURL()

        // fire ready cbs once
        // Trigger the ready callback
        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb= > {
            cb(route)
          })
        }
      },
      / / onAbort callback
      err= >{...}
    )
Copy the code
  • You can see the callupdateRouteTo update theroute, which triggersafterEachhook
  • callensureURLUpdate the url
  • And call intransitionTotheonCompleteFunction, mainly used in thevue-routerIs initializedhashMode to initialize binding (setupHashListener)
  • Finally trigger passonReadyregisteredreadyCbsThe callback
// src/history/base.js

  AfterEach hook is triggered after updating the route
  updateRoute (route: Route) {
    const prev = this.current
    this.current = route/ / update the current
    this.cb && this.cb(route) // Call the updateRoute callback, which reassigns _Routerroot. _route, triggering a re-rendering of the router-View
    this.router.afterHooks.forEach(hook= > { // Trigger afterEach dog
      hook && hook(/* to*/route, /* from*/prev)
    })
  }
Copy the code
  • updateRouteWill be calledHistorythroughlistenMethod to register the update callback, triggeredroter-viewRerendering of
  • These update callbacks are invue-routerRegistered during initialization
// src/index.js init
history.listen((route) = > {
  this.apps.forEach((app) = > {
    app._route = route / / update the route})})Copy the code
  • And then execute allafterEachhook
  • At this point, a complete route jump is complete, and the corresponding guard and hook are also triggered

conclusion

  • The entire navigation is resolved (validated) by extracting the corresponding guards and hooks from the routing records of different states
  • Then form a queue and userunQueue,iteratorPerform guard execution skillfully
  • And in it, the parsing of asynchronous components,postEnterCbInstance acquisition problem in
  • The whole guard and hook execution process is as follows
    • Navigation is triggered.
    • Call the beforeRouteLeave guard in the deactivated component.
    • Call the global beforeEach guard.
    • Call the beforeRouteUpdate guard (2.2+) in the reused component.
    • Call beforeEnter in routing configuration.
    • Parse the asynchronous routing component.
    • Call beforeRouteEnter in the activated component.
    • Call the global beforeResolve guard (2.5+).
    • Navigation confirmed.
    • Call the global afterEach hook.
    • Trigger a DOM update.
    • Call the callback passed to Next in the beforeRouteEnter guard with the created instance.

reference

  • Github.com/dwqs/blog/i…
  • Github.com/dwqs/blog/i…
  • Github.com/dwqs/blog/i…
  • Github.com/vuejs/vue-r…
  • Juejin. Cn/post / 684490…
  • Juejin. Cn/post / 684490…
  • Juejin. Cn/post / 684490…
  • Juejin. Cn/post / 684490…
  • www.jianshu.com/p/29e8214d0…
  • Ustbhuangyi. Making. IO/vue – analysi…
  • Blog.liuyunzhuge.com/2020/04/08/…

PS

  • The rest will be introduced later. If you think it’s ok, you can give it a thumbs up at ✨
  • personalgithub, also summed up a few things, welcome star
  • Drawing -board based on Canvas
  • Front-end introduction Demo, best practices collection FE-awesome -demos
  • One that automatically generates aliasesvue-cli-pluginwww.npmjs.com/package/vue…

NPM package

  • vue-cli-plugin-auto-alias
  • @bryanadamss/drawing-board
  • @bryanadamss/num2chn
  • ant-color-converter

communication

  • If you have any problems, you can add wechat to communicate and grow together and make progress together

essay

  • Denver annual essay | 2020 technical way with me The campaign is under way…