Vue – the router source code parsing words | | 1.5 w multiple warning – 【 in 】
- Hello everyone, I am Guanghui 😎
- This is
vue-router
Source code analysis of the middle, is also the core - This article mainly introduces the following points
- This paper introduces the
vue-router
How is route matching done - How do guards and hooks trigger
- How are asynchronous components handled, and so on
- This paper introduces the
- 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 help
star
✨
- 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
VueRouter
Encapsulate the two key steps described above intotransitionTo
The method of- Next, let’s take a look
transitionTo
The implementation of the
transitionTo
- As I mentioned earlier,
transitionTo
Methods are defined in the base classHistory
On 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
- in
setupListeners
The chapter also introducestransitionTo
Method signature of - Receive three parameters
location
forRawLocation
Type, which represents the address to resolveonComplete
The jump success callback is invoked when the route jump succeedsonAbort
Is a jump failure (cancel) callback, called when the route is cancelled
- Look at the internal logic
- call
The router instances
thematch
Method to get the route object to jump to from the route mapping tableroute
; This is essentiallyRouting matching
Process; - Get what you want to jump to
route
After the callconfirmTransition
completeroute
Parse the jump, and call the corresponding callback method when the jump is successful and canceled; This is aNavigation parsing
process- Call on success
updateRoute
Trigger rerender, and then trigger the related callback; We’ll cover rendering in a later chapter - When cancel (failed), the related callback is triggered
- Call on success
- So let’s look at the route matching process
Routing matching
transitionTo
Will callThe router instances
thematch
Method 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 instance
Method, also called by the matchermatch
Method, pass the parameters through directly- For the creation of a match, see the previous one
Create matcher
chapter
- For the creation of a match, see the previous one
- Let’s move on to the matcher
match
methods
// 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 the
router.match
The parameters are passed through, so the signatures are exactly the sameraw
isRawLocation
Type: indicates the address to be matchedcurrentRoute
Is the current routing objectredirectedFrom
It means what address it was redirected from
- We look at the
match
Method logic- First of all, it’s against the incoming
raw
Address, formatted (normalized) - Then fetch the formatted address
name
name
If yes, check whether it can passname
innameMap
To find the corresponding route recordRouteRecord
- If not, create a new one
route
The object returned - If it can be found, fill it
params
And uses this routing record to create a new oneRoute
The object returned
- If not, create a new one
name
If it does not exist, judgepath
If there is a- Exist, then use
PathList, pathMap
callmatchRoute
Check whether a route is matched, and then find the matched route record. Then use this route record to create a new oneroute
The object returned
- Exist, then use
name
,path
Aren’t there- Create a new one
route
The object returned
- Create a new one
- First of all, it’s against the incoming
- 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 that
VueRoute
The address defined isRawLocation
Type, which is union type, supportedstring
andLocation
type
// 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.push
So are the parameters of the methodRawLocation
Type, so use$router.push
For 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 see
VueRouter
All 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 to
string
Type 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 address
raw
Copy,params
, return directly
- If so, copy the original address
- Handles relative route (relative parameter) jumps that carry only parameters, namely
this.$router.push({params:{id:1}})
In the form of- The definition of such an address is
There is no path
,Only params
andThe current route object exists
- The main processing logic is
- To merge
params
- If the route is named, this parameter is used
current.name
As anext.name
And assignmentparams
- Non-named route: Finds the matched route record from the current route object and retrieves the route record
path
As anext.path
And fill itparams
- Returns the processed address
- To merge
- Due to this jump mode, only
params
, so the object must be routed from the currentcurrent
To get the available fields (path
,name
), as its own value, and then jump
- The definition of such an address is
- Processing by
path
Way to jump- call
parsePath
frompath
Parsing outPath, Query, hash
- And then to
current.path
forbasePath
, the resolution (resolve
)The final path
- right
query
Merge operation - right
hash
Pre-append#
operation - Returns a
_normalized:true
Identification of theLocation
object
- call
- After the above processing, no matter what address is passed in, return a string with
_normalized:true
Identification of theThe Location type
The object of - normalize-location.png
Check whether the address matches matchRoute
- We know that
VueRouter
Dynamic route matching is supported, as shown in the following figure - dynamic-route.png
- What we did in the last video
Generating Routing Records
The chapter also introduces,VueRouter
When routing records are generated, the route passespath-to-regexp
The package generates a regular extension object and assigns a value to the routing recordregex
Field, used to obtain subsequent dynamic route parameters- The main logic is to provide a dynamic route
user/:id
And an address/user/345
Through thepath-to-regexp
I 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 examples
Generating Routing Records
chapter
- The main logic is to provide a dynamic route
- The above logic for extracting parameters is in
matchRoute
Implementation of the matchRoute
Located 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 one
boolean
Value, which represents the value passed inpath
Whether or not it passesThe regex matching
; Although returning oneboolean
Value, but inside it does something very important frompath
To extract dynamic routing parameter values, we look at the complete logic - First call
path.match(regex)
- Cannot match direct return
false
- Yes and none
params
To return totrue
- All that’s left is one case where it matches and
params
Yes, this is the caseparams
Do the correct assignment- The whole assignment is mostly traversal
path.match(regex)
Returns the value and retrievesregex
Stored in thekey
, and then assign the values in turn, refer to the comments above for details; - about
regex
,path-to-regexp
, you can refer toGenerating Routing Records
Chapters andhttps://www.npmjs.com/package/path-to-regexp
- The whole assignment is mostly traversal
- And then there’s the point, the point of assignment
pathMatch
What is?- This is essentially the same thing as the wildcard
*
Related to the VueRouter
The special treatment of wildcards can be seenRouter.vuejs.org/zh/guide/es…- namely
pathMatch
Represents the path to which the wildcard is matched
- namely
- This is essentially the same thing as the wildcard
- 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
fillParams
You can view it asmatchRoute
The 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 by
Regexp.compile
In combination withregexpCompileCache
Implementation of the Regexp.compile
To receive aDynamic Routing path
, returns a function that can be used to do inverse parsing;Regexp.compile
Examples 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, yes
Regexp.compile
The returned function is cached - then
matchRoute
Add thepathMatch
Assigned toparams[0]
- call
Regexp.compile
Returns the function toparams
Is the input parameter, inverse parsingurl
And return - Delete added
params[0]
- fill-params.png
Create the route object _createRoute
- No matter on the top
normalizeLocation
,matchRoute
,fillParams
It’s all for incomingaddress
Do something; - while
match
The route object () method is used to find the route object that matches the address_createRoute
Method 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
record
Used to generateThe Route objects
Destination route record oflocation
The target addressredirectedFrom
The 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 redirect
redirect
field - Add an alias route
matchAs
field- You can see in the front
Generating Routing Records
chapter
- You can see in the front
- If there are records added to the redirect
- You can see that different methods are called for different routing record types
- Redirect a route call
redirect
methods - Alias routing call
alias
methods - Rest of the calls
createRoute
methods
- Redirect a route call
- The activity diagram is as follows
- create-route-inner.png
- Actually,
redirect
,alias
Method is also called internallycreateRoute
methods - So let’s see
createRoute
Method implementation
createRoute
createRoute
Located 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 the
VueRouter
Support for incoming customizationSerialize the queryString
Method, so the first step is to get serializationqueryString
The method of - Then the
query
Made a deep copy to avoid interaction - The next step is to make new
Route
object - If it is redirected from another route, the full redirected source address is generated and assigned to the newly generated one
Route
object - The last call
Object.freeze
A freeze on newRoute
Object, becauseRoute
The object isimmutable
the - The whole process is as follows
- create-route.png
- You can see the generation
Route
Is calledgetFullPath
To 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 see
getFullPath
Is in thepath
Appendqs
andhash
- In addition to generate
Route
Is calledformatMatch
In 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 a
Route
It doesn’t correspond to oneRoute objects
? - Actually in the glossary introduction
Route Routing object
When, also mentioned, aRoute Routing object
There 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 later
route.matched[route.matched.length - 1]
To get the currentroute
Corresponding to an exact matchRouteRecord
- After watching
createRoute
Let’s seealias
The implementation of the
Create an alias routing object alias
alias
Located 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 first
matchAs
getaliasedPath
. - Then pick
aliasedPath
Walk againmatch
getaliasedMatch
Route objects aliasedMatch
If so, takealiasedMatch
Precisely matchedRoute record object
andlocation
To generate theRoute objects
return- If it does not exist, create a new one
Route objects
return - It might be a little convoluted, but let’s do an example
- And as we already know,
/a
Alias set/b
, two route records are generated, and/b
On the routing record ofmatchAs
for/a
- The ones you forget can look ahead
Generate alias routing records
chapter
- The ones you forget can look ahead
- Incoming here
alias
thematchAs
Is equivalent to/a
First, takematchAs
namely/a
Get filled inparams
The path of the - Call it from this path
match
Find a matchRoute objects
And remember torouteA
- As mentioned earlier, routing objects are associated with routing records, so from
routeA
Can obtain accurate matching routing recordsrouteRecordA
- Take this routing record and
/b
thelocation
To generate the routing object and return - This is what the official website says
The 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
- And as we already know,
- The activity diagram is as follows
- alias.png
- Let’s see
redirect
The implementation of the
Create a redirect for a redirected route object
- So let’s take a look
record.redirect
Several 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'}}}
- string
- Let’s look at this first
redirect
Method 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 time
redirectedFrom
/a
->/b
->/c
In the/c
To 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,
redirectedFrom
There is no value - in
redirect
The method inside willlocation
As aredirectedFrom
Parameters of the callmatch
Method,match
If it is found that the redirection is still needed, the call continuesredirect
At this time,redirectedFrom
It has a value. It’s the first one passed inlocation
, in order to complete the transfer of the initial address
- On the first redirect,
- 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-redirect
Route (trigger route forward) is redirected to/baz
./baz
It’s going to redirect to/foo
, the final displayFoo
Components; So,redirect
The method should be called twice; - We can look at the output of the example above
- redirect-demo.png
- Will find
redirect
The method is called four times, the first two times due to route jumpsredirect
The last two are triggered by the need to resolve the route while the component is renderingredirect
Call; - You can compare the call stack
- redirect-stack-1.png
- redirect-stack-2.png
- You can see the first and second ones
redirect
Is made up oftransitionTo
The trigger - redirect-stack-3.png
- redirect-stack-4.png
- The third and fourth are component renderings
render
callresolve
The trigger- Why does component rendering need to parse routes, as explained in the component section below
- redirect-stack-1.png
- You can see the first call
redirect
from/named-redirect
Redirected to/baz
At this time,redirectFrom
It has no value - And the second call is from
/baz
Redirected to/foo
At this time,redirectFrom
That’s the address that triggered the first redirect/named-redirect
- And ultimately
$route
There will also be one onredirectFrom
The address that triggered the first redirect is preserved - We just looked at it
redirectFrom
Let’s seeredirect
Concrete 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, yes
record.redirect
Standardized, unified generation of oneredirect
Object (redirect target)- Now, why do we normalize, as I mentioned earlier,
redirect
Supports string, object, and function types, so you need to standardize them for unified processing
- Now, why do we normalize, as I mentioned earlier,
- And then it takes precedence
redirect
thequery hash params
Value to domatch
, will only be taken if it does not existInitial address location
thequery hash params
- And then it decides what the redirection target is
Named form
orPath form
- Named form
- To determine
nameMap
If there is no target routing record, interrupt and give a hint; - Then retrace
match
Process, and willlocation
As aredirectedFrom
Pass in, and you’re doneredirectedFrom
Transfer closed loop of match
It will continue to determine if there is a redirect, so that multiple multiple redirect scenarios are covered
- To determine
- Path form
- take
path
Match, need to get the full path, so first fromrecord
Pull out the original pathrawPath
And fill in what was resolved earlierparams
Get the full address - Take the full address and go back
match
The process will also belocation
As aredirectedFrom
Pass in, doneredirectedFrom
Transfer closed loop of match
It will continue to determine if there is a redirect, so that multiple multiple redirect scenarios are covered
- take
- If neither
Named form
Is notPath form
Directly create a new routing object to return - The process is as follows
- redirect-full.png
summary
- The process of route matching is actually taking
Address RawLocation
generateRoute object Route
In betweenRoute records RouteRecord
It acts as an intermediate bridge because the routing record contains important information about the generated routing object. So the process should be to takeaddress
Find the corresponding route in the routing mapping tableRouting record
And then takeRouting record
generateRoute objects
- The above matching logic is mainly composed of
match
Function implementation, the key logic containsAddress formatting normalizeLocation
,Check whether the address matches matchRoute
,Fill the parameter fillParams
,Create the route object _createRoute
- in
normalizeLocation
, torawLocation
To standardize and facilitate subsequent processing - in
matchRoute
With the help ofpath-to-regexp
Check if the address matches and extractparams
fillParams
You can view it asExtract the params
Reverse 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 get
RawLocation
The correspondingRoute
- get
Route
, we can parse the navigation
Navigate the parse (validation) process
- I mentioned it before
transitionTo
Method is calledmatch
Method to get the targetRoute
After that, it will be calledconfirmTransition
Method to do navigation parsing - We know that
vue-router
Various hooks, guard functions are triggered sequentially when routing jumps, for exampleBeforeRouteLeave, beforeRouteEnter, etc
; - First of all, these hooks and guards are defined in
vue-router
On the instance, some are route exclusive, some are located.vue
Component, 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 in
confirmTransition
In the confirmTransition
Methods are defined insrc/base.js
In the
// SRC /base.js History class
// Confirm the route redirect
confirmTransition (/* to*/route: Route, onComplete: Function, 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
route
Destination route object. The destination to be resolvedto
Object, andcurrent
Can be understood asfrom
Object.onComplete
The jump completes the callbackonAbort
Cancel, 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 jumps
abort
Method, it’s mainly rightonAbort
Method made a layer of packaging; This method is called when the navigation is cancelled- It receives a
err
Parameter if there is a registration error callback anderr
For theNavigationDuplicated
Errors are traversederrorCbs
List performs the error callback within it - The last call
onAbort
Call back and pass inerr
The parameters are handled externally
- It receives a
- Next, determine whether to repeat the jump, mainly using
isSameRoute
Check 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 hopsabort
Method and pass inNavigationDuplicated
Error and terminate the process isSameRoute
Mainly judgePath, name, Hash, Query, params
If 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 determined
ensureURL
Method to updateurl
Compare the route records that need to be updated, deactivated, or activated
- After determining the repeat jump, you need to compare
from
,to
Route 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 in
resolveQueue
Method, a record list of the current and destination routing objects is passed in, deconstructed from the return valueupdated, deactivated, activated
- Look at the
resolveQueue
implementation
// 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 out
current
andnext
The 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 list
The 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 list
The index and the right side are the route records to be deactivated
- First of all find out
- For example,
current
To:[1, 2, 3]
,next
for[1, 2, 3, 4, 5]
, the current route object containsOne, two, three
The destination routing object contains three routing recordsOne, two, three, four, five
Five Routing Records- After the calculation
max
for5
- Loop, find the first unequal index is
3
- So what needs to be updated is
Next, slice (0, 3)
namelyOne, two, three
- What needs to be activated is
next.slice(3)
namely4, 5,
- What needs to be inactivated is
current.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 out
vue-router
What are the hooks and guard functionsrouter.beforeEach
Global front guardrouter.beforeResolve
Global Parsing Guard (new in V2.5.0)router.afterEach
Global post-hookRouteConfig.beforeEnter
Route exclusive guardvm.beforeRouteEnter
Vue component internal route entry guardvm.beforeRouteUpdate
Vue Component Routing Update Guard (new in V2.2)vm.beforeRouteLeave
Vue component routing away guard
- You can see some of them are definitions
VueRouter
Some are defined in configuration rulesRouteConfig
Some are defined inRouteComponent Routing component
On the- The hooks and guards of the first two are easy to get because we are in
The History class
Holding theVueRouter
Instance, easily accessible to the guards and hooks and executed with little extra processing; - The only thing that’s hard to deal with is the definition
RouteComponent Routing component
The guard function inRouteRecord
Get all theRouteComponent Routing component
And extract the corresponding guard from it, and finally bind its context to ensure that the execution result is correct;
- The hooks and guards of the first two are easy to get because we are in
- In the last video, we got the ones that need to be updated, activated, and deactivated
RouteRecord Indicates route records
Let’s take a look at which guards to extract deactivated
Extract thebeforeRouteLeave
updated
Extract thebeforeRouteUpdate
activated
Extract thebeforeRouteEnter
, there is a special scenario where the asynchronous route component can be extracted only after the parsing of the asynchronous route component is completebeforeRouteEnter
Guards, 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 defined
queue
- I did the following in turn
- Extract the
deactivated
In thebeforeRouteLeave
The guards - To obtain the
VueRouter
Defined on the instancebeforeEach
The guardsbeforeEach
Guards are defined directly inVueRouter
The instance
- from
updated
Extract thebeforeRouteUpdate
The guards - from
activated
To obtain the exclusive routebeforeEnter
The guardsbeforeEnter
Guards were originally defined inRouteConfig
Is passed to the routing record, so it can be directly obtained from the routing record
- parsing
activated
Asynchronous routing component in- Routing Component Support
import()
Dynamic import, so it’s going to be handled here
- Routing Component Support
- Extract the
- So let’s start with a method with a very similar name
extractLeaveGuards
andextractUpdateHooks
ExtractLeaveGuards, extractUpdateHooks
- Both are located
src/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 internally
extractGuards
, the former passes an extra parametertrue
- Let’s see
extractGuards
// 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 records
records
- namely
extractLeaveGuards
In the incomingdeactivated
Routing record array;extractUpdateHooks
In the incomingupdated
Routing record array
- namely
- Receives a guard name to extract
name
beforeRouteLeave
andbeforeRouteUpdate
string
- A function that binds above and below a guard
bind
extractLeaveGuards
,extractUpdateHooks
It’s all passingbindGuard
Method, which we parse below
- And whether one needs to output in reverse order
reverse
Boolean value; Optional parameter;extractLeaveGuards
The relay will betrue
, represents that the returned array (guard function array) needs to be output in reverse order;
- Returns a
item
isFunction
An array of
- Receives an array of routing records
- Look at the internal logic
- call
flatMapComponents
The incomingrecords
And a receiverdef, instance, match, key
Parameter arrow function, returns oneguards
The guards array - Then according to the
reverse
To decide if it’s rightguards
The array is reversed- Why reverse order?
- in
createRoute
As 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 - 1
Location), parent record first - Some guard functions need to be executed in reverse order, for example
beforeRouteLeave
, it needs to be called on the accurately matched routing component first, and then on the parent component
- The last call
flatten
willguards
flat
- call
- Look at the first
flatMapComponents
implementation
// 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 records
matched
And a functionfn
, returns a passflatten
Array to processmatched
That’s what we passed inrecords
fn
Is to receivedef, instance, match, key
Parameter arrow function
- This method essentially iterates through each routing component in the routing record and calls the external functions in turn as an input parameter
fn
, returns the result fromfn
Function determines, and finally outputs the result array flat- This method is also used when parsing asynchronous components
- It’s going to be on the incoming
records
callmap
Method, and iterate through eachrecord
The definition ofcomponents
Field and paircomponents
againmap
Traversal, and then call the incomingfn
.map
As a result,fn
Return result components
Fields 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 fn
def, instance, match, key
The arrow of the argument function has four argumentsdef
The correspondingm.components[key]
The routing component definition (Foo, Bar, Baz
)instance
The correspondingm.instances[key]
isrouter-view
Component instance, about routing records androute-view
How do they relate to each otherThe view components
When parsingm
This corresponds to the current route record traversedkey
Is 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 call
extractGuard
The mapping is extracted directly from the routing component definitionname
Guard function of - And then we call pass
extractGuards
thebind
Method is to guard the binding context - We look at the
extractGuard
implementation
// 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
- call
extend
To apply global mixins - Returns the corresponding guard function
- call
- After extracting a single guard, you need to call the incoming one
bind
Method to which the context is bound; bind
And 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
extractGuards
The 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 through
resolveAsyncComponents
Method implemented
resolveAsyncComponents
- Before we look at how to parse asynchronous components, let’s take a look
vue
What 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 as
Vue 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 function
Resolve, reject
Component definition, return onePromise
Returns an object with a specific identification field - The asynchronous component code in the parse routing record is located
src/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 records
matched
, returns a receiveFrom, to, next
Inside is the parsing logic for asynchronous components resolveAsyncComponents
It 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 components
hasAsync
, and the number of asynchronous components currently waiting to be resolvedpending
- And then called
flatMapComponents
getrecords
And 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 find
hasAsync
Is stillfalse
, which means no asynchronous component directlynext()
Go to the next step
- Defines an identity field for asynchronous components
- How do you determine if a component is asynchronous?
- As we said earlier, in
vue
The asynchronous component must be a factory function called internallyResove, reject
Or returnPromise
Or return a particular format object, but it’s definitely a function - The second
vue
Each instance in thecid
, if you havecid
Means that the corresponding instance has been generated, so the asynchronous componentcid
Must be forundefined
- So the way to tell if it’s an asynchronous component is
Function &&cid === 'undefined'
- As we said earlier, in
- If it is judged to be an asynchronous component
hasAsync
Set totrue
And let thepending
Automatic increment: indicates that asynchronous components have been discovered. After the components are parsedpending
The decrement, and whenpending<=0
It indicates that the asynchronous component parsing is complete and can be callednext
Proceed to the next step - As I mentioned earlier,
vue
The asynchronous component factory function ofResolve, reject
Both 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 one
Promise
Functions or objects of a particular format, so the following can happen- If it is returned
Promise
, the two methods are passed to the returnedPromise.then
In the - If a particular format object is returned
component
Field and pass the two methods in againcomponent.then
In the
- If it is returned
- Due to the
Resolve, reject
Has beenonce
A wrapper, even if passed multiple times, will only be executed once - We look at the
Resolve, reject
methods - They were all taken by one
once
Method to ensure that it is executed only once reject
- Throw an error and call
next
Pass to the next process
- Throw an error and call
resolve
- Let’s see if it is
esm
If, take it.default
Field 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
- Let’s see if it is
- Again, all of the above logic is
resolveAsyncComponents
Return function logic that will not actually be called until the queue is executed - At this point, our queue
queue
It 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 through
RunQueue, iterator
It works with each other
runQueue
runQueue
Methods 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 queue
queue
, an iterative functionfn
, a callback function that completes executioncb
- Inside is a recursive implementation
- Defines a
step
Function and receives an object that identifies the queue execution stepindex
- It must be called manually
step
To jump to the execution of the next queue item- This is used when parsing components
- when
index
If 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 called
fn
And 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 through
step(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 one
hook
That is, guards in guard queues, hook functions andnext
Function (The step function passed in by runQueue
) - If a route changes during the execution, it is cancelled immediately
- And then try to call
hook
And passes in the destination route object, the current route object, and a receiveto
Arrow function of - In fact, these three parameters correspond to what the guard will receive
From, to, next
Three parameters
router.beforeEach((to, from, next) = > {
// ...
})
Copy the code
- We know the guards
next
Is afunction
And can receive the following parameters to meet different route forward requirementsnext()
: 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 receiving
to
The arrow function handles these scenarios - Every item in the queue will be there
iterator
Is called once and passesnext()
To jump to the execution of the next queue item - To understand the
RunQueue, iterator
Now, 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 in
queue
, the iteratoriterator
And a fully executed callback function - Under the first review
queue
What elements are in the queueBeforeRouteLeave guard
- global
BeforeEach 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 executes
iterator
In 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 from
iterator
In the incomingFrom, to, next
- It is then called after parsing the asynchronous component
next
To 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 extracted
beforeRouteEnter
the
Extracting beforeRouteEnter
- extracting
beforeRouteEnter
Slightly different from the other guards -
- because
beforeRouteEnter
The component may be asynchronous, sobeforeRouteEnter
The extraction cannot begin until the asynchronous component has been resolved
- because
-
- There is also a difference in the route transition animation for
out-in
The asynchronous component may have been parsed, butrouter-view
The instance may not be registered and cannot be called at this timebeforeRouteEnter
; seeissue #750
- There is also a difference in the route transition animation for
- because
beforeRouteEnter
Supports passing a callback tonext
To access the component instance, as shown below
beforeRouteEnter (to, from, next) {
next(/*postEnterCb*/vm= > {
// Access component instances through 'VM'
})}
Copy the code
- And this
vm
Is stored in therouter-view
On the instance, so you have to waitrouter-view
The 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 calling
extractEnterGuards
before - One is declared in the outer layer
postEnterCbs
An array of- Used to store
The callback function passed to Next in beforeRouteEnter
, we callpostEnterCb
That is, the pullback after entry
- Used to store
- And one to determine whether the jump is over
isValid
function isValid
The function is passed inextractEnterGuards
In theextractEnterGuards
Returns a wrapper through the higher-order function formbeforeRouteEnter
Named function ofrouteEnterGuard
, which is called when the queue is executed and executes the realbeforeRouteEnter
The guardsguard
guard
Is received when executedThe from and to
And a modified onenext
, it receives onepostEnterCb
- this
postEnterCb
Access may be required in the futurevm
- So will
postEnterCb
withpoll
Method wrapping is inserted as defined on the outsidepostEnterCbs
In the array poll
The method is mainly used to solve the above mentionedissue #750
And it keeps polling untilrouter-view
When the instance existspostEnterCb
And pass mount torouter-view
Component instance on- So that’s it
next
Access to the logic of the component instance - Extract the
beforeRouteEnter
The guards and the ones in thempostEnterCbs
Then inqueue
After joining together thebeforeResolve
The 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 time
queue
Is in therouteEnterGuard
The function andresolveHook
- And then execute the queue, in the queue
routerEnterGuard
andresolveHook
Will 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,
beforeRouteEnter
andbeforeResolve
Is called in sequence, and the end-of-queue callback is executed - Is called in the end-of-queue callback
onComplete
And the incomingObjective the Route
And in the$nextTick
Saved before traversingpostEnterCbs
, that is, the incomingnext
The callback - Here the
onComplete
Yes 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 call
updateRoute
To update theroute
, which triggersafterEach
hook - call
ensureURL
Update the url - And call in
transitionTo
theonComplete
Function, mainly used in thevue-router
Is initializedhash
Mode to initialize binding (setupHashListener
) - Finally trigger pass
onReady
registeredreadyCbs
The 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
updateRoute
Will be calledHistory
throughlisten
Method to register the update callback, triggeredroter-view
Rerendering of- These update callbacks are in
vue-router
Registered 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 all
afterEach
hook - 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 use
runQueue
,iterator
Perform guard execution skillfully - And in it, the parsing of asynchronous components,
postEnterCb
Instance 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 ✨
- personal
github
, 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 aliases
vue-cli-plugin
www.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…