这是对vue-router 3 版本的源码分析。
本次分析会按以下方法进行:
- 按官网的使用文档顺序,围绕着某一功能点进行分析。这样不仅能学习优秀的项目源码,更能加深对项目的某个功能是如何实现的理解。这个对自己的技能提升,甚至面试时的回答都非常有帮助。
- 在围绕某个功能展开讲解时,所有不相干的内容都会暂时去掉,等后续涉及到对应的功能时再加上。这样最大的好处就是能循序渐进地学习,同时也不会被不相干的内容影响。省略的内容都会在代码中以…表示。
- 每段代码的开头都会说明它所在的文件目录,方便定位和查阅。如果一个函数内容有多个函数引用,这些都会放在同一个代码块中进行分析,不同路径的内容会在其头部加上所在的文件目录。
本章讲解router中 router-link 组件是如何实现导航的。
另外我的vuex3源码分析也发布完了,欢迎大家学习:
vuex3 最全面最透彻的源码分析
还有vue-router的源码分析:
vue-router 源码分析——1. 路由匹配
vue-router 源码分析——2. router-link 组件是如何实现导航的
官网例子
- 在官网例子中,对做了三个注释:这是个组件、传入to属性,渲染成标签。
- 以我们主要分析 router-link 组件如果利用必要的数据来实现导航的。
- 这里先解释一下,渲染html页面的功能,是vue-router调用了vue的render函数,这个是vue的核心功能不做分析。所以这里是分析router是如何定位到对应路由以及做了哪些信息收集和处理的。
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script><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>
</div>
Vue实例挂载link组件,从而可以使用标签
- 这里的install.js实际上是一个VUE的插件,这样当创建和挂载根实例时自动导入了插件。
- 这里面还涉及到VUE的混入(Vue.mixin),实现了在任意组件内通过this.$ router和this.$route来访问路由器和当前路由。感兴趣的小伙伴可以去看VUE官网的解释。
// ./install.js
import Link from './components/link'export function install(Vue) {Vue.component('RouterLink', Link)
}
link.js源码内容
- 传入了一个to变量,对应’/foo’,tag默认为’a’,即标签.
- render函数是vue用来描述如果构建虚拟DOM的,即如何按某个定义来构建组件。
- 如何渲染为html是vue中的核心内容,这里不做讨论,只分析vue-router中的源码内容,即resolve方法。
const { location, route, href } = router.resolve(this.to,current,this.append ) ...const classes = {}const data: any = { class: classes }...return h(this.tag, data, this.$slots.default) }
}
路由实例的 resolve 方法
- resolve入参的to是我们传入的字符串’/foo’,current是当前路由this.$route,append是undefine。
- resolve 方法内部又调用了很多函数和方法,得到需要的数据并返回。
- 让我们接着分析调用的函数(normalizeLocation,this.match, createHref)。
// ./router.js
export default class VueRouter {...resolve (to: RawLocation,current?: Route,append?: boolean) {...const location = normalizeLocation(to, current, append, this)const route = this.match(location, current)const fullPath = route.redirectedFrom || route.fullPathconst base = this.history.baseconst href = createHref(base, fullPath, this.mode) // this.mode 看做 'hash' 即可return {location,route,href...} }
}
normalizeLocation 函数分析
- 入参说明:raw = ‘/foo’, current = this.route, append = undifune, router = this.$router
- parsePath函数暂时没有额外影响,这里不做分析,只是说明 parsePath 的具体内容
- 最终返回一个对象,里面有 _normalized: true 和 path: '/foo’相关内容。
// ./util/location.js
export function normalizeLocation(raw: RawLocation,current: ?Route,append: ?boolean,router: ?VueRouter
): Location {let next: Location = typeof raw === 'string' ? { path: raw } : rawif (next._normalized) {return next }...// parsePath 的实际内容为 {'path': '/foo', 'query': '', 'hash': ''}const parsedPath = parsePath(next.path || '')// basePath 为当前的路由的路径const basePath = (current && current.path) || '/'// resolvePath函数对'/foo'也没有额外影响,可以理解直接返回了'/foo'赋值给pathconst path = parsedPath.path? resolvePath(parsedPath.path, basePath, append || next.append): basePath...return {_normalized: true,path,... }
}
this.match方法获得的route是什么
- 对应代码 const route = this.match(location, current),current = this.$route。
- match方法和resolve方法一样,是定义在VueRouter中的,它直接返回了路由匹配器的match方法。
- 路由匹配器中的match方法会遍历pathList和pathMap,利用正则表达式查看对应的path是否匹配,如果匹配,这里则返回 _createRoute 函数调用。
-
- pathList和pathMap是在初始化router时生成的,这里为方便理解,再说明一下。
-
- pathList是一个数组,记录着我们初始化时定义的各个url path,例如’/foo’,‘/bar’
-
- pathMap是一个哈希结构,key为path,value为相关的数据(比如path, component, regex等等)。
- _createRoute 函数又调用了 ./util/route.js 的 createRoute 函数。它返回了一个被冻结的对象,里面主要有path和matched属性,matched在这里可以看做等于 [record]。
- 所以this,match方法返回的是一个和我们输入的路径to匹配的一个route对象,里面有我们需要的path,record等内容。
// ./router.js
export default class VueRouter {...match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {return this.matcher.match(raw, current, redirectedFrom)}
}// ./create-matcher.js
export function createMatcher(...) {...function match {raw: RawLocation,currentRoute?: Route,redirectedFrom?: Location}: Route {// 由于传入的raw是已经标准化过的,所以这里的location和raw没有任何区别const location = normalizedLocation(raw, currentRoute, false, router)const { name } = locationif (name) {... } 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.param)) {return _createRoute(record, location, redirectedFrom) } } }}function matchRoute(regex: RouteRegExp,path: string,params: Object): boolean {const m = path.match(regex)if (!m) {return false } else if (!params) {return true }...return true}function _createRoute(record: ?RouteRecord,location: Location,redirectedFrom?: Location ): Route {...return createRoute(record, location. redirectedFrom, router) }
}// ./util/route.js
export function createRoute(record: ?RouteRecord,location: Location,redirectedFrom?: ?Location,router?: VueRouter
): Route {const route: Route = {path: location.path || '/',matched: record ? formatMatch(record) :[], // 这里可以先简单理解为 [record]... }return Object.freeze(route)
}
createHref函数分析
- 对应代码 href = createHref(base, fullPath, this.mode),对应base = undefine, fullPath = ‘/foo’, this.mode = ‘hash’。
- 在vue-route中,默认为hash模式,所以所有的的path前面都会带一个#号
- 这个#号就是在这个函数中体现的
// ./router.js
function createHref(base: string, fullPath: string, mode) {var path = mode === 'hash' ? '#' + fullPath : fullPathreturn base ? cleanPath(base + '/' + path) : path
}
总结
- recolve返回的对象里面的内容主要为location, route , href。
- location是标准化后的to,并且打上了标记表示已标准化,防止多次标准化,提升效率。
- route是通过遍历pathList和pathMap,利用正则表达式找到的和to匹配的路由对象,里面包含很多需要的内容。
- href在默认的hash模式下,会在to的前面加上#号,例如这里的’#/foo’。
// ./router.jsexport default class VueRouter {...resolve(to, current, append) {const location = normalizeLocation(to, current, append, this)const route = this.match(location, current)const fullPath = route.redirectedFrom || route.fullPathconst base = this.history.baseconst href = createHref(base, fullPath, this.mode)return {location,route,href,... } }
}
- 后续的内容就是vue利用h函数将link组件渲染为html的内容,例如设置类名,定义handler函数处理跳转,绑定点击事件,指定 a 标签等等。
- 这里再对vue-router官方文档中提到的匹配成功后自动设置的class属性值,通过源码分析一下:
-
- 通过下面的代码可以看出,如果你不想要 router-link-active 的类名,可以在初始化router时,加入options.linkActiveClass属性就可以了