您的位置:首页 > 教育 > 培训 > Vue Router源码分析

Vue Router源码分析

2024/10/5 13:47:02 来源:https://blog.csdn.net/weixin_61933613/article/details/139338635  浏览:    关键词:Vue Router源码分析

摘要:最近项目中遇到了点Vue Router的问题,发现有些只是没理清楚,再次复习了下Vue Router的源码,记录下...

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

        先来看一个最基本使用例子,学习源码可结合这个例子逐步调试,理解整个路由工作过程:

<div id="app"><h1>Hello App!</h1><p><!-- 使用 router-link 组件来导航. --><!-- 通过传入 `to` 属性指定链接. --><!--** <router-link> 默认会被渲染成一个 `<a>` 标签** --><router-link to="/foo">Go to Foo</router-link><router-link to="/bar">Go to Bar</router-link></p><!-- 路由出口 --><!-- 路由匹配到的组件将渲染在这里 --><router-view></router-view>
</div>
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 // (缩写)相当于 routes: routes
})// 4. **创建和挂载根实例**。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({el: '#app',render(h) {return h(App)},router
})

        关于VueRouter,先从 Vue.use(VueRouter) 说起。

1. 路由注册

        Vue 从设计上就是一个渐进式JavaScript框架,它本身的核心是解决视图渲染的问题,其它的能力就通过插件的方式来解决。Vue-Router就是官方维护的路由插件,在介绍它的注册实现之前,我们先来分析一下 Vue 通用的插件注册原理

1.1 Vue.use

        Vue提供了Vue.use的全局API来注册这些插件,Vue.js插件初始化函数的实现定义在 vue/src/core/global-api/use.js 中:

