vue-router源码解析-vue router详解

2023-08-07 20:07:40

 

vue-router 简介

Vue-Router 的能力十分强大,它支持 hash、history、abstract 3 种路由方式,提供了 <router-link> 和 <router-view> 2 种组件,还提供了简单的路由配置和一系列好用的 API。

简单使用流程

import Vue from vue import VueRouter from vue-router import App from ./App Vue.use(VueRouter) // 1. 定义(路由)组件。 // 可以从其他文件 import 进来 const Foo = { template: <div>foo</div> } const Bar = { template: <div>bar</div> } // 2. 定义路由 // 每个路由应该映射一个组件。 其中"component" 可以是 // 通过 Vue.extend() 创建的组件构造器, // 或者,只是一个组件配置对象。 const routes = [ { path: /foo, component: Foo }, { path: /bar, component: Bar }, ] // 3. 创建 router 实例,然后传 `routes` 配置 const router = new VueRouter({ routes, }) // 4. 创建和挂载根实例。 // 记得要通过 router 配置参数注入路由, // 从而让整个应用都有路由功能 const app = new Vue({ el: #app, render(h) { return h(App) }, router, })

路由的注册

Vue-Router 本质上就是一个路由的插件,vue 中用 Vue.use 来注册插件

Vue.use(plugin)

参数:{ Object | Function } plugin 可以是对象也可以是函数.

安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。调用 install 方法时,会将 Vue 作为参数传入。install 方法被同一个插件多次调用时,插件也只会被安装一次。

插件的类型,可以是 install 方法,也可以是一个包含 install 方法的对象。插件只能被安装一次,保证插件列表中不能有重复的插件。
// vue/src/core/global-api/use.js (vue.use源码) export function initUse(Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { // 接受一个 plugin 参数,可以是对象或者是函数 const installedPlugins = this._installedPlugins || (this._installedPlugins = []) // _installedPlugins 数组,它存储所有注册过的 plugin if (installedPlugins.indexOf(plugin) > -1) { // 判断插件是不是已经别注册过,如果被注册过,则直接终止方法执行,此时只需要使用indexOf方法即可。 return this } const args = toArray(arguments, 1) args.unshift(this) // toArray方法我们在就是将类数组转成真正的数组。使用toArray方法得到arguments。除了第一个参数之外,剩余的所有参数将得到的列表赋值给args,然后将Vue添加到args列表的最前面。这样做的目的是保证install方法被执行时第一个参数是Vue,其余参数是注册插件时传入的参数。 if (typeof plugin.install === function) { plugin.install.apply(plugin, args) } else if (typeof plugin === function) { plugin.apply(null, args) } // 由于plugin参数支持对象和函数类型,所以通过判断plugin.install和plugin哪个是函数,即可知用户使用哪种方式祖册的插件,然后执行用户编写的插件并将args作为参数传入。 installedPlugins.push(plugin) // 最后,将插件添加到installedPlugins中,保证相同的插件不会反复被注册 return this } }

当我们执行 Vue.use 注册插件的时候,就会执行这个 install 方法,并且在这个 install 方法的第一个参数我们可以拿到 Vue 对象,这样的好处就是作为插件的编写方不需要再额外去 import Vue 了。

路由安装 intall 方法

在路由源码的 src/install.js 文件中

