ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
>PS:个人笔记,仅供参考,需要深入了解请阅读参考资料。 [TOC] # 参考资料 [https://ustbhuangyi.github.io/vue-analysis/prepare/directory.html#sfc](https://ustbhuangyi.github.io/vue-analysis/prepare/directory.html#sfc) [https://yuchengkai.cn/docs/frontend/vue.html#%E8%B7%AF%E7%94%B1%E6%B3%A8%E5%86%8C](https://yuchengkai.cn/docs/frontend/vue.html#%E8%B7%AF%E7%94%B1%E6%B3%A8%E5%86%8C) # 例子 看其提供的 API 来进行分析: ```js 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 }) ``` # 路由注册 先从`Vue.use(VueRouter)`说起。 Vue 从它的设计上就是一个渐进式 JavaScript 框架,它本身的核心是解决视图渲染的问题,其它的能力就通过插件的方式来解决。Vue-Router 就是官方维护的路由插件,在介绍它的注册实现之前,我们先来分析一下 Vue 通用的插件注册原理。 Vue 提供了`Vue.use`的全局 API 来注册这些插件,定义在`vue/src/core/global-api/use.js`中: ```js export function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])) if (installedPlugins.indexOf(plugin) > -1) { return this } 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`有没有定义`install`方法,如果有的话则调用该方法,并且该方法执行的第一个参数是`Vue`;最后把`plugin`存储到`installedPlugins`中。 可以看到 Vue 提供的插件注册机制很简单,每个插件都需要实现一个静态的`install`方法,当我们执行`Vue.use`注册插件的时候,就会执行这个`install`方法,并且在这个`install`方法的第一个参数我们可以拿到`Vue`对象,这样的好处就是作为插件的编写方不需要再额外去`import Vue`了。 # 路由安装 Vue-Router 的入口文件是`src/index.js`,其中定义了`VueRouter`类,也实现了`install`的静态方法:`VueRouter.install = install`,它的定义在`src/install.js`中: ```js export let _Vue export function install (Vue) { // 确保 install 只调用一次 if (install.installed && _Vue === Vue) return install.installed = true // 把 Vue 赋值给全局变量 _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 () { if (isDef(this.$options.router)) { this._routerRoot = this this._router = this.$options.router this._router.init(this) // 为 _route 属性实现双向绑定 Vue.util.defineReactive(this, '_route', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } registerInstance(this, this) }, destroyed () { registerInstance(this) } }) Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) // 全局注册 router-link 和 router-view Vue.component('RouterView', View) Vue.component('RouterLink', Link) const strats = Vue.config.optionMergeStrategies strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created } ``` 通过`Vue.use(plugin)`时候,就是在执行`install`方法。`Vue-Router`的`install`方法会给每一个组件注入`beforeCreate`和`destoryed`钩子函数,在`beforeCreate`做一些私有属性定义和路由初始化工作. # VueRouter 实例化 VueRouter 的实现是一个类,我们先对它做一个简单地分析,它的定义在`src/index.js`中: ```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 = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] // 路由匹配对象 this.matcher = createMatcher(options.routes || [], this) // 根据 mode 采取不同的路由方式 let mode = options.mode || 'hash' this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' } this.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}`) } } } ... ``` 在实例化 VueRouter 的过程中,核心是创建一个路由匹配对象,并且根据 mode 来采取不同的路由方式。 ## 路由匹配对象(matcher) `matcher`相关的实现都在`src/create-matcher.js`中,我们先来看一下`matcher`的数据结构: ```js export type Matcher = { match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route; addRoutes: (routes: Array<RouteConfig>) => void; }; ``` `Matcher`返回了 2 个方法,`match`和`addRoutes`,`match`方法,顾名思义它是做匹配,那么匹配的是什么,在介绍之前,我们先了解路由中重要的 2 个概念,`Loaction`和`Route`,它们的数据结构定义在`flow/declarations.js`中。 - Location ``` declare type Location = { _normalized?: boolean; name?: string; path?: string; hash?: string; query?: Dictionary<string>; params?: Dictionary<string>; append?: boolean; replace?: boolean; } ``` Vue-Router 中定义的`Location`数据结构和浏览器提供的`window.location`部分结构有点类似,它们都是对`url`的结构化描述。举个例子:`/abc?foo=bar&baz=qux#hello`,它的`path`是`/abc`,`query`是`{foo:'bar',baz:'qux'}`。 - Route ```js eclare type Route = { path: string; name: ?string; hash: string; query: Dictionary<string>; params: Dictionary<string>; fullPath: string; matched: Array<RouteRecord>; redirectedFrom?: string; meta?: any; } ``` `Route`表示的是路由中的一条线路,它除了描述了类似`Loctaion`的`path`、`query`、`hash`这些概念,还有`matched`表示匹配到的所有的`RouteRecord`。 ### createMatcher ```js 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 { //... } return { match, addRoutes } } ``` `createMatcher`函数的作用就是创建路由映射表,然后通过闭包的方式让`addRoutes`和`match`函数能够使用路由映射表的几个对象,最后返回一个`Matcher`对象。 <br /> `createMathcer`首先执行的逻辑是`const { pathList, pathMap, nameMap } = createRouteMap(routes)`创建一个路由映射表,`createRouteMap`的定义在`src/create-route-map`中: ```js export function createRouteMap ( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord> ): { pathList: Array<string>; pathMap: Dictionary<RouteRecord>; nameMap: Dictionary<RouteRecord>; } { 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) // 为每一个 route 生成 RouteRecord }) for (let i = 0, l = pathList.length; i < l; i++) { if (pathList[i] === '*') { pathList.push(pathList.splice(i, 1)[0]) l-- i-- } } return { pathList, // 存储所有的 path pathMap, // 表示一个 path 到 RouteRecord 的映射关系 nameMap // 表示 name 到 RouteRecord 的映射关系 } } ``` `createRouteMap`函数的目标是把用户的路由配置转换成一张路由映射表,它包含 3 个部分,`pathList`存储所有的`path`,`pathMap`表示一个`path`到`RouteRecord`的映射关系,而`nameMap`表示`name`到`RouteRecord`的映射关系。 `RouteRecord`的数据结构如下: ```js declare type RouteRecord = { path: string; regex: RouteRegExp; components: Dictionary<any>; instances: Dictionary<any>; name: ?string; parent: ?RouteRecord; redirect: ?RedirectOption; matchAs: ?string; beforeEnter: ?NavigationGuard; meta: any; props: boolean | Object | Function | Dictionary<boolean | Object | Function>; } ``` 由于`pathList`、`pathMap`、`nameMap`都是引用类型,所以在遍历整个`routes`过程中去执行`addRouteRecord`方法,会不断给他们添加数据。那么经过整个`createRouteMap`方法的执行,我们得到的就是`pathList`、`pathMap`和`nameMap`。其中`pathList`是为了记录路由配置中的所有`path`,而`pathMap`和`nameMap`都是为了通过`path`和`name`能快速查到对应的`RouteRecord`。(忽略中间的详细过程) # 路由初始化和路由跳转 ## 路由初始化 当根组件调用`beforeCreate`钩子函数时,会执行以下代码 ```js beforeCreate () { // 只有根组件有 router 属性,所以根组件初始化时会初始化路由 if (isDef(this.$options.router)) { this._routerRoot = this this._router = this.$options.router this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } registerInstance(this, this) } ``` 接下来看下路由初始化会做些什么 ```js init(app: any /* Vue component instance */) { // 保存组件实例 this.apps.push(app) // 如果根组件已经有了就返回 if (this.app) { return } this.app = app // 赋值路由模式 const history = this.history // 判断路由模式,以哈希模式为例 if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { // 添加 hashchange 监听 const setupHashListener = () => { history.setupListeners() } // 路由跳转 history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } // 该回调会在 transitionTo 中调用 // 对组件的 _route 属性进行赋值,触发组件渲染 history.listen(route => { this.apps.forEach(app => { app._route = route }) }) } ``` 在路由初始化时,核心就是进行路由的跳转,改变 URL 然后渲染对应的组件。接下来来看一下路由是如何进行跳转的。 ## 路由跳转 ```js transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { // 获取匹配的路由信息 const route = this.router.match(location, this.current) // 确认切换路由 this.confirmTransition(route, () => { // 以下为切换路由成功或失败的回调 // 更新路由信息,对组件的 _route 属性进行赋值,触发组件渲染 // 调用 afterHooks 中的钩子函数 this.updateRoute(route) // 添加 hashchange 监听 onComplete && onComplete(route) // 更新 URL this.ensureURL() // 只执行一次 ready 回调 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) }) } }) } ``` 在路由跳转中,需要先获取匹配的路由信息,所以先来看下如何获取匹配的路由信息 ```js function match( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { // 序列化 url // 比如对于该 url 来说 /abc?foo=bar&baz=qux##hello // 会序列化路径为 /abc // 哈希为 ##hello // 参数为 foo: 'bar', baz: 'qux' const location = normalizeLocation(raw, currentRoute, false, router) const { name } = location // 如果是命名路由,就判断记录中是否有该命名路由配置 if (name) { const record = nameMap[name] // 没找到表示没有匹配的路由 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] } } } 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) } } } // 没有匹配的路由 return _createRoute(null, location) } ``` `createRoute`可以根据`record`和`location`创建出来,最终返回的是一条`Route`路径,我们之前也介绍过它的数据结构。在 Vue-Router 中,所有的`Route`最终都会通过`createRoute`函数创建,并且它最后是不可以被外部修改的。 <br /> 得到匹配的路由信息后就是做路由跳转了,即执行`confirmTransition`。其核心就是判断需要跳转的路由是否存在于记录中,然后执行各种导航守卫函数,最后完成 URL 的改变和组件的渲染。