export function initUse (Vue: GlobalAPI) {Vue.use = function (plugin: Function | Object) {  // **Vue对象的use方法,用于注册插件**// 存储所有注册过的plugin,未定义过则初始化为空数组**const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))if (installedPlugins.indexOf(plugin) > -1) {  // **保证插件只注册一次**return this}const args = toArray(arguments, 1)  // toArray函数将arguments对象转换为真正的数组,索引从1开始,跳过第一个参数(即plugin)args.unshift(this)  // install方法的第一个参数,存储Vueif (typeof plugin.install === 'function') {  // 判断plugin有没有定义install方法plugin.install.apply(plugin, args)  // 调用插件的install方法,将plugin对象作为上下文,并将args数组作为参数传递给install方法} else if (typeof plugin === 'function') {  // 插件本身就是一个函数,则直接调用plugin.apply(null, args)}installedPlugins.push(plugin)   // 已注册的插件添加到installedPlugins数组中,以便跟踪已安装的插件return this}
}

        上述方法中,Vue.use 接受一个plugin参数,并且维护了一个_installedPlugins数组,它存储所有注册过的plugin;接着又会判断 plugin 有没有定义install方法,如果有的话则调用该方法,并且该方法执行的第一个参数是 Vue;最后把plugin存储到 installedPlugins 中。 可以看到 Vue 提供的插件注册机制很简单,每个插件都需要实现一个静态的 install 方法,当我们执行 Vue.use 注册插件的时候,就会执行这个 install 方法,并且在这个install方法的第一个参数我们可以拿到Vue对象,这样的好处就是作为插件的编写方不需要再额外去import Vue(Vue的插件对Vue对象是有依赖的,但又不能去单独去import Vue,因为那样会增加包体积,所以就通过这种方式拿到Vue对象)。

1.2 路由安装

        Vue-Router的入口文件是src/index.js,其中定义了VueRouter类,也实现了install的静态方法:VueRouter.install = install,它的定义在 src/install.js 中。

export let _Vue   // _Vue变量,用于存储传入的Vue构造函数;export后可在源码的任何地方访问Vue
export function install (Vue) { // VueRouter的install的静态方法,用于安装Vue Router**if (install.installed && _Vue === Vue) return  // 避免重复安装。如果已安装且传入的Vue构造函数与之前保存的相同,则直接返回install.installed = true  // 已安装的标志位_Vue = Vue  // 保留传入的Vue**const isDef = v => v !== undefined  // 检查变量是否已定义const registerInstance = (vm, callVal) => {  // registerInstance,用于在组件中注册路由实例let i = vm.$options._parentVnodeif (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {i(vm, callVal)}}// 最重要的一步:利用Vue.mixin去把beforeCreate和destroyed钩子函数注入到每一个组件中**Vue.mixin({beforeCreate () {  // beforeCreate生命周期钩子中执行一些逻辑,包括**初始化路由**、定义响应式对象等if (isDef(this.$options.router)) { // 判断当前组件是否存在$options.router,存在则是根组件this._routerRoot = this  // 将当前组件设置为根组件(根Vue实例)this._router = this.$options.router  //  将当前组件的$options.router赋值给 _router,即保存了路由实例this._router.init(this)// 将_route变量变成响应式对象,实现当路由发生变化时自动更新视图Vue.util.defineReactive(this, '_route', this._router.history.current)  // 把this._route变成响应式对象} else {  // 非根组件, 将其与根组件关联起来// 当前组件有父组件,并且父组件存在_routerRoot,则将其设置为当前组件的_routerRoot,否则将当前组件设置为自身的_routerRootthis._routerRoot = (this.$parent && this.$parent._routerRoot) || this}registerInstance(this, this)},destroyed () { // 注销路由实例registerInstance(this)}})// 原型上定义$router属性,使得在组件中可以通过**this.$router访问路由实例**Object.defineProperty(Vue.prototype, '$router', {get () { return this._routerRoot._router }})// 原型上定义 $route 属性,使得在组件中可以通过**this.$route访问当前路由信息**Object.defineProperty(Vue.prototype, '$route', {get () { return this._routerRoot._route }})Vue.component('RouterView', View) **// 全局注册名为RouterView的组件,使用的组件是View**Vue.component('RouterLink', Link) **// 全局注册名为RouterLink的组件,使用的组件是Link**const strats = Vue.config.optionMergeStrategies  // 获取Vue的配置选项合并策略strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created  // 将路由相关的生命周期钩子函数合并策略设置为created
}

        当用户执行Vue.use(VueRouter)的时候,实际上就是在执行install函数(完成将 Vue Router 注入到 Vue 实例中),为了确保 install 逻辑只执行一次,用了install.installed变量做已安装的标志位。

        另外用一个全局的_Vue来接收参数Vue,因为作为Vue的插件对Vue对象是有依赖的,但又不能去单独去import Vue,因为那样会增加包体积,所以就通过这种方式拿到Vue对象。 Vue-Router安装最重要的一步就是利用Vue.mixin去把beforeCreate和destroyed钩子函数注入到每一个组件中。Vue.mixin的定义,在vue/src/core/global-api/mixin.js 中:

export function initMixin (Vue: GlobalAPI) { // 接受参数Vue,用于初始化Vue实例Vue.mixin = function (mixin: Object) {  // 将mixin函数添加到Vue上。参数mixin,表示要混入的选项this.options =** mergeOptions(this.options, mixin)  // 将当前Vue实例的选项与传入的mixin对象进行合并return this}
}

        它的实现实际上非常简单,就是把要混入的对象通过mergeOptions合并到Vue的options 中,由于每个组件的构造函数都会在extend阶段合并Vue.options到自身的options中,所以也就相当于每个组件都定义了mixin定义的选项。 回到 Vue-Router 的install方法,先看混入的beforeCreate钩子函数,对于根Vue实例而言,执行该钩子函数时定义了this._routerRoot表示它自身; this._router表示VueRouter的实例router,它是在new Vue的时候传入的;另外执行了this._router.init()方法初始化 router,这个逻辑之后介绍,然后用 defineReactive方法把this._route变成响应式对象,这个作用我们之后会介绍。而对于子组件而言,由于组件是树状结构,在遍历组件树的过程中,它们在执行该钩子函数的时候this._routerRoot始终指向的离它最近的传入了router对象作为配置而实例化的父实例。 对于beforeCreate和destroyed钩子函数,它们都会执行registerInstance方法,这个方法的作用之后会介绍。 接着给Vue原型上定义了router和route 2个属性的get方法,这就是为什么我们可以在组件实例上可以访问this.router以及 this.route,它们的作用之后介绍。 接着又通过Vue.component 方法定义了全局的<router-link>和<router-view> 2个组件,这也是为什么我们在写模板的时候可以使用这两个标签,它们的作用也是之后介绍。 最后定义了路由中的钩子函数的合并策略,和普通的钩子函数一样。

总结:

  • Vue编写插件的时候,通常要提供静态的install方法;
  • Vue-Router的install方法会给每一组件注入beforeCreated和destoryed钩子函数。在beforeCreated做一些私有属性定义和路由初始化工作;

2. VueRouter对象

        VueRouter 的实现是一个类,定义在 src/index.js 中,先对它做一个简单地分析:

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 = {}) { this.app = null  // 初始化了一些属性this.apps = []this.options = optionsthis.beforeHooks = []  // 导航守卫this.resolveHooks = []this.afterHooks = []this.matcher = createMatcher(options.routes || [], this)let mode = options.mode || 'hash'  // **根据传入的options.mode来确定路由模式****// history模式支持判断,**supportsPushState会对浏览器UA进行检测this.fallback = mode === 'history' && !supportsPushState && options.fallback !== falseif (this.fallback) {mode = 'hash'}if (!inBrowser) {  // abstract模式不是浏览器环境下使用mode = 'abstract'}this.mode = modeswitch (mode) {  // 根据路由模式创建对应的路由历史对象this.history(继承于history Class)case 'history':this.history = new HTML5History(this, options.base)breakcase 'hash':this.history = new HashHistory(this, options.base, this.fallback)breakcase 'abstract':this.history = new AbstractHistory(this, options.base)breakdefault:if (process.env.NODE_ENV !== 'production') {assert(false, `invalid mode: ${mode}`)}}}// 路由匹配方法: 传入原始位置raw、当前路由current和重定向来源redirectedFrom,返回匹配的路由对象match (raw: RawLocation,current?: Route,redirectedFrom?: Location): Route {return this.matcher.match(raw, current, redirectedFrom)}get currentRoute (): ?Route {  // 当前的路由对象,通过访问路由历史对象的current属性获取return this.history && this.history.current}// 初始化路由和应用程序实例,并监听路由变化,更新应用程序实例的_route属性init (app: any) {// 非生产环境,并且 install.installed 不为真,则抛出错误提示,提醒在创建根实例之前调用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)  // 应用程序实例app添加到apps数组中,用于跟踪多个应用程序实例if (this.app) { // 如果当前应用程序实例this.app已存在,则直接返回,否则将传入的应用程序实例app 设置为当前应用程序实例return}this.app = appconst history = this.history// 根据路由模式初始化路由历史对象,并监听路由变化if (history instanceof HTML5History) {  //  History模式,则调用transitionTo方法进行路由过渡到当前位置history.transitionTo(history.getCurrentLocation())} else if (history instanceof HashHistory) { //  Hash模式则设置监听器并调用transitionTo 方法进行路由过渡到当前位置const setupHashListener = () => {history.setupListeners()}history.transitionTo(history.getCurrentLocation(),setupHashListener,setupHashListener)}history.listen(route => {  // 每当路由变化时,更新每个应用程序实例 _route属性为新的路由信息this.apps.forEach((app) => {app._route = route})})}// 导航触发之前调用的钩子函数,可以用来进行导航守卫beforeEach (fn: Function): Function {  // 函数作为参数,并将其注册到beforeHooks钩子数组中return registerHook(this.beforeHooks, fn)}// 导航确认之前调用的钩子函数,和 beforeEach 类似,但是在**所有异步路由组件解析之后调用**beforeResolve (fn: Function): Function {  // 将函数注册到resolveHooks钩子数组中return registerHook(this.resolveHooks, fn)}// 导航成功完成之后调用afterEach (fn: Function): Function {  // 将函数注册到afterHooks钩子数组中return registerHook(this.afterHooks, fn)}// 路由初始化完成时调用的回调函数onReady (cb: Function, errorCb?: Function) {  // 将onReady方法传递给路由历史管理对象(如this.history) 的onReady方法this.history.onReady(cb, errorCb)}// 当路由初始化失败时调用的回调函数onError (errorCb: Function) {  // 将onError方法传递给路由历史管理对象的onError方法this.history.onError(errorCb)}// 路由历史堆栈中添加一个新的路由记录push (location: RawLocation, onComplete?: Function, onAbort?: Function) {this.history.push(location, onComplete, onAbort)  // 调用路由历史管理对象的push方法,用于向历史堆栈中添加新的路由记录。}// 替换当前的路由记录,导航到指定的位置replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {this.history.replace(location, onComplete, onAbort)  // 类似于push,但是用于替换当前路由记录而不是添加新的记录}go (n: number) {this.history.go(n)  // 整数参数n,表示前进或后退的步数。调用路由历史管理对象的go方法,以在浏览器历史记录中导航}back () {this.go(-1)  // back方法调用go(-1),表示后退一页}forward () {this.go(1)  // forward方法调用go(1),表示前进一页}// 获取与目标位置匹配的组件数组,用于动态加载路由组件getMatchedComponents (to?: RawLocation | Route): Array<any> {  const route: any = to  // 可选的参数to,表示要匹配的目标路由。返回目标路由的匹配组件数组? to.matched? to: this.resolve(to).route: this.currentRouteif (!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,normalizedTo: Location,resolved: Route} {const location = normalizeLocation(    // 将目标位置标准化to,current || this.history.current,append,this)const route = this.match(location, current)  // 使用路由匹配器(matcher)对目标位置进行匹配,得到路由信息const fullPath = route.redirectedFrom || route.fullPathconst base = this.history.baseconst href = createHref(base, fullPath, this.mode)  // 根据路由信息生成href,并返回解析后的路由信息对象return {location,route,href,normalizedTo: location,resolved: route}}// 将新的路由配置添加到路由匹配器中,并触发对应的路由更新**addRoutes (routes: Array<RouteConfig>) {  this.matcher.addRoutes(routes)if (this.history.current !== START) {this.history.transitionTo(this.history.getCurrentLocation())}}
}

        VueRouter 定义了一些属性和方法,下面按照书序逐个分析其作用:

        首先,从它的构造函数看,当我们执行 new VueRouter 的时候做了哪些事情。

