相信用vue开发过项目的小伙伴都不会陌生,vue-router模块帮助咱们处理单页面应用的理由跳转的,咱们只须要将不一样path对应的组件信息传给vue-router,就能够在页面局部刷新的状况下实现路由跳转了,你有没有以为对这一处理过程感到很好奇,想要揭开这一操做的神秘面纱?来吧,让咱们一块儿开启探索之旅~html
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
routes: [...]
})
new Vue({
router
...
})
复制代码
咱们看到在使用路由以前须要调用Vue.use(VueRouter),这一操做就好像给一个拼装玩具的安装核心部件的过程.这使得VueRouter可使用Vue.下面简单介绍下Vue.use方法.vue
export function initUse(Vue: GlobalAPI) {
Vue.use = function(plugin) {
// 获取当前插件列表
const installedPlugins = this._installedPlugins || (this._installedPlugins = []) if(this.installedPlugins.indexOf(plugin) > -1) {
return this
}
const args = toArray(arguments, 1)
// 插入vue
args.unshift(this)
// 通常插件都有一个install函数
// 经过该函数可让插件使用Vue
if(typeof plugin.install === 'function') {
plugin.install.apply(plugin, args) // this指向plugin
} else {
plugin.apply(null, args) // this指向window
}
}
}
复制代码
接下来我会和你一块儿一步步深刻源码,探究整个路由切换的实现逻辑.html5
拿到一个项目的源码的时候,咱们首先要去观察他的文件结构,对项目的总体结构有一个大体的了解.主要的源码逻辑都在src目录下:node
|—— index.js
|—— install.js
└── create-matcher.js
└── create-route-map.js
|—— components
|—— link.js
|—— view.js
|—— history
|—— abstract.js
|—— base.js
|—— errors.js
|—— hash.js
|—— html5.js
└── util
|—— async.js
|—— dom.js
|—— location.js
|—— misc.js
|—— params.js
|—— path.js
|—— push-state.js
|—— query.js
|—— resolve-components.js
|—— route.js
|—— scroll.js
|—— warn.js
复制代码
// ./index.js
import { install } from './install
import { START } from './util/route'
import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'
import type { Matcher } from './create-matcher'
// ...
// VueRouter类
export default class VueRouter {
constructor (options: RouterOptions = {}) {
// 根实例
this.app = null
// 组件实例数组
this.apps = []
// vue-router的钩子函数
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 建立路由匹配实例,传入咱们定义的routes
this.matcher = createMatcher(options.routes || [], this)
// 判断模式
let mode = options.mode || 'hash'
// fallback不等于false,且mode传入history可是不支持pushState api的时候调整路由模式为hash
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
// 非浏览器环境mode='abstract'
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
// 根据不一样的模式建立对应的history实例
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}`)
}
}
}
// vue-router 初始化函数
init (app: any /* Vue component instance */) {
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)
// 建立一个组件销毁的处理程序
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () => {
// 若是组件数组中存在某个对应组件的实例,则清除
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// 确保咱们有一个根组件或者null若是没有组件的状况下
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null
})
// 根组件若是已经建立,则直接返回,咱们不须要在建立一个新的history 监听
if (this.app) {
return
}
// 不然建立根组件
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// 调用history对象的listen方法,主要是为了把回调中触发组件的_route对象的监听的逻辑// cb函数赋值给history对象的cb属性,以便在路由更新的时候调用
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
/*
下面是
vue-router的一系列api
...
*/
VueRouter.install = install // 挂载install函数
VueRouter.version = '__VERSION__' // 定义版本号
// 判断若是window上挂载了Vue则自动使用插件
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
}
复制代码
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
// 若是已经安装过,则直接返回
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, c allVal)
}
}
// 给Vue实例的钩子函数混入一些属性,并添加_route响应式对象
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
// 将根组件的_routerRoot属相指向Vue实例
this._routerRoot = this
// 将根组件_router属性指向传入的router对象
this._router = this.$options.router
// router初始化,调用vueRouter的init方法
this._router.init(this)
// 调用Vue的defineReactive增长_route的响应式对象
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 将每一个组件的_routerRoot属性都指向根Vue实例
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 注册vueComponent进行Observer处理
registerInstance(this, this)
},
destroyed () {
// 注销VueComponent
registerInstance(this)
}
})
// 给Vue实例添加 $router属性,指向 _router 为VueRouter的实例
// _route为一个存数量路有数据的对象
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 注册组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// vue钩子合并策略
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
复制代码
这一操做主要是:git
让VueRouter可使用Vue,github
给Vue实例的beforeCreate钩子函数中混入如下逻辑:web
(1)加上_routerRoot, _router 属性vue-router
(2)初始化vueRouter编程
(3) 添加_router的响应式对象,这样Vue实例能够监听到路由的变化api
全局注册两个路由组件, RouterView和RouterLink
还记得入口文件里有这样一步操做吗?
this.matcher = createMatcher(options.routes || [], this)
复制代码
下面让咱们一块儿来看看createMatcher函数.
// ./create-matcher.js
/*
建立matcher
@params { Array } routes 初始化的时候传进来的路由配置
@params { Object } router vueRouter的实例
*/
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
// 建立路由映射表, pathList,路由path组成的数组,
// pathMap路由path和routeRecord组成的映射表
// nameMap路由name和routeRecord组成的映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
// 当前路由对应的name存在则在nameMap中查找对应的record
const record = nameMap[name]
if (process.env.NODE_ENV !== 'production') {
warn(record, `Route with name '${name}' does not exist`)
}
// 若是没找到对应的record则建立对应的record
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 = {}
}
// 复制路由的参数到location中
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)
}
// 处理重定向
function redirect (
record: RouteRecord,
location: Location
): Route {
// ...
}
// 处理路由的别名,并为其对应的建立路由
function alias (
record: RouteRecord,
location: Location,
matchAs: string
): Route {
// ...
}
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
// 最终为每一个路由建立对应的路由映射信息
return createRoute(record, location, redirectedFrom, router)
}
return {
match,
addRoutes
}
}
function matchRoute (
regex: RouteRegExp,
path: string,
params: Object
): boolean {
const m = path.match(regex)
if (!m) {
return false
} else if (!params) {
return true
}
for (let i = 1, len = m.length; i < len; ++i) {
const key = regex.keys[i - 1]
const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i]
if (key) {
// Fix #1994: using * with props: true generates a param named 0
params[key.name || 'pathMatch'] = val
}
}
return true
}
// ...
复制代码
匹配路由以前会为每一个路由建立对应的路由映射表
// ./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>
} {
// pathList被建立用来控制path匹配优先级
const pathList: Array<string> = oldPathList || []
// $flow-disable-line
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// $flow-disable-line
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
routes.forEach(route => {
// 为每一个路由映射一个组装好的record对象
addRouteRecord(pathList, pathMap, nameMap, route)
})
// ensure wildcard routes are always at the end
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
return {
pathList,
pathMap,
nameMap
}
}
// 为每一个路由映射一个record对象
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
// 从每一个路由配置对象中解构出path和name
const { path, name } = route
if (process.env.NODE_ENV !== 'production') {
// ...
}
const pathToRegexpOptions: PathToRegexpOptions =
route.pathToRegexpOptions || {}
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
const record: RouteRecord = {
path: normalizedPath, // path
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
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) {
// Warn if route is named, does not redirect and has a default child route.
// If users navigate to this route by name, the default child will
// not be rendered (GH Issue #629)
if (process.env.NODE_ENV !== 'production') {
if (
route.name &&
!route.redirect &&
route.children.some(child => /^\/?$/.test(child.path))
) {
// ...
}
}
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
if (!pathMap[record.path]) {
pathList.push(record.path)
// 建立path到record的映射
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) {
// ...
}
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
}
}
if (name) {
if (!nameMap[name]) {
// 建立name到record的映射
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
// ..
}
}
}
复制代码
这个文件主要建立了三个变量:
vur-router为咱们提供了三种路由模式,对应的就是history文件夹里存放的内容,base.js建立了history的基类,hash.js,html5.js和abstract.js则在基类的基础上,扩展自身模式对应的属性和方法.
abstract模式是Node.js中使用的路由模式,主要原理是用数组来模拟浏览器记录,而后经过对数组的操做来模拟路由的前进后退.这里咱们主要介绍浏览器中使用的两种路由模式,hash模式和history模式.
export class History {
router: Router // vueRouter类
base: string // 基础路径
current: Route // 当前路由
pending: ?Route
cb: (r: Route) => void // 路由切换的回调
ready: boolean //
readyCbs: Array<Function>
readyErrorCbs: Array<Function>
errorCbs: Array<Function>
// implemented by sub-classes
+go: (n: number) => void
+push: (loc: RawLocation) => void
+replace: (loc: RawLocation) => void
+ensureURL: (push?: boolean) => void
+getCurrentLocation: () => string
constructor (router: Router, base: ?string) {
this.router = router
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
}
// 监听路由切换
listen (cb: Function) {
this.cb = cb
}
// 路由切换
onReady (cb: Function, errorCb: ?Function) {
// 路由切换完成则执行回调,不然push进readyCbs数组中
if (this.ready) {
cb()
} else {
this.readyCbs.push(cb)
if (errorCb) {
this.readyErrorCbs.push(errorCb)
}
}
}
// 把发生错误时须要执行的回调收集起来
onError (errorCb: Function) {
this.errorCbs.push(errorCb)
}
// 核心函数,控制路由跳转
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
// 获取匹配的路由信息
const route = this.router.match(location, this.current)
// 判断是否跳转
this.confirmTransition(
route,
() => {
// 更新路由
this.updateRoute(route)
// 执行跳转完成的回调
onComplete && onComplete(route)
// 暂且理解成修改浏览器地址
this.ensureURL()
// 保证readyCbs数组中的回调函数制备调用一次
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)
})
}
}
)
}
/*
* @mathods 判断是否跳转
* @params { Route } route 匹配的路由对象
* @params { Function } onComplete 跳转完成时的回调
* @params { Function } onAbort 取消跳转时的回调
*/
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
const abort = err => {
// after merging https://github.com/vuejs/vue-router/pull/2771 we
// When the user navigates through history through back/forward buttons
// we do not want to throw the error. We only throw it if directly calling
// push/replace. That's why it's not included in isError
// 当用户经过浏览器操做前进后退按钮的时候,咱们不想抛出错误,
// 咱们仅仅会在直接调用push和replace方法的时候抛出错误
if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err)
})
} else {
warn(false, 'uncaught error during route navigation:')
console.error(err)
}
}
onAbort && onAbort(err)
}
// 若是相同的路由则不跳转
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort(new NavigationDuplicated(route))
}
// 下面是跳转的逻辑
// 经过对比解析出可复用的组件,失活的组件, 须要渲染的组件,
// matched里存放的是路由记录的数组
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
// 切换路由要作的一系列任务队列
const queue: Array<?NavigationGuard> = [].concat(
// 清除失活的组件, 经过触发beforeRouteLeave导航钩子,执行清除对应组件的路由记录等逻辑
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// 可复用的组件, 经过触发 beforeRouteUpdate 导航钩子,来作一些更新逻辑
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// 解析要激活的异步组件, 也有对应的 beforeRouteEnter导航钩子
resolveAsyncComponents(activated)
)
this.pending = route
// 用迭代器类执行queue中的导航守卫钩子
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 执行任务队列中的任务
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// 等到全部的异步组件加载完成后
// 执行组件进入的导航守卫钩子
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => {
cb()
})
})
}
})
})
}
// 更新路由
updateRoute (route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
}
复制代码
history对象的基础类主要处理了路由跳转的逻辑,在路由跳转过程当中,先获取路由的匹配信息,未找到匹配的路由信息则建立新的路由,而后判断是否跳转.跳转则比较跳转先后的路由信息,解析出失活的组件,可复用的组件和须要激活的组件,并调用对应路由导航钩子函数,从而更新路由信息.
// hash.js
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash()
}
// 这是一个延迟,知道app挂载完成,以避免hashChange的监听被过早的触发
setupListeners () {
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
// 开启路由切换的监听,若是支持pushState api,
// 则监听popState事件,不支持,则监听hashChange事件
window.addEventListener(
supportsPushState ? 'popstate' : 'hashchange',
() => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
)
}
// 跳转到新的路由
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
)
}
// 替换当前路由
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
// 前进或后退到某个路由
go (n: number) {
window.history.go(n)
}
// 更新url
ensureURL (push?: boolean) {
const current = this.current.fullPath
if (getHash() !== current) {
push ? pushHash(current) : replaceHash(current)
}
}
getCurrentLocation () {
return getHash()
}
}
复制代码
html5.js的内容和hash.js的内容大同小异,不一样的就是hash.js中会去判断浏览器是否支持pushState api,支持的话监听popState事件,不支持的话监听hashChange事件,而html5.js是直接监听popState事件.
base基类扩展的HashHistory类和HTML5History类,主要增长了如下内容:
监听路由切换,保证在app挂载以后开启监听,index.js中相关代码
// 不然建立根组件
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
复制代码
为history对象扩展push.replace,go,ensureURL,getCurrentLocation等方法.
export default {
// 组件名称
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
// used by devtools to display a router-view badge
data.routerView = true
// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots
// 父元素的createElement方法
const h = parent.$createElement
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.
// 获取组件层级,知道 _routerRoot 指向Vue实例时终止循环
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode && parent.$vnode.data
if (vnodeData) {
if (vnodeData.routerView) {
depth++
}
if (vnodeData.keepAlive && parent._inactive) {
inactive = true
}
}
parent = parent.$parent
}
data.routerViewDepth = depth
// render previous view if the tree is inactive and kept-alive
// 若是组件被缓存,则渲染缓存的组件
if (inactive) {
return h(cache[name], data, children)
}
// 根据组件层级去查找route路由对象中匹配的组件
const matched = route.matched[depth]
// 若是没找到匹配的组件,则渲染空节点
if (!matched) {
cache[name] = null
return h()
}
// 将查找出来的组件也赋值给cache[name]
const component = cache[name] = matched.components[name]
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
// 添加注册钩子, 钩子会被注入到组件的生命周期钩子中
// 这会在install.js中给Vue中组件的生命周期混入钩子中调用
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
}
}
// 给prepatch的钩子函数也注册该实例, 为了同一个组件能够在不一样的路由下复用
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
// 给初始化的钩子函数中也注册该实例,以便路由发生变哈的时候激活缓存的组件
data.hook.init = (vnode) => {
if (vnode.data.keepAlive &&
vnode.componentInstance &&
vnode.componentInstance !== matched.instances[name]
) {
matched.instances[name] = vnode.componentInstance
}
}
// 处理props
let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
if (propsToPass) {
// clone to prevent mutation
propsToPass = data.props = extend({}, propsToPass)
// pass non-declared props as attrs
const attrs = data.attrs = data.attrs || {}
for (const key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key] = propsToPass[key]
delete propsToPass[key]
}
}
}
// 对应当前route对象的组件存在, 且没有在缓存, 执行渲染操做
return h(component, data, children)
}
}
复制代码
// ./components/link.js
export default {
// 组件名称
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a' // 默认建立a标签
},
exact: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
event: {
type: eventTypes,
default: 'click'
}
},
render (h: Function) {
// 获取挂载的VueRouter实例
const router = this.$router
// 获取当前路由
const current = this.$route
// 解析出路由的详细信息
const { location, route, href } = router.resolve(
this.to,
current,
this.append
)
const classes = {}
const globalActiveClass = router.options.linkActiveClass
const globalExactActiveClass = router.options.linkExactActiveClass
// Support global empty active class
const activeClassFallback =
globalActiveClass == null ? 'router-link-active' : globalActiveClass
const exactActiveClassFallback =
globalExactActiveClass == null
? 'router-link-exact-active'
: globalExactActiveClass
const activeClass =
this.activeClass == null ? activeClassFallback : this.activeClass
const exactActiveClass =
this.exactActiveClass == null
? exactActiveClassFallback
: this.exactActiveClass
const compareTarget = route.redirectedFrom
? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
: route
classes[exactActiveClass] = isSameRoute(current, compareTarget)
classes[activeClass] = this.exact
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location)
} else {
router.push(location)
}
}
}
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
this.event.forEach(e => {
on[e] = handler
})
} else {
on[this.event] = handler
}
const data: any = { class: classes }
const scopedSlot =
!this.$scopedSlots.$hasNormal &&
this.$scopedSlots.default &&
this.$scopedSlots.default({
href,
route,
navigate: handler,
isActive: classes[activeClass],
isExactActive: classes[exactActiveClass]
})
if (scopedSlot) {
if (scopedSlot.length === 1) {
return scopedSlot[0]
} else if (scopedSlot.length > 1 || !scopedSlot.length) {
if (process.env.NODE_ENV !== 'production') {
warn(
false,
`RouterLink with to="${
this.props.to
}" is trying to use a scoped slot but it didn't provide exactly one child.`
)
}
return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
}
}
if (this.tag === 'a') {
data.on = on
data.attrs = { href }
} else {
// 从子元素中找到第一个a标签,给他绑定事件监听和设置href属性
// find the first <a> child and apply listener and href
const a = findAnchor(this.$slots.default)
if (a) {
// in case the <a> is a static node
a.isStatic = false
const aData = (a.data = extend({}, a.data))
aData.on = on
const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
aAttrs.href = href
} else {
// doesn't have <a> child, apply listener to self
// 没有找到a标签,则绑定当前元素自身
data.on = on
}
}
return h(this.tag, data, this.$slots.default)
}
}
复制代码
vue-router的源码解读到此就告一段落了,个人github仓库有完整的代码注解和部分模块的思惟导图仓库地址,你们若是对此有兴趣,想要学习的同窗必定先把vue-router git仓库的代码克隆下来,对照个人解读来看,或者能够先本身试着去读,固然直白的读源码会显得有些枯燥,你能够尝试带着本身的问题或者疑惑去读,有目的性的阅读更容易坚持.不明白的地方能够多读几遍,源码中函数的逻辑每每用到了不一样js文件中的函数,要顺着思路去往下捋.我没有对util文件里用到的一些钩子函数作过多的解读,可是但愿你们均可以去仔细研究下,能够收获到更多的设计思路和编程技巧.刚入源码坑不久,若是有不对的或者解释不到位的地方欢迎指出,有什么建议或想法,欢迎留言或者加微信lj_de_wei_xin
与我交流~