import View from ./components/view import Link from ./components/link export let _Vue export function install(Vue) { // Vue.use()的时候会把vue传入第一个参数,这里的vue参数就是vue中传入的 // 重复判断 如果installed是true说明已经注册过了 直接返回 if (install.installed && _Vue === Vue) return install.installed = true // 内部需要一个变量来存Vue ,因为作为 Vue 的插件对 Vue 对象是有依赖的 _Vue = Vue const isDef = (v) => v !== undefined // 为router-view组件关联路由组件 const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode // 而这个方法只在router-view组件中存在,router-view组件定义在(../components/view.js) // 所以,如果vm的父节点为router-view,则为router-view关联当前vm,即将当前vm做为router-view的路由组件 // 调用vm.$options._parentVnode.data.registerRouteInstance方法 if ( isDef(i) && isDef((i = i.data)) && isDef((i = i.registerRouteInstance)) ) { i(vm, callVal) } } // mixin的作用是将mixin的内容混合到Vue的初始参数options中 Vue.mixin({ // this === new Vue({router:router}) === Vue根实例 // 为什么是beforeCreate而不是created呢?因为如果是在created操作的话,$options已经初始化好了。 beforeCreate() { // 如果判断当前组件是根组件的话,就将我们传入的router和_root挂在到根组件实例上。判断是否使用了vue-router插件 if (isDef(this.$options.router)) { this._routerRoot = this // 保存VueRouter实例,this.$options.router仅存在于Vue根实例上,其它Vue组件不包含此属性,所以下面的初始化,只会执行一次 this._router = this.$options.router this._router.init(this) // 响应式定义_route属性,保证_route发生变化时,组件(router-view)会重新渲染 Vue.util.defineReactive(this, _route, this._router.history.current) } else { // 如果判断当前组件是子组件的话,就将我们_root根组件挂载到子组件。注意是引用的复制,因此每个组件都拥有了同一个_root根组件挂载在它身上。 this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } // 为router-view组件关联路由组件 registerInstance(this, this) }, destroyed() { // 取消router - view和路由组件的关联 registerInstance(this) }, }) Object.defineProperty(Vue.prototype, $router, { // 将$router挂载到组件实例上 this.$router get() { return this._routerRoot._router }, }) // $router是VueRouter的实例对象,$route是当前路由对象,也就是说$route是$router的一个属性 Object.defineProperty(Vue.prototype, $route, { // 每个组件访问到的this.$route,其实最后访问的都是Vue根实例的_route get() { return this._routerRoot._route }, }) // 注册router-view、router-link全局组件 Vue.component(RouterView, View) Vue.component(RouterLink, Link) const strats = Vue.config.optionMergeStrategies //设置路由组件守卫的合并策略 strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created }

当用户执行 Vue.use(VueRouter) 的时候,实际上就是在执行实例 VueRouter 上的 install 函数,所以注册方法完成了以下事情:

避免重复安装:通过添加 installed 为 true 标识来判断是否重复安装存储 Vue 后续依赖使用:install 方法被调用时, Vue.use()的时候会把 vue 传入第一个参数,Vue 会被赋值给事先定义好的_Vue 变量注册了一个全局混入:注册了两个生命周期钩子 beforeCreate 和 destroyed,因为是 vue 的 mixin 方法,所以注册时不会调用,只有 vue 执行钩子时调用添加实例属性、方法:在 Vue 原型上注入$router、$route 属性,方便在 vue 实例中通过 this.$router、this.$route 快捷访问注册 router-view、router-link 全局组件设置路由组件守卫的合并策略

VueRouter 的构造函数

src/index.js 看下 VueRouter 构造函数

先看构造函数的 constructor 构造函数 内部的实现
export default class VueRouter { static install: () => void static version: string static isNavigationFailure: Function static NavigationFailureType: any static START_LOCATION: Route 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 = {}) { if (process.env.NODE_ENV !== production) { warn( this instanceof VueRouter, `Router must be called with the new operator.` ) } this.app = null // 保存挂载实例 this.apps = [] // VueRouter支持多实例 this.options = options //接收的参数 this.beforeHooks = [] // 接收 beforeEach hook this.resolveHooks = [] // 接收 beforeResolve hook this.afterHooks = [] // 接收 afterEach hook this.matcher = createMatcher(options.routes || [], this) // 路由匹配器,创建路由matcher对象,传入routes路由配置列表及VueRouter实例 let mode = options.mode || hash // 默认hash模式 this.fallback = mode === history && !supportsPushState && options.fallback !== false // 表示在浏览器不支持 history.pushState 的情况下,根据传入的 fallback 配置参数,决定是否回退到hash模式 if (this.fallback) { // 如果可以回退就是hash模式 mode = hash } // 非浏览器环境,强制使用abstract模式 if (!inBrowser) { mode = abstract } this.mode = mode switch (mode) { // 根据不同mode,实例化不同history实例 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}`) } } } }

VueRouter 构造函数主要干了下面几件事:

接收 RouterOptions 类型的参数 options,也就是我们通常配置的路由信息表,主要是 mode,routes 等;对一些属性赋予了初值,保存 vue 实例,接收全局导航守卫(beforeEach、beforeResolve、afterEach)的数组做了初始化创建了路由匹配器,路由matcher对象,传入routes路由配置列表及VueRouter实例确定路由模式,先设置默认值,如果options.mode没有配置的话默认就是hash模式;接下来fallback判断 如果是mode:history模式 但是不支持pushState,回退到hash模式;非浏览器模式下强制abstract模式根据路由模式生成不同的History实例

匹配器 matcher

在构造函数内

this.matcher = createMatcher(options.routes || [], this)

createMatcher的实现在src/create-matcher.js中

// matcher 的数据结构 export type Matcher = { match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route; //匹配方法 addRoutes: (routes: Array<RouteConfig>) => void; // 添加Route方法 getRoutes: () => Array<RouteRecord>; }; // Matcher工厂函数 export function createMatcher ( routes: Array<RouteConfig>, // 路由配置列表 router: VueRouter // VueRouter实例 ): Matcher { // 创建一个路由映射表 const { pathList, pathMap, nameMap } = createRouteMap(routes) // 添加路由 function addRoutes (routes) { // 由于传入pathList, pathMap, nameMap了,所以createRouteMap方法会执行添加逻辑 createRouteMap(routes, pathList, pathMap, nameMap) } function getRoutes () { return pathList.map(path => pathMap[path]) } function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { const location = normalizeLocation(raw, currentRoute, false, router) const { name } = location if (name) { const record = nameMap[name] if (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 = {} } if (currentRoute && typeof currentRoute.params === object) { for (const key in currentRoute.params) { if (!(key in location.params) && paramNames.indexOf(key) > -1) { location.params[key] = currentRoute.params[key] } } } location.path = fillParams(record.path, location.params, `named route "${name}"`) return _createRoute(record, location, redirectedFrom) } else if (location.path) { location.params = {} for (let i = 0; i < pathList.length; i++) { const path = pathList[i] const record = pathMap[path] if (matchRoute(record.regex, location.path, location.params)) { return _createRoute(record, location, redirectedFrom) } } } // no match return _createRoute(null, location) } // ...... return { match, getRoutes, addRoutes } }

1. createMatcher 接收 2 个参数,一个是 router,它是我们 new VueRouter 返回的实例,一个是 routes,它是用户定义的路由配置.

2. 返回一个Matcher对象;Matcher对象包含一个用于匹配的match方法和一个动态添加路由的addRoutes方法(match,addRoutes)

3. 而这两个方法都声明在createMatcher内部,由于闭包特性,它能访问到createMatcher作用域的所有变量

createRouteMap

匹配器中用到的createRouteMap方法位于src/create-route-map.js中

// 导出创建的路由映射map、添加路由记录 export function createRouteMap ( routes: Array<RouteConfig>, // 路由配置列表 oldPathList?: Array<string>,// 旧pathList oldPathMap?: Dictionary<RouteRecord>,// 旧pathMap oldNameMap?: Dictionary<RouteRecord>,//旧nameMap parentRoute?: RouteRecord ): { pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord> } { // 若旧的路由相关映射列表及map存在,则使用旧的初始化(借此实现添加路由功能) const pathList: Array<string> = oldPathList || [] const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null) const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null) // 遍历路由配置对象,生成/添加路由记录 routes.forEach(route => { addRouteRecord(pathList, pathMap, nameMap, route, parentRoute) }) // 确保path:*永远在在最后 for (let i = 0, l = pathList.length; i < l; i++) { if (pathList[i] === *) { pathList.push(pathList.splice(i, 1)[0]) l-- i-- } } // 开发环境,提示非嵌套路由的path必须以/或者*开头 if (process.env.NODE_ENV === development) { // warn if routes do not include leading slashes const found = pathList // check for missing leading slash .filter(path => path && path.charAt(0) !== * && path.charAt(0) !== /) if (found.length > 0) { const pathNames = found.map(path => `- ${path}`).join(\n) warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`) } } return { pathList, pathMap, nameMap } }

createRouteMap 函数的目标是把用户的路由配置转换成一张路由映射表,它包含 3 个部分,

pathList 存储所有的 path,

pathMap 表示一个 path 到 RouteRecord 的映射关系,

nameMap 表示 name 到 RouteRecord 的映射关系

createRouteMap的逻辑:

1. 先判断路由相关映射表是否已经存在,若存在则使用,否则新建(这就实现了createRouteMap创建/新增的双重功能)

2. 然后遍历routes,依次为每个route调用addRouteRecord生成一个RouteRecord并更新pathList、pathMap和nameMap

3. 由于pathList在后续逻辑会用来遍历匹配,为了性能,所以需要将path:*放置到pathList的最后

4. 最后检查非嵌套路由的path是否是以/或者*开头

addRouteRecord

这个方法主要是创建路由记录并更新路由映射表,在同个文件内

// 添加路由记录,更新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) { // route.path不能为空 assert(path != null, `"path" is required in a route configuration.`) // route.component不能为string assert( typeof route.component !== string, `route config "component" for path: ${String( path || name )} cannot be a ` + `string id. Use an actual component instead.` ) warn( // eslint-disable-next-line no-control-regex !/[^\u0000-\u007F]+/.test(path), `Route with path "${path}" contains unencoded characters, make sure ` + `your path is correctly encoded before passing it to the router. Use ` + `encodeURI to encode static segments of your path.` ) } const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {} // 生成格式化后的path(子路由会拼接上父路由的path) const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict) // 匹配规则是否大小写敏感?(默认值:false) if (typeof route.caseSensitive === boolean) { pathToRegexpOptions.sensitive = route.caseSensitive } // 生成一条路由记录 const record: RouteRecord = { path: normalizedPath, // 利用path-to-regexp包生成用来匹配path的增强正则对象,可以用来匹配动态路由 regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 保存路由组件,支持命名视图 components: route.components || { default: route.component }, alias: route.alias //别名 ? typeof route.alias === string ? [route.alias] : route.alias : [], instances: {}, enteredCbs: {}, name, parent, matchAs, redirect: route.redirect, //重定向 beforeEnter: route.beforeEnter,//路由独享的守卫 meta: route.meta || {},//元信息 props: // 动态路由传参 route.props == null ? {} : route.components ? route.props : { default: route.props } } if (route.children) { //命名路由 && 未使用重定向 && 子路由配置对象path为或/时,使用父路由的name跳转时,子路由将不会被渲染 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) }) } //若pathMap中不存在当前路径,则更新pathList和pathMap if (!pathMap[record.path]) { pathList.push(record.path) pathMap[record.path] = record } // 处理别名 if (route.alias !== undefined) { const aliases = Array.isArray(route.alias) ? route.alias : [route.alias] for (let i = 0; i < aliases.length; ++i) { const alias = aliases[i] if (process.env.NODE_ENV !== production && alias === path) { warn( false, `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.` ) // skip in dev to make it work continue } const aliasRoute = { path: alias, children: route.children } addRouteRecord( pathList, pathMap, nameMap, aliasRoute, parent, record.path || / // matchAs ) } } // 处理命名路由 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}" }` ) } } }

1. 检查了路由规则中的path和component

2. 生成path-to-regexp的选项pathToRegexpOptions

3. 格式化path,如果是嵌套路由,则会追加上父路由的path

4. 生成路由记录

5. 处理嵌套路由,递归生成子路由记录

6. 更新pathList、pathMap

7. 处理别名路由,生成别名路由记录

8. 处理命名路由,更新nameMap

路由模式

vueRouter采用两种路由模式,一种hash+hashChange(hash),另一种利用History API的pushState+popState(history)

hash:利用hash改变时页面不会刷新并会触发hashChange这个特性来实现前端路由

history:利用了HTML5 History API 的pushState方法和popState事件来实现前端路由

回到VueRouter 的构造函数中可以看到:

switch (mode) { case history: // 用于支持pushState的浏览器src/history/html5.js this.history = new HTML5History(this, options.base) break case hash: // 用于不支持pushState的浏览器src/history/hash.js this.history = new HashHistory(this, options.base, this.fallback) break case abstract: // 用于非浏览器环境(服务端渲染)src/history/abstract.js this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== production) { assert(false, `invalid mode: ${mode}`) } }

HTML5History、HashHistory、AbstractHistory三者都是继承于基础类History(src/history/base.js );

三者不光能访问History类的所有属性和方法,他们还都实现了基础类中声明的需要子类实现的5个接口(go、push、replace、ensureURL、getCurrentLocation)

History类

它是父类(基类),其它类都是继承它的(src/history/base.js)

export class History { router: Router base: string current: Route pending: ?Route cb: (r: Route) => void ready: boolean readyCbs: Array<Function> readyErrorCbs: Array<Function> errorCbs: Array<Function> listeners: Array<Function> cleanupListeners: Function // implemented by sub-classes // 需要子类(HTML5History、HashHistory)实现的方法 +go: (n: number) => void +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void +replace: ( loc: RawLocation, onComplete?: Function, onAbort?: Function ) => void +ensureURL: (push?: boolean) => void +getCurrentLocation: () => string +setupListeners: Function constructor (router: Router, base: ?string) { this.router = router this.base = normalizeBase(base) // 格式化base,保证base是以/开头 // start with a route object that stands for "nowhere" this.current = START // 当前指向的route对象,默认为START;即from this.pending = null // 记录将要跳转的route;即to this.ready = false this.readyCbs = [] this.readyErrorCbs = [] this.errorCbs = [] this.listeners = [] } // ...... } export const START = createRoute(null, { path: / })

- 保存了router实例

- 规范化了base,确保base是以/开头

- 初始化了当前路由指向,默认只想START初始路由;在路由跳转时,this.current代表的是from

- 初始化了路由跳转时的下个路由,默认为null;在路由跳转时,this.pending代表的是to

- 初始化了一些回调相关的属性

history.transitionTo 是 Vue-Router 中非常重要的方法,当我们切换路由线路的时候,就会执行到该方法,匹配到新线路后又做了哪些事情:

1. transitionTo 首先根据目标 location 和当前路径 this.current 执行 this.router.match 方法去匹配到目标的路径 let route = this.router.match(location, this.current)

2. 这样就创建了一个初始的 Route,而 transitionTo 实际上也就是在切换 this.current

3. 拿到新的路径后,那么接下来就会执行 confirmTransition 方法去做真正的切换

HTML5History类

继承自History类,来自src/history/html5.js文件

export class HTML5History extends History { _startLocation: string constructor (router: Router, base: ?string) { // 初始化父类History super(router, base) // 获取初始location this._startLocation = getLocation(this.base) } setupListeners () { // 监听任务队列,如果没有就返回 if (this.listeners.length > 0) { return } const router = this.router const expectScroll = router.options.scrollBehavior // 检测是否需要支持scroll const supportsScroll = supportsPushState && expectScroll // 若支持scroll,初始化scroll相关逻辑 if (supportsScroll) { this.listeners.push(setupScroll()) } const handleRoutingEvent = () => { // 获取当前路径 const current = this.current //获取location const location = getLocation(this.base) //某些浏览器,会在打开页面时触发一次popstate // 此时如果初始路由是异步路由,就会出现`popstate`先触发,初始路由后解析完成,进而导致route未更新 // 所以需要避免,如果路径是初始值并且初始location值时候需要返回 if (this.current === START && location === this._startLocation) { return } // 路由地址发生变化,则跳转,并在跳转后处理滚动,这里的transitionTo就是继承父History的方法 this.transitionTo(location, route => { if (supportsScroll) { // 如果有滚动需要调用滚动方法 handleScroll(router, route, current, true) } }) } window.addEventListener(popstate, handleRoutingEvent) // 点击浏览器的前进、后退按钮等,会触发 popstate 事件,然后加入事件队列中 this.listeners.push(() => { window.removeEventListener(popstate, handleRoutingEvent) }) } // 其他history方法 go (n: number) { window.history.go(n) } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { pushState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } // ...... }

1. 他是继承于History类,所以在构造函数中调用了父类构造函数(super(router,base))

2. 检查了是否需要支持滚动行为,如果支持,则初始化滚动相关逻辑

3. 监听了popstate事件,并在popstate触发时,调用transitionTo方法实现跳转

4. 某些浏览器下,打开页面会触发一次popstate,此时如果路由组件是异步的,就会出现popstate事件触发了,但异步组件还没解析完成,最后导致route没有更新

HashHistory类

继承自History类,位于src/history/hash.js

export class HashHistory extends History { constructor (router: Router, base: ?string, fallback: boolean) { super(router, base) // fallback只有在指明了mode为history,但是浏览器又不支持popstate,用户手动指明了fallback为true时,才为true,其它情况为false // 如果需要回退,则将url换为hash模式(/#开头) // this.base来自父类 if (fallback && checkFallback(this.base)) { return } ensureSlash() } // this is delayed until the app mounts // to avoid the hashchange listener being fired too early setupListeners () { if (this.listeners.length > 0) { return } const router = this.router const expectScroll = router.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll) { this.listeners.push(setupScroll()) } const handleRoutingEvent = () => { const current = this.current if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { if (supportsScroll) { handleScroll(this.router, route, current, true) } if (!supportsPushState) { // 不支持则是hash,直接替换路径 replaceHash(route.fullPath) } }) } // 判断浏览器是否支持supportsPushState,来选择是popstate事件或者是hashchange const eventType = supportsPushState ? popstate : hashchange window.addEventListener( eventType, handleRoutingEvent ) this.listeners.push(() => { window.removeEventListener(eventType, handleRoutingEvent) }) } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo( location, route => { pushHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort ) } // ...... } // 检查回退,将url转换为hash模式(添加/#) function checkFallback (base) { const location = getLocation(base) if (!/^\/#/.test(location)) { window.location.replace(cleanPath(base + /# + location)) return true } } // 确保url是以/开头 function ensureSlash (): boolean { const path = getHash() if (path.charAt(0) === /) { return true } replaceHash(/ + path) return false } // 替换hash记录 function replaceHash (path) { if (supportsPushState) { replaceState(getUrl(path)) } else { window.location.replace(getUrl(path)) } }

检查了fallback,看是否需要回退,传入的fallback只有在用户设置了history且又不支持pushState并且启用了回退时才为true

所以,此时,需要将history模式的url替换成hash模式,即添加上#,这个逻辑是由checkFallback实现的

如果不是fallback,则直接调用ensureSlash,确保url是以/开头的

初始化过程

在安装注册阶段install内,全局混入beforeCreate勾子触发后调用router实例的init方法并传入Vue根实例,完成初始化流程,该初始化只会调用一次

Vue.mixin({ beforeCreate () { // 判断是否使用了vue-router插件 if (isDef(this.$options.router)) { ... this._router = this.$options.router // 保存VueRouter实例,this.$options.router仅存在于Vue根实例上,其它Vue组件不包含此属性,所以下面的初始化,只会执行一次 // beforeCreate hook被触发时,调用 this._router.init(this) // 初始化VueRouter实例,并传入Vue根实例 } else { ... } ... } })

init方法

在构造函数VueRouter中

// 初始化,app为Vue根实例,在全局注册时候传入vue实例this init (app: any /* Vue component instance */) { // 开发环境,确保已经安装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) // 保存实例 // 绑定destroyed hook,避免内存泄露 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 // 需要确保始终有个主应用 if (!this.app) this.history.teardown() }) //main app已经存在,则不需要重复初始化history 的事件监听 if (this.app) { return } this.app = app const history = this.history // PS:这里是3.5.5版本路由,老版本,这里是HTML5History和HashHistory分别处理; // 若是HTML5History类,则直接调用父类的transitionTo方法,跳转到当前location // 若是HashHistory,在调用父类的transitionTo方法后,并传入onComplete、onAbort回调 if (history instanceof HTML5History || history instanceof HashHistory) { // 3.5.5版本路由 // 如果是HTML5History或者HashHistory都会统一执行父类history上的的transitionTo方法,并且会传入三个参数 const handleInitialScroll = routeOrError => { const from = history.current // 获取滚动参数属性 const expectScroll = this.options.scrollBehavior // 判断浏览器是否支持history模式并且是否带了滚动属性的(history才支持滚动) const supportsScroll = supportsPushState && expectScroll if (supportsScroll && fullPath in routeOrError) { // 调用滚动的方法 handleScroll(this, routeOrError, from, false) } } const setupListeners = routeOrError => { history.setupListeners() handleInitialScroll(routeOrError) } // 统一执行history父类上的改变路由方法,然后去执行各自对应的方法,hash和history不同 history.transitionTo( history.getCurrentLocation(),//hash是获取带#的路径,history是获取/的路径 setupListeners, setupListeners ) } // 调用父类的listen方法,添加回调; // 回调会在父类的updateRoute方法被调用时触发,重新为app._route赋值 // 由于app._route被定义为响应式,所以app._route发生变化,依赖app._route的组件(route-view组件)都会被重新渲染 history.listen(route => { this.apps.forEach(app => { app._route = route }) }) } // 父类history中 // 路由跳转 transitionTo ( location: RawLocation, // 原始location,一个url或者是一个Location interface(自定义形状,在types/router.d.ts中定义) onComplete?: Function, // 跳转成功回调 onAbort?: Function// 跳转失败回调 ) { ... }

init过程:

- 检查了VueRouter是否已经安装

- 保存了挂载router实例的vue实例(VueRouter支持多实例嵌套,所以存在this.apps来保存持有router实例的vue实例)

- 注册了一个一次性钩子destroyed,在destroyed时,卸载this.app,避免内存泄露

- 检查了this.app,避免重复事件监听

- 根据history类型,调用transitionTo跳转到不同的初始页面

transitionTo 方法:

1. 首个参数是需要解析的地址,第二是跳转成功回调,第三个是跳转失败回调

2. 可以看到传入的成功、失败回调都是setupListeners函数,setupListeners函数内部调用了history.setupListeners方法,hsah和history都有这个方法

组件(link和view)

当我们点击 router-link 的时候,实际上最终会执行 router.push,如下:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) { // $flow-disable-line if (!onComplete && !onAbort && typeof Promise !== undefined) { return new Promise((resolve, reject) => { this.history.push(location, resolve, reject) }) } else { this.history.push(location, onComplete, onAbort) } }

然后再执行相应history(hash或者history上的push方法)

router-view

路由最终的渲染离不开组件,Vue-Router 内置了 <router-view> 组件,它的定义在src/components/view.js 中;

export default { name: RouterView, functional: true, //函数化组件 props: { name: { type: String, default: default } }, render (_, { props, children, parent, data }) { // parent:是使用vue-router所在组件(父),可以取到父组件的上下文,解析具名插曹 // 标记routerView组件 data.routerView = true //函数式组件没有data,没有组件实例。因此使用了父组件中的$createElement函数,用以渲染组件。 const h = parent.$createElement // 组件上的name属性 const name = props.name // 获取当前的路径 const route = parent.$route // 做了一层缓存 const cache = parent._routerViewCache || (parent._routerViewCache = {}) // determine current view depth, also check to see if the tree // has been toggled inactive but kept-alive. // 代表深度,用于路由嵌套问题中 let depth = 0 let inactive = false // 从当前组件一直遍历到最外层的根组件 while (parent && parent._routerRoot !== parent) { const vnodeData = parent.$vnode ? parent.$vnode.data : {} // 获取vnode的data //router-view属性标记当前vnode组件是否为路由组件 if (vnodeData.routerView) { depth++ } //在keepAlive中处于非激活状态 if (vnodeData.keepAlive && parent._directInactive && parent._inactive) { inactive = true } // 由内而外循环 parent = parent.$parent } data.routerViewDepth = depth // 当组件被keepAlive缓存时 if (inactive) { // 读取缓存数据 const cachedData = cache[name] // 读取缓存组件 const cachedComponent = cachedData && cachedData.component if (cachedComponent) { // #2301 // pass props if (cachedData.configProps) { fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps) } // 渲染缓存组件 return h(cachedComponent, data, children) } else { // render previous empty view return h() } } // matched是个数组,获取matched深度就是层级关系 const matched = route.matched[depth] // 对没有name属性的router-view而言默认就是name为defult const component = matched && matched.components[name] // render empty node if no matched route or no config component // 找不到对应组件和record路由信息的就清空缓存 if (!matched || !component) { cache[name] = null return h() } // cache component换成组件 cache[name] = { component } // attach instance registration hook // this will be called in the instances 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 } } ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => { matched.instances[name] = vnode.componentInstance } // register instance in init hook // in case kept-alive component be actived when routes changed // 在keepaliva激活时候调用init钩子 data.hook.init = (vnode) => { if (vnode.data.keepAlive && vnode.componentInstance && vnode.componentInstance !== matched.instances[name] ) { matched.instances[name] = vnode.componentInstance } handleRouteEntered(route) } const configProps = matched.props && matched.props[name] // save route and configProps in cache if (configProps) { extend(cache[name], { route, configProps }) fillPropsinData(component, data, route, configProps) } // 渲染组件 return h(component, data, children) } }


以上就是关于《vue-router源码解析-vue router详解》的全部内容,本文网址:https://www.7ca.cn/baike/60026.shtml,如对您有帮助可以分享给好友,谢谢。
标签:
声明