constructor (options: RouterOptions = {}) {this.app = null  // 初始化属性this.apps = []this.options = optionsthis.beforeHooks = []this.resolveHooks = []this.afterHooks = []this.matcher = createMatcher(options.routes || [], this)  // 创建路由匹配器,未传入路由配置,则使用空数组作为默认值let mode = options.mode || 'hash'this.fallback = mode === 'history' && !supportsPushState && options.fallback !== falseif (this.fallback) {mode = 'hash'}if (!inBrowser) {mode = 'abstract'}this.mode = mode  // 路由模式设置到路由器实例的 mode 属性中switch (mode) {case 'history':this.history = new HTML5History(this, options.base)breakcase 'hash':this.history = new HashHistory(this, options.base, this.fallback)breakcase 'abstract':this.history = new AbstractHistory(this, options.base)breakdefault:if (process.env.NODE_ENV !== 'production') {assert(false, `invalid mode: ${mode}`)}}
}

        构造函数定义了一些属性,其中this.app表示根Vue实例,this.apps保存持有$options.router属性的 Vue实例,this.options 保存传入的路由配置

        this.beforeHooks、 this.resolveHooks、this.afterHooks 表示一些钩子函数,我们之后会介绍;this.matcher表示路由匹配器(重点),我们之后会介绍;

        this.fallback表示在浏览器不支持history.pushState的情况下,根据传入的fallback配置参数,决定是否回退到hash模式;

        this.mode表示路由创建的模式;

        this.history表示路由历史的具体的实现实例,它是根据this.mode的不同实现不同,它有History基类,然后不同的history实现都是继承History。 实例化VueRouter后会返回它的实例router,我们在new Vue的时候会把router作为配置的属性传入,回顾一下上一节我们讲beforeCreate混入的时候有这么一段代码:

