看完拉勾前端训练营关于Vue-Router的实现,干货满满,但Vue-Router的实现实在是绕,因此作一下笔记,确认以及加深本身的了解。进了拉勾前端训练营两个多月,收获仍是挺多的,群里很多大牛,还有美女班主任,导师及时回答学员的疑问,幽默风趣,真是群里一席谈,胜读四年本科(literally true,四年本科的课程真的水=_=)。html
实现前,看一下实现的功能:前端
建立一个项目。首先确定是要建立Vue Router的类,在根目录下建立index.js文件:vue
export default class VueRouter {constructor (option) {this._routes = options.routes || [] } init () {} }复制代码
咱们平时建立路由实例时,会传入一个对象,像这样:html5
const router = new VueRouter({ routes })复制代码
因此构造函数应该要有一个对象,若是里面有路由routes,赋值给this._routes,不然给它一个空数组。options里固然有其余属性,但先无论,以后再实现。 还有一个init方法,用来初始化设定。设计模式
因为Vue Router是插件,要想使用它,必须经过Vue.use方法。该方法会断定传入的参数是对象还函数,若是是对象,则调用里面的install方法,函数的话则直接调用。 Vue Router是一个对象,因此要有install方法。实现install以前,看一下Vue.use的源码,这样能够更好理解怎样实现install:数组
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会先断定Vue有没有一个属性叫_installedPlugins,有则引用,没有就为Vue添加属性_installedPlugins,它是一个空数组,再去引用它。_installedPlugins是记录安装过的插件。接下来断定_installedPlugins里有没有传入的插件,有则不用安装。 把传入的参数从第二个开始,变成数组,把Vue放入数组首位。若是插件是对象,则调用它的install方法,插件方法里的上下文this依然是它自身,传入刚才变成数组的参数。函数的话,不用考虑上下文,直接调用。最后记录该插件是安装过的。app
如今简单把install方法实现,在根目录下新建install.js:ide
export let _Vue = nullexport default function install (Vue) { _Vue = Vue _Vue.mixin({ beforeCreate () { if (this.$options.router) {this._router = this.$options.routerthis._routerRoot = this// 初始化 router 对象this._router.init(this) } else {this._routerRoot = this.$parent && this.$parent._routerRoot } } })复制代码
全局变量_Vue是为了方便其余Vue Router模块的引用,否则的话其余模式须要引入Vue,比较麻烦。mixin是把Vue中某些功能抽取出来,方便在不一样地方複用,这里的用法是全局挂载鈎子函数。函数
先判断是否为根实例,若是是根实例,会有路由传入,因此会$options.router存在。根实例的话则添加两个私有属性,其中_routerRoot是为了方便根实例如下的组件引用,而后初始化router。若是是根实例下的组件,去找一下有没有父组件,有就引用它的_routerRoot,这样能够经过_routerRoot.router来引用路由。工具
挂载函数基本完成。当咱们使用Vue Router,还有两个组件挂载:Router Link和Router View。在根目录下建立文件夹components,建立文件link.js和view.js。先把Router Link实现:
export default { name: 'RouterLink', props: {to: { type: String, required: true} }, render (h) {return h('a', { attrs: { href: '#' + this.to } }, [this.$slots.default]) } }复制代码
RouterLink接收一个参数to,类型是字符串。这里不使用template,是由于运行版本的vue没有编译器,把模板转为渲染函数,要直接用渲染函数。 简单讲一下渲染函数的用法,第一个参数是标签类型,第二个是标签的属性,第三是内容。详细能够看vue文档。 咱们要实现的实际上是<a :href="{{ '#' + this.to }}"><slot name="default"></slot></a>。因此第一个参数是a,第二个它的链接,第三个之因此要用数组,是由于标签的内容是一个slot标签节点,子节点要用数组包起来。 至于RouterView,如今不知道它的实现,大概写一下:
export default { name: 'RouterView', render (h) {return h () } }复制代码
在install里把两个组件注册:
import Link from './components/link'import View from './components/view'export default function install (Vue) { ... _Vue.component(Link.name, Link) _Vue.component(View.name, View) }复制代码
接下来要建立create-matcher,它是用来生成匹配器,主要返回两个方法:match和addRoutes。前者是匹配输入路径,获取路由表相关资料,后者是手动添加路由规则到路由表。这两个方法都是要依赖路由表,因此咱们还要实现路由表生成器:create-router-map,它接收路由规则,返回一个路由表,它是对象,里面有两个属性,一个是pathList,它是一个数组,存有全部路由表的路径,另外一个是pathMap,是一个字典,键是路径,而值的路径相应的资料。 在项目根目录下建立create-router-map.js:
export default function createRouteMap (routes) { // 存储全部的路由地址 const pathList = [] // 路由表,路径和组件的相关信息 const pathMap = {} return { pathList, pathMap } }复制代码
咱们须要遍历路由规则,在这过程当中作两件事:
这里的难点是有子路由,因此要用递归,但如今先不要考虑这问题,简单把功能实现:
function addRouteRecord (route, pathList, pathMap, parentRecord) { const path = route.path const record = {path: path,component: route.component,parentRecord: parentRecord// ... } // 判断当前路径,是否已经存储在路由表中了 if (!pathMap[path]) { pathList.push(path) pathMap[path] = record } }复制代码
如今考虑一会儿路由的问题。首先要先有断定路由是否有子路由,有的话遍历子路由,递归处理,还要考虑路径名称问题,若是是子路由,path应该是父子路径合并,因此这里要断定是否存有父路由。
function addRouteRecord (route, pathList, pathMap, parentRecord) { const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path const record = {path: path,component: route.component,parentRecord: parentRecord// ... } // 判断当前路径,是否已经存储在路由表中了 if (!pathMap[path]) { pathList.push(path) pathMap[path] = record } // 判断当前的route是否有子路由 if (route.children) { route.children.forEach(childRoute => { addRouteRecord(childRoute, pathList, pathMap, route) }) } }复制代码
若是有传入父路由资料,path是父子路径合并。
最后把addRouteRecord添加到createRouteMap:
export default function createRouteMap (routes) { // 存储全部的路由地址 const pathList = [] // 路由表,路径和组件的相关信息 const pathMap = {} // 遍历全部的路由规则 routes routes.forEach(route => { addRouteRecord(route, pathList, pathMap) }) return { pathList, pathMap } }复制代码
createRouteMap实现了,能够把create-matcher的路由表建立和addRoute实现:
import createRouteMap from './create-route-map'export default function createMatcher (routes) { const { pathList, pathMap } = createRouteMap(routes) function addRoutes (routes) { createRouteMap(routes, pathList, pathMap) } return { match, addRoutes } }复制代码
最后要实现match了,它接收一个路径,而后返回路径相关资料,相关资料不只仅是它自身的,还有它的父路径的资料。这里先实现一个工具类函数,它是专门建立路由的,就是返回路径以及它的相关资料。建立util/route.js:
export default function createRoute (record, path) { // 建立路由数据对象 // route ==> { matched, path } matched ==> [record1, record2] const matched = [] while (record) { matched.unshift(record) record = record.parentRecord } return { matched, path }复制代码
其实功能很简单,就是不断获取上一级的资料,放进数组首位。配上createRoute,match基本就实现了:
import createRoute from './util/route' function match (path) {const record = pathMap[path]if (record) { // 建立路由数据对象 // route ==> { matched, path } matched ==> [record1, record2] return createRoute(record, path) }return createRoute(null, path) }复制代码
在VueRouter的构造函数里把matcher加上:
import createMatcher from './create-matcher'export default class VueRouter { constructor (options) {this._routes = options.routes || []this.matcher = createMatcher(this._routes) ...复制代码
matcher作好后,开始实现History类吧,它的目的是根据用户设定的模式,管理路径,通知 RouterView把路径对应的组件渲染出来。
在项目根目录新建history/base.js:
import createRoute from '../util/route'export default class History { constructor (router) {this.router = router// 记录当前路径对应的 route 对象 { matched, path }this.current = createRoute(null, '/') } transitionTo (path, onComplete) {this.current = this.router.matcher.match(path) onComplete && onComplete() } }复制代码
建立时当时路径先默认为根路径,current是路由对象,属性有路径名和相关资料,transitionTo是路径跳转时调用的方法,它更改current和调用回调函数。 以后不一样模式(如hash或history)的类都是继承History。这里只实现HashHistory:
import History from './base'export default class HashHistory extends History { constructor (router) {super(router)// 保证首次访问的时候 #/ensureSlash() } getCurrentLocation () {return window.location.hash.slice(1) } setUpListener () {window.addEventListener('hashchange', () => { this.transitionTo(this.getCurrentLocation()) }) } }function ensureSlash () { if (window.location.hash) {return } window.location.hash = '/'}复制代码
HashHistory基本是围绕window.location.hash,因此先讲一下它。简单来讲,它会返回#后面的路径名。若是对它赋值,它会在最前面加上#。明白window.location.hash后,其余方法都不难理解。setUpListener注册一个hashchange事件,表示当哈希路径(#后的路径)发生变化,调用注册的函数。
html5模式不实现了,继承HashHistory算了:
import History from './base'export default class HTML5History extends History { }复制代码
History的类基本实现了,可是如今还不是响应式的,意味着即便实例发生变化,视图不会变化。这问题后解决。
回到VueRouter的构造函数:
constructor(options) ...const mode = this.mode = options.mode || 'hash'switch (mode) { case 'hash':this.history = new HashHistory(this)break case 'history':this.history = new HTML5History(this)break default:throw new Error('mode error') } }复制代码
这里使用了简单工厂模式 (Simple Factory Pattern),就是设计模式中工厂模式的简易版。它存有不一样的类,这些类都是继承同一类的,它经过传入的参数进行判断,建立相应的实例返回。简单工厂模式的好处是用户不用考虑建立实例的细节,他要作的是导入工厂,往工厂传入参数,就可得到实例。
以前的History有一个问题,就是它不是响应式的,也就是说,路径发生变化,浏覧器不会有任何反应,要想为响应式,能够给它一个回调函数:
import createRoute from '../util/route'export default class History { constructor (router) { ...this.cb = null } ... listen (cb) {this.cb = cb } transitionTo (path, onComplete) {this.current = this.router.matcher.match(path)this.cb && this.cb(this.current) onComplete && onComplete() } }复制代码
加上listen方法,为History添加回调函数,当路径发生转变时调用。
把以前的初始化方法init补上:
init (app) { // app 是 Vue 的实例 const history = this.history history.listen(current => { app._route = current }) history.transitionTo( history.getCurrentLocation(), history.setUpListener ) }复制代码
给history的回调函数是路径发生变化,把路由传给vue实例,而后是转换至当前路径,完成时调用history.setUpListener。不过直接把history.setUpListener放进去有一个问题,由于这等因而仅仅把setUpListener放进去,里面的this指向window,因此要用箭头函数封装,这样的话,就会调用history.setUpListener,this指向history。
init (app) {// app 是 Vue 的实例const history = this.historyconst setUpListener = () => { history.setUpListener() } history.listen(current => { app._route = current }) history.transitionTo( history.getCurrentLocation(), setUpListener ) }复制代码
用箭头函数把history.setUpListener封装一下,this就指向history。
init完成实现,回来把install的剩馀地方实现了。当初始化完成后,把vue实例的路由(不是路由表)变成响应式,可使用 Vue.util.defineReactive(this, '_route', this._router.history.current),就是为vue实例添加一个属性_route,它的值是this._router.history.current,最后添加router和route。 完整代码以下:
import Link from './components/link'import View from './components/view'export let _Vue = nullexport default function install (Vue) { // 判断该插件是否注册略过,能够参考源码 _Vue = Vue // Vue.prototype.xx _Vue.mixin({ beforeCreate () { // 给全部 Vue 实例,增长 router 的属性 // 根实例 // 以及全部的组件增长 router 属性 if (this.$options.router) {this._router = this.$options.routerthis._routerRoot = this// 初始化 router 对象this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current)// this.$parent// this.$children } else {this._routerRoot = this.$parent && this.$parent._routerRoot } } }) _Vue.component(Link.name, Link) _Vue.component(View.name, View) Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) }复制代码
如今就能够如平时开发同样,使用router和route。
最后把RouterView实现。其实它也没什么,就是获取当取路径,从路径中获得组件,而后渲染出来。问题是要考虑父子组件的问题。把思想整理一下,当有父组件时,确定是父组件已经渲染出来,子组件是从父组件的RouterView组件渲染,还有是$route有的是当前路径和匹配的资料的数组,即包括父组件的数组,因此可遍历得到要渲染的组件:
export default { name: 'RouterView', render (h) {const route = this.$routelet depth = 0//routerView表示已经完成渲染了this.routerView = truelet parent = this.$parentwhile (parent) { if (parent.routerView) { depth++ } parent = parent.$parent }const record = route.matched[depth]if (record) { return h(record.component) }return h() } }复制代码
if (parent.routerView)
是由于是确认父组件是否已经渲染,若是渲染,它的routerView为true,用depth来记录有多少父路由,而后经过它获取matched的资料,有的话则渲染获取的组件。
Vue Router的代码量很少,但实在是绕,简单总结一下比较好。先看一下项目结构:
用一张表把全部的文件做用简述一遍:
文件 | 做用 |
---|---|
index.js | 存放VueRouter类 |
install.js | 插件类必需要有的函数,用来给Vue.use调用 |
create-route-map.js | 生成路由表,它输出一个对象,有pathList和pathMap属性,前者是存有全部路径的数组,后者是字典,把路径和它的资料对应 |
util/route.js | 一个函数接收路径为参数,返回路由对象,存有matched和path属性,matched是匹配到的路径的资料和父路径资料,它是一个数组,path是路径自己 |
create-matcher.js | 利用create-route-map建立路由表,且返回两个函数,一个是用util/route匹配路由,另外一个是手动把路由规则转变成路由 |
history/base.js | History类文件,用来做历史管理,存有当前路径的路由,以及转换路径的方法 |
history/hash.js | HashHistory类文件,继承至History,用做hash模式下的历史管理 |
components/link.js | Router-Link的组件 |
components/view.js | Router-View的组件 |