内容简介:路由这个概念最开始是在后端出现的,以前使用模板引擎开发页面的时候经常会看到这样的路径:有时还会有带.asp或.html的路径,这就是所谓的SSR(Server Side Render),通过服务端渲染,直接返回页面。其响应过程是这样的
路由这个概念最开始是在后端出现的,以前使用模板引擎开发页面的时候经常会看到这样的路径:
http://hometown.xxx.edu.cn/bbs/forum.php
有时还会有带.asp或.html的路径,这就是所谓的SSR(Server Side Render),通过服务端渲染,直接返回页面。
其响应过程是这样的
1.浏览器发出请求
2.服务器监听到80端口(或443)有请求过来,并解析url路径
3.根据服务器的路由配置,返回相应信息(可以是 html 字串,也可以是 json 数据,图片等)
4.浏览器根据数据包的Content-Type来决定如何解析数据
简单来说路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能。就像路由器在网络层中扮演的角色一样,肩负着将数据包正确导向目的地址的重任,只不过在这里变成了客户端浏览器的指路人,所谓的前端路由,指的是一种能力,即:
不依赖于服务器,根据不同的URL渲染不同的页面
前端路由与后端路由
在 Ajax
还没有诞生的时候,路由的工作是交给后端来完成的,当进行页面切换的时候,浏览器会发送不同的 URL
请求,服务器接收到浏览器的请求时,通过解析不同的 URL
去拼接需要的 Html
或模板,然后将结果返回到浏览器端进行渲染。
服务器端路由同样是有利亦有弊。它的好处是安全性更高,更严格得控制页面的展现。这在某些场景中是很有用的,譬如下单支付流程,每一步只有在上一步成功执行之后才能抵达。这在服务器端可以为每一步流程添加验证机制,只有验证通过才返回正确的页面。那么前端路由不能实现每一步的验证?自然不是,姑且相信你的代码可以写的很严谨,保证正常情况下流程不会错,但是另一个不得不面对的事实是:前端是毫无安全性可言的。用户可以肆意修改代码来进入不同的流程,你可能会为此添加不少的处理逻辑。相较之下,当然是后端控制页面的进入权限更为安全和简便。
另一方面,后端路由无疑增加了服务器端的负荷,并且需要reload页面,用户体验其实不佳。
前端路由的出现
在 90s 年代初,大多数的网页都是通过直接返回 HTML
的,用户的每次更新操作都需要重新刷新页面。及其影响交互体验,随着网络的发展,迫切需要一种方案来改善这种情况。
1996,微软首先提出 iframe 标签, iframe
带来了异步加载和请求元素的概念,随后在 1998 年,微软的 Outloook Web App 团队提出 Ajax
的基本概念(XMLHttpRequest的前身),并在 IE5
通过 ActiveX
来实现了这项技术。在微软实现这个概念后,其他浏览器比如 Mozilia
, Safari
, Opera
相继以 XMLHttpRequest
来实现 Ajax
。(:sob: 兼容问题从此出现,话说微软命名真喜欢用X,MFC源码一大堆。。)不过在 IE7 发布时,微软选择了妥协,兼容了 XMLHttpRequest
的实现。
有了 Ajax
后,用户交互就不用每次都刷新页面,体验带来了极大的提升。
但真正让这项技术发扬光大的,(。・∀・)ノ゙还是后来的 Google Map,它的出现向人们展现了 Ajax
的真正魅力,释放了众多开发人员的想象力,让其不仅仅局限于简单的数据和页面交互,为后来异步交互体验方式的繁荣发展带来了根基。
而异步交互体验的更高级版本就是我们熟知的 SPA
, SPA
不单单在页面交互上做到了不刷新,而且在页面之间跳转也做到了不刷新,为了做到这一点,就促使了前端路由的诞生。
前端路由的实现方式
前端路由其实只要解决两个问题:
- 在页面不刷新的前提下实现url变化
-
捕捉到url的变化,以便执行页面替换逻辑
在 2014 年之前,大家是通过 hash 来实现路由,url hash 就是类似于:http://www.xxx.com/#/login
这种 #。后面 hash
值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。另外每次 hash
值的变化,还会触发 hashchange
这个事件,通过这个事件我们就可以知道 hash
值发生了哪些变化。然后我们便可以监听 hashchange
来实现更新页面部分内容的操作:
function matchAndUpdate () { // todo 匹配 hash 做 dom 更新操作 } window.addEventListener('hashchange', matchAndUpdate)
后来,因为 HTML5
标准发布。多了两个 API, pushState
和 replaceState
,通过这两个 API
可以改变 url
地址且不会发送请求。同时还有 popstate
事件。通过这些就能用另一种方式来实现前端路由了,但原理都是跟 hash
实现相同的。用了 HTML5
的实现,单页路由的 url
就不会多出一个#,变得更加美观。但因为没有 # 号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面:
function matchAndUpdate () { // todo 匹配路径 做 dom 更新操作 } window.addEventListener('popstate', matchAndUpdate)
Vue-Router的实现方式
Vue-Router
跟 Vuex
一样都是通过 Vue.use
这个全局 API
来注册的,这个方法定义在 vue/src/core/global-api/use.js
:
/* @flow */ import { toArray } from '../util/index' export function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])) if (installedPlugins.indexOf(plugin) > -1) { return this } // additional parameters const args = toArray(arguments, 1) args.unshift(this) if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, args) } installedPlugins.push(plugin) return this } }
Vue.use
接受一个 plugin
参数,并且维护了一个 _installedPlugins
数组,它存储所有注册过的 plugin
;如果 plugin
是一个对象,会判断 plugin
有没有定义 install
方法,如果有的话则调用该方法,并且该方法执行的第一个参数是 Vue
;如果 plugin
是一个函数,它会被作为 install
方法,最后把 plugin
存储到 installedPlugins
数组里面, Vue
的这种插件注册的机制有一个好处就是我们不需要额外的去 import Vue
了。
路由的注册
Vue-Router
的入口在 src/index.js
,其中 install
方法定义在 src/install.js
,可以看一下 src
下面的目录结构:
├── components │ ├── link.js │ └── view.js ├── create-matcher.js ├── create-route-map.js ├── history │ ├── abstract.js │ ├── base.js │ ├── hash.js │ └── html5.js ├── index.js ├── install.js └── util ├── async.js ├── dom.js ├── location.js ├── misc.js ├── params.js ├── path.js ├── push-state.js ├── query.js ├── resolve-components.js ├── route.js ├── scroll.js └── warn.js
简单看下 install
的流程:
import View from './components/view' import Link from './components/link' export let _Vue export function install (Vue) { // 确保Vue-Router只被install一次 if (install.installed && _Vue === Vue) return install.installed = true _Vue = Vue const isDef = v => v !== undefined const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { i(vm, callVal) } } Vue.mixin({ // 在beforeCreate钩子里面初始化路由 beforeCreate () { // 根组件的$options上才有router对象 if (isDef(this.$options.router)) { // 设置根路由 this._routerRoot = this // 获取到根组件上的router实例 this._router = this.$options.router // 路由初始化 this._router.init(this) // 为_route属性实现双向绑定 Vue.util.defineReactive(this, '_route', this._router.history.current) } else { // 获取父组件的_routerRoot this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } // 注册<router-view></router-view>实例的钩子 registerInstance(this, this) }, destroyed () { registerInstance(this) } }) // 方便全局通过this.$router获取路由实例 Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) // 方便全局通过this.$route获取路由对象 Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) // 注册全局组件<router-view/>和<router-link/> Vue.component('RouterView', View) Vue.component('RouterLink', Link) // 使用和created相同的合并策略 const strats = Vue.config.optionMergeStrategies // use the same hook merging strategy for route hooks strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created }
首先通过设立一个 installed
标志位来确保 Vue-Router
只被安装一次,然后通过变量 _Vue
承载传入的 Vue
实例,然后利用 Vue.mixin
向每一个 Vue
实例注册 beforeCreate
和 destroyed
钩子函数。
在 beforeCreate
函数里面,如果是根组件,将根组件赋值给 this._routerRoot
,获取根组件的路由实例之后执行 init
初始化函数,然后调用 Vue
的 defineReactive
将 _route
变为响应式对象,如果不是根组件则获取父组件的 _routerRoot
属性。
在 beforeCreate
函数的最后部分和 destroyed
函数里面都执行了 registerInstance
函数,这个函数是注册 <router-view>
实例的钩子函数,根据传入参数的个数来决定是注册还是取消注册,函数的定义在 src/components/view.js
里面:
// attach instance registration hook // this will be called in the instance's injected lifecycle hooks data.registerRouteInstance = (vm, val) => { // val could be undefined for unregistration const current = matched.instances[name] if ( (val && current !== vm) || (!val && current === vm) ) { matched.instances[name] = val } }
回到 install.js
,紧接着,为了让我们能够全局的使用 this.$router
和 this.$route
在 Vue
原型上定义了对应的 get
方法,然后通过 Vue.component
注册了全局组件 注册全局组件<router-view/>
和 和<router-link/>
,最后定义了一些钩子函数的使用策略,这就是整个 Vue-Router
的安装过程。
路由的实例化
先看一下 Vue-Router
的构造函数,当我们 new
一个 Vue-Router
的时候都干了些什么:
/* @flow */ import { install } from './install' import { START } from './util/route' import { assert } from './util/warn' import { inBrowser } from './util/dom' import { cleanPath } from './util/path' import { createMatcher } from './create-matcher' import { normalizeLocation } from './util/location' import { supportsPushState } from './util/push-state' import { HashHistory } from './history/hash' import { HTML5History } from './history/html5' import { AbstractHistory } from './history/abstract' import type { Matcher } from './create-matcher' export default class VueRouter { static install: () => void; static version: string; app: any; apps: Array<any>; ready: boolean; readyCbs: Array<Function>; options: RouterOptions; mode: string; history: HashHistory | HTML5History | AbstractHistory; matcher: Matcher; fallback: boolean; beforeHooks: Array<?NavigationGuard>; resolveHooks: Array<?NavigationGuard>; afterHooks: Array<?AfterNavigationHook>; constructor (options: RouterOptions = {}) { // 根Vue实例 this.app = null // 存储含有this.$options.router属性的Vue实例 this.apps = [] // 传入路由的配置 this.options = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] // 创建路由匹配对象 this.matcher = createMatcher(options.routes || [], this) // 默认为hash模式 let mode = options.mode || 'hash' // 当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false if (this.fallback) { mode = 'hash' } // 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式 if (!inBrowser) { mode = 'abstract' } this.mode = mode // 根据mode采用不同的路由方式 switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } } match ( raw: RawLocation, current?: Route, redirectedFrom?: Location ): Route { return this.matcher.match(raw, current, redirectedFrom) } get currentRoute (): ?Route { return this.history && this.history.current } init (app: any /* Vue component instance */) { // 在初始化Vue-Router之前必须先通过Vue.use(VueRouter)注册 process.env.NODE_ENV !== 'production' && assert( install.installed, `not installed. Make sure to call \`Vue.use(VueRouter)\` ` + `before creating root instance.` ) this.apps.push(app) // set up app destroyed handler // https://github.com/vuejs/vue-router/issues/2639 app.$once('hook:destroyed', () => { // clean out app from this.apps array once destroyed const index = this.apps.indexOf(app) if (index > -1) this.apps.splice(index, 1) // ensure we still have a main app or null if no apps // we do not release the router so it can be reused if (this.app === app) this.app = this.apps[0] || null }) // main app previously initialized // return as we don't need to set up new history listener if (this.app) { return } this.app = app const history = this.history if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) } beforeEach (fn: Function): Function { return registerHook(this.beforeHooks, fn) } beforeResolve (fn: Function): Function { return registerHook(this.resolveHooks, fn) } afterEach (fn: Function): Function { return registerHook(this.afterHooks, fn) } onReady (cb: Function, errorCb?: Function) { this.history.onReady(cb, errorCb) } onError (errorCb: Function) { this.history.onError(errorCb) } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.history.push(location, onComplete, onAbort) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.history.replace(location, onComplete, onAbort) } go (n: number) { this.history.go(n) } back () { this.go(-1) } forward () { this.go(1) } getMatchedComponents (to?: RawLocation | Route): Array<any> { const route: any = to ? to.matched ? to : this.resolve(to).route : this.currentRoute if (!route) { return [] } return [].concat.apply([], route.matched.map(m => { return Object.keys(m.components).map(key => { return m.components[key] }) })) } resolve ( to: RawLocation, current?: Route, append?: boolean ): { location: Location, route: Route, href: string, // for backwards compat normalizedTo: Location, resolved: Route } { current = current || this.history.current const location = normalizeLocation( to, current, append, this ) const route = this.match(location, current) const fullPath = route.redirectedFrom || route.fullPath const base = this.history.base const href = createHref(base, fullPath, this.mode) return { location, route, href, // for backwards compat normalizedTo: location, resolved: route } } addRoutes (routes: Array<RouteConfig>) { this.matcher.addRoutes(routes) if (this.history.current !== START) { this.history.transitionTo(this.history.getCurrentLocation()) } } } function registerHook (list: Array<any>, fn: Function): Function { list.push(fn) return () => { const i = list.indexOf(fn) if (i > -1) list.splice(i, 1) } } function createHref (base: string, fullPath: string, mode) { var path = mode === 'hash' ? '#' + fullPath : fullPath return base ? cleanPath(base + '/' + path) : path } VueRouter.install = install VueRouter.version = '__VERSION__' // 通过link标签引用js的实行自动注册 if (inBrowser && window.Vue) { window.Vue.use(VueRouter) }
构造函数里面定义了一些属性,其中 this.app
表示根 Vue
的实例, this.apps
存储含有 this.$options.router
属性的Vue实例,初始化 Vue-Router
后传入的配置都会存储在 this.options
, this.beforeHooks
、 this.resolveHooks
、 this.afterHooks
用来存储钩子函数, this.matcher
是路由匹配后返回的对象, this.fallback
会根据配置的 mode
参数以及浏览器支持度来决定给是否回退到 hash
模式, this.mode
就是路由创建的模式,这里提供 hash
、 history
、 abstract
三种模式, this.history
表示根据不同的路由模式来创建的路由 history
的具体实现方式。
实例化 Vue-Router
之后会返回它的实例 router
,我们在使用 Vue-Router
的时候需要在初始化 Vue
的时候传入这个 router
属性:
new Vue({ el: '#app', router, render: h => h(App) })
这个时候会把 router
属性配置到 this.$options
,回想到 install.js
里面在 beforeCreate
钩子函数里面执行的方法:
beforeCreate () { // 根组件的$options上才有router对象 if (isDef(this.$options.router)) { // 设置根路由 this._routerRoot = this // 获取到根组件上的router实例 this._router = this.$options.router // 路由初始化 this._router.init(this) // .... }
所以这个时候会执行 init
方法:
init (app: any /* Vue component instance */) { // 在初始化Vue-Router之前必须先通过Vue.use(VueRouter)注册 process.env.NODE_ENV !== 'production' && assert( install.installed, `not installed. Make sure to call \`Vue.use(VueRouter)\` ` + `before creating root instance.` ) // 存储app实例 this.apps.push(app) // set up app destroyed handler // https://github.com/vuejs/vue-router/issues/2639 app.$once('hook:destroyed', () => { // clean out app from this.apps array once destroyed const index = this.apps.indexOf(app) if (index > -1) this.apps.splice(index, 1) // ensure we still have a main app or null if no apps // we do not release the router so it can be reused if (this.app === app) this.app = this.apps[0] || null }) // main app previously initialized // return as we don't need to set up new history listener if (this.app) { return } this.app = app const history = this.history // 根据history实现的方式不同执行不同的逻辑 if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } // 更新根组件的路由对象 history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) }
init
其实没干很多事情,首先把传入的 Vue
实例存储到 apps
数组中,然后把 this.history
赋值给一个本地变量,根据 this.history
实现方式的不同执行不同的逻辑,最后通过 history
的回调更新路由对象也就是 this.$route
。
无论 this.history
是基于 history
还是 hash
实现的,最后都会调用 transitionTo
方法,这个方法定义在 src/history/base.js
:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { const route = this.router.match(location, this.current) this.confirmTransition(route, () => { // ... }
实际上就是调用 match
方法:
match ( raw: RawLocation, current?: Route, redirectedFrom?: Location ): Route { return this.matcher.match(raw, current, redirectedFrom) }
那我们可以先把上面的逻辑放一边,先了解一下 matchers
的构建,相关的源码在 src/create-matcher.js
:
match的实现
/* @flow */ import type VueRouter from './index' import { resolvePath } from './util/path' import { assert, warn } from './util/warn' import { createRoute } from './util/route' import { fillParams } from './util/params' import { createRouteMap } from './create-route-map' import { normalizeLocation } from './util/location' export type Matcher = { match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route; addRoutes: (routes: Array<RouteConfig>) => void; }; export function createMatcher ( routes: Array<RouteConfig>, router: VueRouter ): Matcher { const { pathList, pathMap, nameMap } = createRouteMap(routes) // 添加路由路径关系映射 function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) } function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { // 根据raw和currentRoute计算出新的location const location = normalizeLocation(raw, currentRoute, false, router) const { name } = location if (name) { // 如果是命名路由,取出对应的路由record const record = nameMap[name] if (process.env.NODE_ENV !== 'production') { warn(record, `Route with name '${name}' does not exist`) } // 生成一条新记录 if (!record) return _createRoute(null, location) const paramNames = record.regex.keys .filter(key => !key.optional) .map(key => key.name) if (typeof location.params !== 'object') { location.params = {} } // 赋值params if (currentRoute && typeof currentRoute.params === 'object') { for (const key in currentRoute.params) { if (!(key in location.params) && paramNames.indexOf(key) > -1) { location.params[key] = currentRoute.params[key] } } } if (record) { location.path = fillParams(record.path, location.params, `named route "${name}"`) return _createRoute(record, location, redirectedFrom) } } else if (location.path) { location.params = {} for (let i = 0; i < pathList.length; i++) { const path = pathList[i] const record = pathMap[path] if (matchRoute(record.regex, location.path, location.params)) { return _createRoute(record, location, redirectedFrom) } } } // no match return _createRoute(null, location) } function redirect ( record: RouteRecord, location: Location ): Route { const originalRedirect = record.redirect let redirect = typeof originalRedirect === 'function' ? originalRedirect(createRoute(record, location, null, router)) : originalRedirect if (typeof redirect === 'string') { redirect = { path: redirect } } if (!redirect || typeof redirect !== 'object') { if (process.env.NODE_ENV !== 'production') { warn( false, `invalid redirect option: ${JSON.stringify(redirect)}` ) } return _createRoute(null, location) } 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 if (name) { // resolved named direct const targetRecord = nameMap[name] 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) { // 1. resolve relative redirect const rawPath = resolveRecordPath(path, record) // 2. resolve 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: ${JSON.stringify(redirect)}`) } return _createRoute(null, location) } } function alias ( record: RouteRecord, location: Location, matchAs: string ): Route { const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`) const aliasedMatch = match({ _normalized: true, path: aliasedPath }) if (aliasedMatch) { const matched = aliasedMatch.matched const aliasedRecord = matched[matched.length - 1] location.params = aliasedMatch.params return _createRoute(aliasedRecord, location) } return _createRoute(null, location) } function _createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: Location ): Route { if (record && record.redirect) { return redirect(record, redirectedFrom || location) } if (record && record.matchAs) { return alias(record, location, record.matchAs) } return createRoute(record, location, redirectedFrom, router) } return { match, addRoutes } } function matchRoute ( regex: RouteRegExp, path: string, params: Object ): boolean { const m = path.match(regex) if (!m) { return false } else if (!params) { return true } for (let i = 1, len = m.length; i < len; ++i) { const key = regex.keys[i - 1] 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 true } function resolveRecordPath (path: string, record: RouteRecord): string { return resolvePath(path, record.parent ? record.parent.path : '/', true) }
createMatcher
接受两个参数,第一个是初始化路由的配置对象 routes
,第二个是我们的路由实例 router
,首先会跑一个 createRouteMap
的逻辑,这个方法的作用是创建一个路由映射,这个方法定义在 src/create-route-map.js
:
/* @flow */ import Regexp from 'path-to-regexp' import { cleanPath } from './util/path' import { assert, warn } from './util/warn' export function createRouteMap ( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord> ): { pathList: Array<string>; pathMap: Dictionary<RouteRecord>; nameMap: Dictionary<RouteRecord>; } { // the path list is used to control path matching priority const pathList: Array<string> = oldPathList || [] // $flow-disable-line const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null) // $flow-disable-line const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null) routes.forEach(route => { addRouteRecord(pathList, pathMap, nameMap, route) }) // 确保通配符的路径在最后才被匹配 // ensure wildcard routes are always at the end for (let i = 0, l = pathList.length; i < l; i++) { if (pathList[i] === '*') { pathList.push(pathList.splice(i, 1)[0]) l-- i-- } } return { pathList, pathMap, nameMap } } function addRouteRecord ( pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) { const { path, name } = route if (process.env.NODE_ENV !== 'production') { // path不能为空并且component的值必须用一个组件名而不是一个string字符串 assert(path != null, `"path" is required in a route configuration.`) assert( typeof route.component !== 'string', `route config "component" for path: ${String(path || name)} cannot be a ` + `string id. Use an actual component instead.` ) } // pathToRegexpOptions是编译正则的选项 const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {} // normalize path const normalizedPath = normalizePath( path, parent, pathToRegexpOptions.strict ) // caseSensitive匹配规则是否大小写敏感?(默认值:false) if (typeof route.caseSensitive === 'boolean') { pathToRegexpOptions.sensitive = route.caseSensitive } const record: RouteRecord = { path: normalizedPath, regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), components: route.components || { default: route.component }, instances: {}, name, parent, matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {}, props: route.props == null ? {} : route.components ? route.props : { default: route.props } } // 如果有子路由 if (route.children) { // Warn if route is named, does not redirect and has a default child route. // If users navigate to this route by name, the default child will // not be rendered (GH Issue #629) if (process.env.NODE_ENV !== 'production') { if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) { warn( false, `Named Route '${route.name}' has a default child route. ` + `When navigating to this named route (:to="{name: '${route.name}'"), ` + `the default child route will not be rendered. Remove the name from ` + `this route and use the name of the default child route for named ` + `links instead.` ) } } // 循环添加子路由路径 route.children.forEach(child => { const childMatchAs = matchAs ? cleanPath(`${matchAs}/${child.path}`) : undefined addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs) }) } // 设置了路由别名 if (route.alias !== undefined) { // 统一转换为数组 const aliases = Array.isArray(route.alias) ? route.alias : [route.alias] aliases.forEach(alias => { const aliasRoute = { path: alias, children: route.children } addRouteRecord( pathList, pathMap, nameMap, aliasRoute, parent, record.path || '/' // matchAs ) }) } // 添加path记录以及建立path和记录的对应关系 if (!pathMap[record.path]) { pathList.push(record.path) pathMap[record.path] = record } // 如果配置了命名路由,给name和record建立映射关系 if (name) { if (!nameMap[name]) { nameMap[name] = record } else if (process.env.NODE_ENV !== 'production' && !matchAs) { // 已经有这个命名的路由存在并且没有给这个路由设置重定向,说明给了重复命名 warn( false, `Duplicate named routes definition: ` + `{ name: "${name}", path: "${record.path}" }` ) } } } function compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptions): RouteRegExp { const regex = Regexp(path, [], pathToRegexpOptions) if (process.env.NODE_ENV !== 'production') { const keys: any = Object.create(null) regex.keys.forEach(key => { // 有重复的动态路径参数 warn(!keys[key.name], `Duplicate param keys in route with path: "${path}"`) keys[key.name] = true }) } return regex } function normalizePath (path: string, parent?: RouteRecord, strict?: boolean): string { // 替换根路由路径 if (!strict) path = path.replace(/\/$/, '') if (path[0] === '/') return path if (parent == null) return path // 将//的路径替换成/ return cleanPath(`${parent.path}/${path}`) }
这个方法的作用是将路由配置转换成一组组映射关系表,返回一个对象:
return { pathList, pathMap, nameMap }
其中 pathList
存储了所有的 path
, pathMap
表示了 path
到 RouteRecord
对象的一一映射关系, nameMap
则表示了 name
到 RouteRecord
对象的映射关系, RouteRecord
对象是对路由配置参数 routes
每一项进行遍历后调用 addRouteRecord
方法生成的一条记录,方法定义如下:
function addRouteRecord ( pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) { const { path, name } = route if (process.env.NODE_ENV !== 'production') { // path不能为空并且component的值必须用一个组件名而不是一个string字符串 assert(path != null, `"path" is required in a route configuration.`) assert( typeof route.component !== 'string', `route config "component" for path: ${String(path || name)} cannot be a ` + `string id. Use an actual component instead.` ) } // pathToRegexpOptions是编译正则的选项 const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {} // normalize path const normalizedPath = normalizePath( path, parent, pathToRegexpOptions.strict ) // caseSensitive匹配规则是否大小写敏感?(默认值:false) if (typeof route.caseSensitive === 'boolean') { pathToRegexpOptions.sensitive = route.caseSensitive } const record: RouteRecord = { path: normalizedPath, regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), components: route.components || { default: route.component }, instances: {}, name, parent, matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {}, props: route.props == null ? {} : route.components ? route.props : { default: route.props } } // 如果有子路由 if (route.children) { // Warn if route is named, does not redirect and has a default child route. // If users navigate to this route by name, the default child will // not be rendered (GH Issue #629) if (process.env.NODE_ENV !== 'production') { if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) { warn( false, `Named Route '${route.name}' has a default child route. ` + `When navigating to this named route (:to="{name: '${route.name}'"), ` + `the default child route will not be rendered. Remove the name from ` + `this route and use the name of the default child route for named ` + `links instead.` ) } } // 循环添加子路由路径 route.children.forEach(child => { const childMatchAs = matchAs ? cleanPath(`${matchAs}/${child.path}`) : undefined addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs) }) } // 设置了路由别名 if (route.alias !== undefined) { // 统一转换为数组 const aliases = Array.isArray(route.alias) ? route.alias : [route.alias] aliases.forEach(alias => { const aliasRoute = { path: alias, children: route.children } addRouteRecord( pathList, pathMap, nameMap, aliasRoute, parent, record.path || '/' // matchAs ) }) } // 添加path记录以及建立path和记录的对应关系 if (!pathMap[record.path]) { pathList.push(record.path) pathMap[record.path] = record } // 如果配置了命名路由,给name和record建立映射关系 if (name) { if (!nameMap[name]) { nameMap[name] = record } else if (process.env.NODE_ENV !== 'production' && !matchAs) { // 已经有这个命名的路由存在并且没有给这个路由设置重定向,说明给了重复命名 warn( false, `Duplicate named routes definition: ` + `{ name: "${name}", path: "${record.path}" }` ) } } }
这个方法首先会对 path
使用 normalizePath
进行规范化处理,我们看一下这个方法:
function normalizePath (path: string, parent?: RouteRecord, strict?: boolean): string { // 替换根路由路径 if (!strict) path = path.replace(/\/$/, '') // 说明是一级路径,直接返回 if (path[0] === '/') return path if (parent == null) return path // 将//的路径替换成/并且拼接父路由的路径 return cleanPath(`${parent.path}/${path}`) }
主要作用就是生成多层路由的具体路径,然后根据已有参数构建 RouteRecord
:
const record: RouteRecord = { path: normalizedPath, regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), components: route.components || { default: route.component }, instances: {}, name, parent, matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {}, props: route.props == null ? {} : route.components ? route.props : { default: route.props } }
这里解释下 regex
这个参数,用到了 path-to-regexp
这个库,这个库可以把路径转换为正则表达式,举个栗子:
const keys = [] const regexp = pathToRegexp('/foo/:bar', keys) // regexp = /^\/foo\/([^\/]+?)\/?$/i // keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
用这个库作为路径匹配引擎是为了实现可选的动态路径参数、匹配零个或多个、一个或多个,甚至是自定义正则匹配。
紧接着判断是否配置了子路由,然后循环调用 addRouteRecord
这个方法,并把当前的 record
作为 parent
,如果设置了路由别名,也会给别名添加一份 record
,最后就是更新映射表,返回一个 Array
对象以及两个 Dictionary
对象。
回到 create-matcher.js
,它对外暴露了两个方法: addRoutes
和 match
,分别用于动态添加路由配置以及返回一个路由的路径,先看 addRoutes
:
function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) }
其实就是在现有的 pathList
、 pathMap
、 nameMap
上动态添加一条新纪录,这几个都是引用类型,执行 addRoutes
之后都会被修改。
match
函数相对复杂一点,接受三个参数,第一个参数可以为 string
也可以是一个 Location
对象,第二个参数表示当前的路由路径,第三个参数也是 Location
对象,跟重定向有关:
function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { // 根据raw和currentRoute计算出新的location const location = normalizeLocation(raw, currentRoute, false, router) const { name } = location if (name) { // 如果是命名路由,取出对应的路由record const record = nameMap[name] if (process.env.NODE_ENV !== 'production') { warn(record, `Route with name '${name}' does not exist`) } // 生成一条新记录 if (!record) return _createRoute(null, location) const paramNames = record.regex.keys .filter(key => !key.optional) .map(key => key.name) if (typeof location.params !== 'object') { location.params = {} } // 赋值params if (currentRoute && typeof currentRoute.params === 'object') { for (const key in currentRoute.params) { if (!(key in location.params) && paramNames.indexOf(key) > -1) { location.params[key] = currentRoute.params[key] } } } if (record) { location.path = fillParams(record.path, location.params, `named route "${name}"`) return _createRoute(record, location, redirectedFrom) } } else if (location.path) { location.params = {} for (let i = 0; i < pathList.length; i++) { const path = pathList[i] const record = pathMap[path] if (matchRoute(record.regex, location.path, location.params)) { return _createRoute(record, location, redirectedFrom) } } } // no match return _createRoute(null, location) }
一开始会执行 normalizeLocation
方法,返回一个新的 location
,看一眼 normalizeLocation
的实现:
export function normalizeLocation ( raw: RawLocation, current: ?Route, append: ?boolean, router: ?VueRouter ): Location { let next: Location = typeof raw === 'string' ? { path: raw } : raw // named target if (next._normalized) { // 已经normalized的直接返回 return next } else if (next.name) { // 如果是命名路由,返回一份备份 return extend({}, raw) } // relative params // 没有path,但是有params和current if (!next.path && next.params && current) { next = extend({}, next) next._normalized = true // 拿到params const params: any = extend(extend({}, current.params), next.params) if (current.name) { next.name = current.name next.params = params } else if (current.matched.length) { const rawPath = current.matched[current.matched.length - 1].path // 根据rawPath和params计算出当前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 } // 将path拆分成path、hash和query const parsedPath = parsePath(next.path || '') const basePath = (current && current.path) || '/' // 返回最后拼接完成好的路径,append用于判断是否在当前 (相对) 路径前添加基路径 const path = parsedPath.path ? resolvePath(parsedPath.path, basePath, append || next.append) : basePath // 解析query parseQuery是提供自定义查询字符串的解析/反解析函数,用于覆盖默认行为 const query = resolveQuery( parsedPath.query, next.query, router && router.options.parseQuery ) // 路由的hash值 let hash = next.hash || parsedPath.hash if (hash && hash.charAt(0) !== '#') { hash = `#${hash}` } return { _normalized: true, path, query, hash } }
这个方法首先会判断当前的 RawLocation
是否已经经过 _normalized
处理,是的话直接返回,否则的话继续判断当前 Location
是否有 name
字段,有的话通过 extend
方法拷贝一份 raw
对象直接返回,这个 extend
方法的实现很简单:
export function extend (a, b) { for (const key in b) { a[key] = b[key] } return a }
当上面的情况都不满足,接着进入下一个判断条件,如果有当前 Route
信息,有 params
但是没有 path
的情况,首先会设置 _normalized
标志位,然后对 params
参数进行合并处理,然后继续分为两种情况处理,分别是 current
有 name
与否,前者的话会直接将 current
的 name
和拼接后的 params
赋值给 next
后直接返回,后者的话会从路由记录里面找到最新的一条记录的 path
,调用 fillParams
方法根据 rawPath
和 params
计算当前 path
,看一下 fillParams
对相对路径的处理:
/* @flow */ import { warn } from './warn' import Regexp from 'path-to-regexp' // $flow-disable-line const regexpCompileCache: { [key: string]: Function } = Object.create(null) export function fillParams ( path: string, params: ?Object, routeMsg: string ): string { params = params || {} try { const filler = regexpCompileCache[path] || (regexpCompileCache[path] = Regexp.compile(path)) // 如果param中有名为pathMatch的key将他设置为{0, params[patchMatch]}的键值对 // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }} if (params.pathMatch) params[0] = params.pathMatch // 将动态路径参数替换成正式参数 return filler(params, { pretty: true }) } catch (e) { if (process.env.NODE_ENV !== 'production') { warn(false, `missing param for ${routeMsg}: ${e.message}`) } return '' } finally { // delete the 0 if it was added delete params[0] } }
其实这里就是把形如 {zapId: 1}
的params参数通过 Regexp.compile
生成的方法拼接到 path
后面,就像这样:
const toPath = pathToRegexp.compile('/user/:id') toPath({ id: 123 }) //=> "/user/123"
回到 create-matcher.js
,计算出新的 location
之后对命名路由和非命名路由进行了不同的处理,如果 name
存在,从 nameMap
字典里面匹配出对应的 record
,如果 record
不存在通过 _createRoute
生成一条新的 record
直接返回,否则取出这条 record
里面的 params
的 key
组成的数组,将 currentRoute
里面的 params
以 key
/ value
的形式不重复地存入名为 location.params
的一个对象里面,最后依然是通过 fillParams
拼接 params
参数到路径尾部,通过 _createRoute
方法创建一条新路径返回。
反之,如果是非命名路由,会通过 pathList
返回 path
对应的 record
,然后通过 createRoute
方法判断是否能够匹配到路由信息,是的话也会通过 _createRoute
生成一条新路径返回。接下来只要搞懂 matchRoute
和 _createRoute
干了什么就行了,先看 matchRoute
:
function matchRoute ( regex: RouteRegExp, path: string, params: Object ): boolean { const m = path.match(regex) if (!m) { return false } else if (!params) { return true } for (let i = 1, len = m.length; i < len; ++i) { const key = regex.keys[i - 1] 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 true }
其实就是通过 match
方法做判断,如果没匹配到直接返回 false
,如果传入了 params
会将 path
里面的 params
以 key
/ value
形式存入,这个传入的 params
在这里是 location.params
所以和前面做的是一样的操作。
接着看 _createRoute
方法,这个方法也在 create_matcher.js
内部:
function _createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: Location ): Route { if (record && record.redirect) { return redirect(record, redirectedFrom || location) } if (record && record.matchAs) { return alias(record, location, record.matchAs) } return createRoute(record, location, redirectedFrom, router) }
无论是否设置了 redirect
还是 alias
最后都会重新调用 _createRoute
,所以这里直接看最后的 createRoute
方法:
/* @flow */ import type VueRouter from '../index' import { stringifyQuery } from './query' const trailingSlashRE = /\/?$/ export function createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: ?Location, router?: VueRouter ): Route { // 提供自定义查询字符串的解析/反解析函数。覆盖默认行为 const stringifyQuery = router && router.options.stringifyQuery let query: any = location.query || {} try { query = clone(query) } catch (e) {} const route: Route = { name: location.name || (record && record.name), meta: (record && record.meta) || {}, path: location.path || '/', hash: location.hash || '', query, params: location.params || {}, fullPath: getFullPath(location, stringifyQuery), // 完整路径 matched: record ? formatMatch(record) : [] } if (redirectedFrom) { route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery) } return Object.freeze(route) } function clone (value) { if (Array.isArray(value)) { return value.map(clone) } else if (value && typeof value === 'object') { const res = {} for (const key in value) { res[key] = clone(value[key]) } return res } else { return value } } // the starting route that represents the initial state export const START = createRoute(null, { path: '/' }) function formatMatch (record: ?RouteRecord): Array<RouteRecord> { const res = [] while (record) { res.unshift(record) record = record.parent } return res } function getFullPath ( { path, query = {}, hash = '' }, _stringifyQuery ): string { const stringify = _stringifyQuery || stringifyQuery return (path || '/') + stringify(query) + hash } export function isSameRoute (a: Route, b: ?Route): boolean { if (b === START) { return a === b } else if (!b) { return false } else if (a.path && b.path) { return ( a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') && a.hash === b.hash && isObjectEqual(a.query, b.query) ) } else if (a.name && b.name) { return ( a.name === b.name && a.hash === b.hash && isObjectEqual(a.query, b.query) && isObjectEqual(a.params, b.params) ) } else { return false } } function isObjectEqual (a = {}, b = {}): boolean { // handle null value #1566 if (!a || !b) return a === b const aKeys = Object.keys(a) const bKeys = Object.keys(b) if (aKeys.length !== bKeys.length) { return false } return aKeys.every(key => { const aVal = a[key] const bVal = b[key] // check nested equality if (typeof aVal === 'object' && typeof bVal === 'object') { return isObjectEqual(aVal, bVal) } return String(aVal) === String(bVal) }) } export function isIncludedRoute (current: Route, target: Route): boolean { return ( current.path.replace(trailingSlashRE, '/').indexOf( target.path.replace(trailingSlashRE, '/') ) === 0 && (!target.hash || current.hash === target.hash) && queryIncludes(current.query, target.query) ) } function queryIncludes (current: Dictionary<string>, target: Dictionary<string>): boolean { for (const key in target) { if (!(key in current)) { return false } } return true }
通过传入的 record
和 location
创建一个不可被修改的 Route
对象,其中有个 matched
属性通过 formatMatch
方法构建:
function formatMatch (record: ?RouteRecord): Array<RouteRecord> { const res = [] while (record) { res.unshift(record) record = record.parent } return res }
通过循环不断地查找当前 record
的 parent
,然后返回这条线上所有的 record
组成的数组。
路由的跳转
无论是 Hash
路由还是 History
路由,在初始化的时候都会通过 transitionTo
方法跳转到初始路径,这个方法也是我们切换路由路径时候使用的方法,下面分析一下该方法的实现过程:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { // 通过location和current返回初始化Route信息 const route = this.router.match(location, this.current) this.confirmTransition(route, () => { this.updateRoute(route) // 执行onComplete回调 onComplete && onComplete(route) this.ensureURL() // fire ready cbs once if (!this.ready) { this.ready = true this.readyCbs.forEach(cb => { cb(route) }) } }, err => { // 如果跳转被终止 if (onAbort) { onAbort(err) } if (err && !this.ready) { this.ready = true this.readyErrorCbs.forEach(cb => { cb(err) }) } }) }
初始化调用的时候,拿到的是初始化的 Route
,通过 confirmTransition
方法进行实际的跳转操作,同时对跳转成功和失败两种情况都设置了回调函数,看一下这个函数的定义:
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) { const current = this.current const abort = err => { if (isError(err)) { // 将所有的错误信息都存入errorCbs if (this.errorCbs.length) { this.errorCbs.forEach(cb => { cb(err) }) } else { warn(false, 'uncaught error during route navigation:') console.error(err) } } onAbort && onAbort(err) } if ( // 如果是同一个路由路径 isSameRoute(route, current) && // in the case the route map has been dynamically appended to route.matched.length === current.matched.length ) { // ensureURL在不同的路由实现方式里面该方法的实现不一样 this.ensureURL() // 如果设置了abort方法这里直接调用 return abort() } // 拿到路径的变化部分以及遗弃部分和升级部分 const { updated, deactivated, activated } = resolveQueue(this.current.matched, route.matched) // 维持一个对应路径变化的导航守卫的钩子组成的List const queue: Array<?NavigationGuard> = [].concat( // in-component leave guards extractLeaveGuards(deactivated), // global before hooks this.router.beforeHooks, // in-component update hooks extractUpdateHooks(updated), // in-config enter guards activated.map(m => m.beforeEnter), // async components resolveAsyncComponents(activated) ) this.pending = route // 定义一个迭代器 const iterator = (hook: NavigationGuard, next) => { if (this.pending !== route) { return abort() } try { hook(route, current, (to: any) => { if (to === false || isError(to)) { // next(false) -> abort navigation, ensure current URL this.ensureURL(true) abort(to) } else if ( typeof to === 'string' || (typeof to === 'object' && ( typeof to.path === 'string' || typeof to.name === 'string' )) ) { // next('/') or next({ path: '/' }) -> redirect abort() if (typeof to === 'object' && to.replace) { this.replace(to) } else { this.push(to) } } else { // 确认跳转,执行回调 // confirm transition and pass on the value next(to) } }) } catch (e) { abort(e) } } runQueue(queue, iterator, () => { const postEnterCbs = [] const isValid = () => this.current === route // wait until async components are resolved before // extracting in-component enter guards // 执行beforeRouteEnter钩子函数 const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) const queue = enterGuards.concat(this.router.resolveHooks) runQueue(queue, iterator, () => { if (this.pending !== route) { return abort() } this.pending = null onComplete(route) if (this.router.app) { this.router.app.$nextTick(() => { postEnterCbs.forEach(cb => { cb() }) }) } }) }) }
首先定义了一个 abort
函数用于处理路由跳转失败的情况以及执行 onAbort
回调,然后判断如果要跳转的路由和当前路由是同一个的话,直接调用 this.ensureURL()
和 abort()
,这个 ensureURL
在不同的路由实现方式里面该方法的实现不一样,最终都是做了路由跳转的操作,紧接着通过 resolveQueue
方法拿到三个数组,分别存储着固定的部分,遗弃的部分以及更新的部分,这个方法的实现如下:
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) for (i = 0; i < max; i++) { if (current[i] !== next[i]) { break } } return { updated: next.slice(0, i), activated: next.slice(i), deactivated: current.slice(i) } }
路由从 current
变为 next
,两个路径的公共部分就是 next.slice(0, i)
, next.slice(0, i)
就是路径需要更新的部分,而 current.slice(i)
就是路径需要变化的部分。
拿到三个数组之后会构造一个 queue
队列,里面存储了路径变化要执行的钩子函数,也就是官方说的路由守卫,会按次序执行一些诸如 beforeRouteLeave
、 beforeRouteUpdate
等方法,下面还会定义一个迭代器,这个迭代器会根据传入的 ro
参数来决定执行 abort
还是 next
方法,最后执行 runQueue
方法来执行这个队列,这个方法的定义如下:
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) { const step = index => { if (index >= queue.length) { cb() } else { if (queue[index]) { fn(queue[index], () => { step(index + 1) }) } else { step(index + 1) } } } step(0) }
这里的 fn
其实就是 iterator
里面的 next
函数,只有执行了 next
函数 index
才会 +1
,才会进行管道中的下一个钩子,如果全部钩子执行完了,则导航的状态会变成 confirmed
(确认的)。
最后可以看一下 Vue-Router
里面对导航的完整解析流程:
导航被触发。
在失活的组件里调用离开守卫。
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve 守卫 (2.5+)。
导航被确认。
调用全局的 afterEach 钩子。
触发 DOM 更新。
用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
总结
传统的路由实现是通过对路径的切换做到的,对于 Vue-Router
而言,路由模块的本质 就是建立起url和页面之间的映射关系。路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url
,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据.
参考链接: Vue核心解密
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Game Engine Architecture, Second Edition
Jason Gregory / A K Peters/CRC Press / 2014-8-15 / USD 69.95
A 2010 CHOICE outstanding academic title, this updated book covers the theory and practice of game engine software development. It explains practical concepts and techniques used by real game studios,......一起来看看 《Game Engine Architecture, Second Edition》 这本书的介绍吧!