beforeCreate() {if (isDef(this.$options.router)) {  // 检查是否定义了路由器实例// ...this._router = this.$options.router  // 将路由器实例赋值给组件实例的 _router 属性this._router.init(this)  // 传入了router实例,都会执行router.init方法// ...}
}  

        所以组件在执行 beforeCreate 钩子函数的时候,如果传入了router实例,都会执行router.init方法:

init (app: any) {// 非生产环境下,使用assert函数检查是否已经安装了Vue Router插件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)  // Vue实例,然后存储到this.apps中if (this.app) {  // 已经初始化过应用程序,则直接返回,避免重复初始化return}this.app = app  // 将传入的应用程序实例赋值给路由器实例的app属性const history = this.history  // 获取this.history,后面判断使用if (history instanceof HTML5History) {  // HTML5Historyhistory.transitionTo(history.getCurrentLocation())  // 将当前路由状态切换到当前地址对应的路由} else if (history instanceof HashHistory) {  // HashHistory执行不同的逻辑const setupHashListener = () => {history.setupListeners()}history.transitionTo(  // 将当前路由状态切换到当前地址对应的路由,并设置哈希变化时的监听器。history.getCurrentLocation(),setupHashListener,setupHashListener)}history.listen(route => {  // 监听路由变化,并将路由信息_route更新到所有应用程序实例中this.apps.forEach((app) => {app._route = route})})
}

        init()方法的逻辑:

        传入的参数是Vue实例,然后存储到this.apps中;只有根Vue实例会保存到this.app中,并且会拿到当前的this.history,根据它的不同类型来执行不同逻辑。

        由于我们平时使用 hash路由多一些,所以我们先看这部分逻辑,先定义了 setupHashListener 函数,接着执行了 history.transitionTo 方法,它是定义在History基类中,代码在 src/history/base.js:

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {const route = this.router.match(location, this.current) // 做匹配// ...
}

        先不着急去看 transitionTo 的具体实现,先看第一行代码,它调用了this.router.match 函数:

// match方法,用于根据给定的原始位置(raw)、当前路由对象(current)和重定向来源(redirectedFrom)
// 来匹配路由并返回匹配的路由对象
match (raw: RawLocation,current?: Route,redirectedFrom?: Location
): Route {** return this.matcher.match(raw, current, redirectedFrom)**
}

        实际上是调用了this.matcher.match方法去做匹配,所以接下来我们先来了解一下 matcher的相关实现。

总结:

        路由初始化的时机是在组件的初始化阶段执行到beforeCreated钩子函数的时候会执行router.init方法。然后会执行history.transitionTo方法做路由过度。

持续更新中...

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com