共计 22487 个字符,预计需要花费 57 分钟才能阅读完成。
前几天笔者看到一个问题:你真的了解 vue-router 的吗?你知道 vue-router 的运行原理吗?抱着这样的问题,笔者开始了 vue-router 的源码探索之旅。本文并没有逐行去深究源码,而是跟着笔者画的流程图来简析每一步的运行流程。
剖析运行流程
笔者根据源码的结构和自己的理解事先画好了一张流程图,乍一看这张运行流程图可能会有点蒙圈,笔者接下来会现根据这张图分析下运行流程,然后再一步一步的剖析源码的核心部分。
为了便于我们理解这张运行流程图,我们将挂载完 vue-router 的 Vue 实例打印出来看看都增加了什么东西:
- $options 下的
router
对象很好理解,这个就是我们在实例化 Vue 的时候挂载的那个 vue-router 实例; -
_route
是一个响应式的路由 route 对象,这个对象会存储我们路由信息,它是通过 Vue 提供的 Vue.util.defineReactive 来实现响应式的,下面的 get 和 set 便是对它进行的数据劫持; -
_router
存储的就是我们从 $options 中拿到的 vue-router 对象; -
_routerRoot
指向我们的 Vue 根节点; -
_routerViewCache
是我们对 View 的缓存; -
$route
和$router
是定义在 Vue.prototype 上的两个 getter。前者指向_routerRoot 下的_route,后者指向_routerRoot 下的_router
接下来让我们顺顺这个“眼花缭乱的图”,以便于我们后面更好的理解之后的源码分析。
首先我们根据 Vue 的插件机制安装了 vue-router,这里其实做的很简单,总结起来就是 封装了一个 mixin,定义了两个 ’ 原型 ’,注册了两个组件 。在这个 mixin 中,beforeCreate 钩子被调用然后判断 vue-router 是否实例话了并初始化路由相关逻辑,前文提到的_routerRoot、_router、_route
便是在此时被定义的。定义了两个“原型”是指在 Vue.prototype 上定一个两个 getter,也就$route 和 $router
。注册了两个组件是指在这里注册了我们后续会用到的 RouterView 和 RouterLink 这两个组件。
然后我们创建了一个 VueRouter 的实例,并将它挂载在 Vue 的实例上,这时候 VueRouter 的实例中的 constructor 初始化了各种钩子队列;初始化了 matcher 用于做我们的路由匹配逻辑并创建路由对象;初始化了 history 来执行过渡逻辑并执行钩子队列。
接下里 mixin 中 beforeCreate 做的另一件事就是执行了我们 VueRouter 实例的 init()方法执行初始化,这一套流程和我们点击 RouteLink 或者函数式控制路由的流程类似,这里我就一起说了。在 init 方法中调用了 history 对象的 transitionTo 方法,然后去通过 match 获取当前路由匹配的数据并创建了一个新的路由对象 route,接下来拿着这个 route 对象去执行 confirmTransition 方法去执行钩子队列中的事件,最后通过 updateRoute 更新存储当前路由数据的对象 current,指向我们刚才创建的路由对象 route。
最开始的时候我们说过 _route
被定义成了响应式的 那么一个路由更新之后,_route
对象会接收到响应并通知 RouteView 去更新视图。
到此,流程就结束了,接下来我们将深入 vue-router 的源码去深度学习其原理。
剖析源码
说在前面
vue-router 的源码都采用了 flow 作为类型检验,没有配置 flow 的话可能会满屏报错,本文不对 flow 做过多的介绍了。为了便于大家的理解,下面的源码部分我会将 flow 相关的语法去掉。顺便附上一些 flow 相关:
flow 官方文档(需要科学上网):https://flow.org/
flow 入门:https://zhuanlan.zhihu.com/p/…
flow 配置:https://zhuanlan.zhihu.com/p/…
项目结构
在拿到一个项目的源码时候,我们首先要去看它的目录结构:
其中 src 是我们的项目源码部分,它包含如下结构:
- componets 是 RouterLink 和 RouterView 这两个组件;
- create-matcher.js 就是我们创建 match 的入口文件;
- create-route-map.js 用于创建 path 列表,path map,name map 等;
- history 是创建 hitory 类的逻辑;
- index.js 就是我们的入口文件,其中创建了 VueRouter 这个类;
- install.js 是我们挂载 vue-router 插件的逻辑;
- util 定义了很多工具函数;
应用入口
通常我们去构建一个 Vue 应用程序的时候入口文件通常会这么写:
// app.js | |
import Vue from 'vue'; | |
import VueRouter from 'vue-router'; | |
import Main from '../components/main'; | |
Vue.use(VueRouter); | |
const router = new VueRouter({ | |
routes: [{ | |
path: '/', | |
component: Main, | |
}], | |
}); | |
// app.js | |
new Vue({ | |
router, | |
template, | |
}).$mount('#app') |
我们可以看到 vue-router 是以插件的形式安装的,并且 vue-router 的实例也会挂载在 Vue 的实例上面。
插件安装
此时我们将目光移入源码的入口文件,发现 index.js 中引入了 install 模块,并在 VueRouter 类上挂载了一个静态的 install 方法。而且还判断了环境中如果已经挂载了 Vue 则自动去使用这个插件。
源码位置:/src/index.js
import {install} from './install' | |
import {inBrowser} from './util/dom' | |
// ... | |
export default class VueRouter {} | |
// ... | |
// 挂载 install;VueRouter.install = install | |
// 判断如果 window 上挂载了 Vue 则自动使用插件;if (inBrowser && window.Vue) {window.Vue.use(VueRouter) | |
} |
接下来看 install.js 这个文件,这个文件导出了 export 方法以供 Vue.use 去安装:
源码位置:/src/install.js
import View from './components/view' | |
import Link from './components/link' | |
// export 一个 Vue 的原因是可以不讲 Vue 打包进插件中而使用 Vue 一些方法; | |
// 只能在 install 之后才会存在这个 Vue 的实例; | |
export let _Vue | |
export function install (Vue) { | |
// 如果插件已经安装就 return | |
if (install.installed && _Vue === Vue) return | |
install.installed = true | |
_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 () { | |
// this.$options.router 为 VueRouter 实例;// 这里判断实例是否已经挂载;if (isDef(this.$options.router)) { | |
// 将 router 的根组件指向 Vue 实例 | |
this._routerRoot = this | |
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) | |
} | |
}) | |
// 为 $router 和 4route 定义 << getter >> 分别指向_routerRoot 的 _router 和 _route | |
// _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 | |
} |
这里需要注意的几点:
- 导出一个 Vue 引用:这是为了不用将整个 Vue 打包进去就可以使用 Vue 提供的一些 API,当然,这些的前提就是 vue-router 必须被安装挂载;
- 在 Vue.prototype 上定义两个 getter:Vue 的组件都是 Vue 实例的一个扩展,他们都可以访问 prototype 上的方法和属性;
- 定义响应式_route 对象:有了这个响应式的路由对象,就可以在路由更新的时候及时的通知 RouterView 去更新组件了;
实例化 VueRouter
接下来我们来看 VueRouter 类的实例化,在 constructor 中主要做的就两件事,创建 matcher 和创建 history:
源码位置:/src/index.js
// ... | |
import {createMatcher} from './create-matcher' | |
import {supportsPushState} from './util/push-state' | |
import {HashHistory} from './history/hash' | |
import {HTML5History} from './history/html5' | |
import {AbstractHistory} from './history/abstract' | |
// ... | |
export default class VueRouter {constructor (options) { | |
this.app = null | |
this.apps = [] | |
// VueRouter 配置项;this.options = options | |
// 三个钩子 | |
this.beforeHooks = [] | |
this.resolveHooks = [] | |
this.afterHooks = [] | |
// 创建路由匹配实例;传人我们定义的 routes:包含 path 和 component 的对象;this.matcher = createMatcher(options.routes || [], this) | |
// 判断模式 | |
let mode = options.mode || 'hash' | |
// 判断浏览器是否支持 history,如果不支持则回退到 hash 模式;this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false | |
if (this.fallback) {mode = 'hash'} | |
// node 运行环境 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}`) | |
} | |
} | |
} | |
// ... | |
} |
创建 matcher
顺着思路我们先看 createMatcher 这个函数:
源码位置:/src/create-matcher.js
import VueRouter from './index' | |
import {resolvePath} from './util/path' | |
import {assert, warn} from './util/warn' | |
import {createRoute} from './util/route' | |
import {fillParams} from './util/params' | |
import {createRouteMap} from './create-route-map' | |
import {normalizeLocation} from './util/location' | |
// routes 为我们初始化 VueRouter 的路由配置;// router 就是我们的 VueRouter 实例;export function createMatcher (routes, router) { | |
// pathList 是根据 routes 生成的 path 数组;// pathMap 是根据 path 的名称生成的 map;// 如果我们在路由配置上定义了 name,那么就会有这么一个 name 的 Map;const {pathList, pathMap, nameMap} = createRouteMap(routes) | |
// 根据新的 routes 生成路由;function addRoutes (routes) {createRouteMap(routes, pathList, pathMap, nameMap) | |
} | |
// 路由匹配函数;function match (raw, currentRoute, redirectedFrom) { | |
// 简单讲就是拿出我们 path params query 等等;const location = normalizeLocation(raw, currentRoute, false, router) | |
const {name} = location | |
if (name) { | |
// 如果有 name 的话,就去 name map 中去找到这条路由记录;const record = nameMap[name] | |
if (process.env.NODE_ENV !== 'production') {warn(record, `Route with name '${name}' does not exist`) | |
} | |
// 如果没有这条路由记录就去创建一条路由对象;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) | |
} | |
} | |
} | |
// no match | |
return _createRoute(null, location) | |
} | |
// ... | |
function _createRoute (record, location, redirectedFrom) { | |
// 根据不同的条件去创建路由对象;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, path, params) {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) {params[key.name] = val | |
} | |
} | |
return true | |
} | |
function resolveRecordPath (path, record) {return resolvePath(path, record.parent ? record.parent.path : '/', true) | |
} |
首先 createMatcher 会根据我们初始化 VueRouter 实例时候定义的 routes 配置,通过 createRouteMap 生成一份含有对应关系的 map,具体逻辑下面我们会说到。然后返回一个包含 match 和 addRoutes 两个方法的对象 match,就是我们实现路由匹配的详细逻辑,他会返回匹配的路由对象;addRoutes 会就是添加路由的方法。
接下来我们顺着刚才的思路去看 create-route-map.js
源码位置:/src/create-route-map.js
/* @flow */ | |
import Regexp from 'path-to-regexp' | |
import {cleanPath} from './util/path' | |
import {assert, warn} from './util/warn' | |
export function createRouteMap (routes, oldPathList, oldPathMap, oldNameMap) { | |
// the path list is used to control path matching priority | |
const pathList = oldPathList || [] | |
// $flow-disable-line | |
const pathMap = oldPathMap || Object.create(null) | |
// $flow-disable-line | |
const nameMap = oldNameMap || Object.create(null) | |
// path 列表 | |
// path 的 map 映射 | |
// name 的 map 映射 | |
// 为配置的路由项增加路由记录 | |
routes.forEach(route => {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-- | |
} | |
} | |
// 返回包含 path 数组,path map 和 name map 的对象;return { | |
pathList, | |
pathMap, | |
nameMap | |
} | |
} | |
function addRouteRecord (pathList, pathMap, nameMap, route, parent, matchAs) {const { path, name} = route | |
if (process.env.NODE_ENV !== 'production') {assert(path != null, `"path" is required in a route configuration.`) | |
assert( | |
typeof route.component !== 'string', | |
`route config "component" for path: ${String(path || name)} cannot be a ` + | |
`string id. Use an actual component instead.` | |
) | |
} | |
// 定义 path 到 Reg 的选项;const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {} | |
// 序列化 path,'/' 将会被替换成 ''; | |
const normalizedPath = normalizePath( | |
path, | |
parent, | |
pathToRegexpOptions.strict | |
) | |
// 正则匹配是否区分大小写;if (typeof route.caseSensitive === 'boolean') {pathToRegexpOptions.sensitive = route.caseSensitive} | |
const record = { | |
path: normalizedPath, | |
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))) { | |
warn( | |
false, | |
`Named Route '${route.name}' has a default child route. ` + | |
`When navigating to this named route (:to="{name:'${route.name}'"), ` + | |
`the default child route will not be rendered. Remove the name from ` + | |
`this route and use the name of the default child route for named ` + | |
`links instead.` | |
) | |
} | |
} | |
route.children.forEach(child => { | |
const childMatchAs = matchAs | |
? cleanPath(`${matchAs}/${child.path}`) | |
: undefined | |
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs) | |
}) | |
} | |
// 如果路由含有别名,则为其添加别名路由记录 | |
// 关于 alias | |
// https://router.vuejs.org/zh-cn/essentials/redirect-and-alias.html | |
if (route.alias !== undefined) {const aliases = Array.isArray(route.alias) | |
? route.alias | |
: [route.alias] | |
aliases.forEach(alias => { | |
const aliasRoute = { | |
path: alias, | |
children: route.children | |
} | |
addRouteRecord( | |
pathList, | |
pathMap, | |
nameMap, | |
aliasRoute, | |
parent, | |
record.path || '/' // matchAs | |
) | |
}) | |
} | |
// 更新 path map | |
if (!pathMap[record.path]) {pathList.push(record.path) | |
pathMap[record.path] = record | |
} | |
// 为定义了 name 的路由更新 name map | |
if (name) {if (!nameMap[name]) {nameMap[name] = record | |
} else if (process.env.NODE_ENV !== 'production' && !matchAs) { | |
warn( | |
false, | |
`Duplicate named routes definition: ` + | |
`{name: "${name}", path: "${record.path}" }` | |
) | |
} | |
} | |
} | |
function compileRouteRegex (path, pathToRegexpOptions) {const regex = Regexp(path, [], pathToRegexpOptions) | |
if (process.env.NODE_ENV !== 'production') {const keys: any = Object.create(null) | |
regex.keys.forEach(key => {warn(!keys[key.name], `Duplicate param keys in route with path: "${path}"`) | |
keys[key.name] = true | |
}) | |
} | |
return regex | |
} | |
function normalizePath (path, parent, strict): string {if (!strict) path = path.replace(/\/$/, '') | |
if (path[0] === '/') return path | |
if (parent == null) return path | |
return cleanPath(`${parent.path}/${path}`) | |
} |
从上述代码可以看出,create-route-map.js 的就是根据用户的 routes 配置的 path、alias 以及 name 来生成对应的路由记录。
创建 history
matcher 这一部分算是讲完了,接下来该说 History 的实例化了,从源码来说 history 文件夹下是有 4 个文件的,base 作为基类,另外三个继承这个基类来分别处理 vue-router 的各种 mode 情况,这里我们主要看 base 的逻辑就可以了。
// install 到处的 Vue,避免 Vue 打包进项目增加体积;import {START, isSameRoute} from '../util/route' | |
export class History {constructor (router, base) { | |
this.router = router | |
this.base = normalizeBase(base) | |
// start with a route object that stands for "nowhere" | |
// 生成一个基础的 route 对象;this.current = START | |
this.pending = null | |
this.ready = false | |
this.readyCbs = [] | |
this.readyErrorCbs = [] | |
this.errorCbs = []} | |
// ... | |
} | |
// ... | |
function normalizeBase (base: ?string): string {if (!base) {if (inBrowser) { | |
// respect <base> tag | |
const baseEl = document.querySelector('base') | |
base = (baseEl && baseEl.getAttribute('href')) || '/' | |
// strip full URL origin | |
base = base.replace(/^https?:\/\/[^\/]+/, '') | |
} else {base = '/'} | |
} | |
// make sure there's the starting slash | |
if (base.charAt(0) !== '/') {base = '/' + base} | |
// remove trailing slash | |
return base.replace(/\/$/, '') | |
} |
基础的挂载和各种实例化都说完了之后,我们可以从 init 入手去看之后的流程了。
之前在讲 install 的时候知道了在 mixin 中的 beforeCreate 钩子里执行了 init,现在我们移步到 VueRouter 的 init 方法。
源码位置:/src/index.js
// ... | |
init (app) { | |
process.env.NODE_ENV !== 'production' && assert( | |
install.installed, | |
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` + | |
`before creating root instance.` | |
) | |
// 从 install 中的调用我们知道,这个 app 就是我们实例化的 vVue 实例;this.apps.push(app) | |
// main app already initialized. | |
if (this.app) {return} | |
// 将 VueRouter 内的 app 指向我们亘 Vue 实例;this.app = app | |
const history = this.history | |
// 针对于 HTML5History 和 HashHistory 特殊处理,// 因为在这两种模式下才有可能存在进入时候的不是默认页,// 需要根据当前浏览器地址栏里的 path 或者 hash 来激活对应的路由 | |
if (history instanceof HTML5History) {history.transitionTo(history.getCurrentLocation()) | |
} else if (history instanceof HashHistory) {const setupHashListener = () => {history.setupListeners() | |
} | |
history.transitionTo(history.getCurrentLocation(), | |
setupHashListener, | |
setupHashListener | |
) | |
} | |
//... | |
} | |
// ... |
可以看到初始化主要就是给 app 赋值,并且针对于 HTML5History 和 HashHistory 进行特殊的处理,因为在这两种模式下才有可能存在进入时候的不是默认页,需要根据当前浏览器地址栏里的 path 或者 hash 来激活对应的路由,此时就是通过调用 transitionTo 来达到目的;
接下来来看看这个具体的 transitionTo:
源码位置:/src/history/base.js
transitionTo (location, onComplete, onAbort) { | |
// localtion 为我们当前页面的路由;// 调用 VueRouter 的 match 方法获取匹配的路由对象,创建下一个状态的路由对象;// this.current 是我们保存的当前状态的路由对象;const route = this.router.match(location, this.current) | |
this.confirmTransition(route, () => { | |
// 更新当前的 route 对象;this.updateRoute(route) | |
onComplete && onComplete(route) | |
// 调用子类的方法更新 url | |
this.ensureURL() | |
// fire ready cbs once | |
// 调用成功后的 ready 的回调函数;if (!this.ready) { | |
this.ready = true | |
this.readyCbs.forEach(cb => { cb(route) }) | |
} | |
}, err => {if (onAbort) {onAbort(err) | |
} | |
// 调用失败的 err 回调函数;if (err && !this.ready) { | |
this.ready = true | |
this.readyErrorCbs.forEach(cb => { cb(err) }) | |
} | |
}) | |
} | |
confirmTransition (route, onComplete, onAbort) { | |
const current = this.current | |
const abort = err => {if (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 | |
) { | |
// 调用子类的方法更新 url | |
this.ensureURL() | |
return abort()} | |
// 交叉比对当前路由的路由记录和现在的这个路由的路由记录 | |
// 以便能准确得到父子路由更新的情况下可以确切的知道 | |
// 哪些组件需要更新 哪些不需要更新 | |
const { | |
updated, | |
deactivated, | |
activated | |
} = resolveQueue(this.current.matched, route.matched) | |
// 注意,matched 里头存储的是路由记录的数组; | |
// // 整个切换周期的队列,待执行的各种钩子更新队列 | |
const queue: Array<?NavigationGuard> = [].concat( | |
// in-component leave guards | |
// 提取组件的 beforeRouteLeave 钩子 | |
extractLeaveGuards(deactivated), | |
// global before hooks | |
this.router.beforeHooks, | |
// in-component update hooks | |
// 提取组件的 beforeRouteUpdate 钩子 | |
extractUpdateHooks(updated), | |
// in-config enter guards | |
activated.map(m => m.beforeEnter), | |
// async components | |
// 异步处理组件 | |
resolveAsyncComponents(activated) | |
) | |
// 保存下一个状态的路由 | |
this.pending = route | |
// 每一个队列执行的 iterator 函数 | |
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 | |
// wait until async components are resolved before | |
// extracting in-component enter guards | |
// 等待异步组件 OK 时,执行组件内的钩子 | |
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) | |
const queue = enterGuards.concat(this.router.resolveHooks) | |
// 在上次的队列执行完成后再执行组件内的钩子 | |
// 因为需要等异步组件以及是 OK 的情况下才能执行 | |
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) { | |
const prev = this.current | |
// 将 current 指向我们更新后的 route 对象;this.current = route | |
this.cb && this.cb(route) | |
this.router.afterHooks.forEach(hook => {hook && hook(route, prev) | |
}) | |
} |
逻辑看似复杂,实际上就是各种钩子函数的来回处理,但是这里要注意下,每一个路由 route 对象都会有一个 matchd 属性,这个属性包含一个路由记录,这个记录的生成在 create-matcher.js 中已经提到了。
等一下,我们好像漏了点东西,init 后面还有一点没说:
源码位置:/src/index.js
// 设置路由改变时候的监听;history.listen(route => {this.apps.forEach((app) => {app._route = route}) | |
}) |
在这里设置了 route 改变之后的回调函数, 会在 confirmTransition 中的 onComplete 回调中调用, 并更新当前的_route 的值,前面我们提到,_route 是响应式的,那么当其更新的时候就会去通知组件重新 render 渲染。
两个组件
大体流程都看完了,接下来可以看看两个组件了,我们先看 RouterView 组件:
源码位置:/src/components/view.js
import {warn} from '../util/warn' | |
export default { | |
name: 'RouterView', | |
functional: true, | |
props: { | |
// 试图名称,默认是 default | |
name: { | |
type: String, | |
default: 'default' | |
} | |
}, | |
render (_, { props, children, parent, data}) { | |
data.routerView = true | |
// directly use parent context's createElement() function | |
// so that components rendered by router-view can resolve named slots | |
// 渲染函数 | |
const h = parent.$createElement | |
const name = props.name | |
// 拿到_route 对象和缓存对象;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) {if (parent.$vnode && parent.$vnode.data.routerView) {depth++} | |
// 处理 keep-alive 组件 | |
if (parent._inactive) {inactive = true} | |
parent = parent.$parent | |
} | |
data.routerViewDepth = depth | |
// render previous view if the tree is inactive and kept-alive | |
// 渲染缓存的 keep-alive 组件 | |
if (inactive) {return h(cache[name], data, children) | |
} | |
const matched = route.matched[depth] | |
// render empty node if no matched route | |
if (!matched) {cache[name] = null | |
return h()} | |
const component = cache[name] = matched.components[name] | |
// attach instance registration hook | |
// this will be called in the instance's injected lifecycle hooks | |
// 添加注册钩子, 钩子会被注入到组件的生命周期钩子中 | |
// 在 src/install.js, 会在 beforeCreate 钩子中调用 | |
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 | |
} | |
} | |
// also register instance in prepatch hook | |
// in case the same component instance is reused across different routes | |
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {matched.instances[name] = vnode.componentInstance | |
} | |
// resolve 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] | |
} | |
} | |
} | |
return h(component, data, children) | |
} | |
} | |
function resolveProps (route, config) {switch (typeof config) { | |
case 'undefined': | |
return | |
case 'object': | |
return config | |
case 'function': | |
return config(route) | |
case 'boolean': | |
return config ? route.params : undefined | |
default: | |
if (process.env.NODE_ENV !== 'production') { | |
warn( | |
false, | |
`props in "${route.path}" is a ${typeof config}, ` + | |
`expecting an object, function or boolean.` | |
) | |
} | |
} | |
} | |
function extend (to, from) {for (const key in from) {to[key] = from[key] | |
} | |
return to | |
} |
然后是 RouterLink 组件:
源码位置:/src/components/link.js
/* @flow */ | |
import {createRoute, isSameRoute, isIncludedRoute} from '../util/route' | |
import {_Vue} from '../install' | |
// work around weird flow bug | |
const toTypes: Array<Function> = [String, Object] | |
const eventTypes: Array<Function> = [String, Array] | |
export default { | |
name: 'RouterLink', | |
props: { | |
to: { | |
type: toTypes, | |
required: true | |
}, | |
tag: { | |
type: String, | |
default: '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 = location.path | |
? createRoute(null, location, 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} | |
if (this.tag === 'a') { | |
data.on = on | |
data.attrs = {href} | |
} else { | |
// find the first <a> child and apply listener and href | |
// 找到第一个 <a> 给予这个元素事件绑定和 href 属性 | |
const a = findAnchor(this.$slots.default) | |
if (a) { | |
// in case the <a> is a static node | |
a.isStatic = false | |
const extend = _Vue.util.extend | |
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) | |
} | |
} | |
function guardEvent (e) { | |
// don't redirect with control keys | |
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return | |
// don't redirect when preventDefault called | |
if (e.defaultPrevented) return | |
// don't redirect on right click | |
if (e.button !== undefined && e.button !== 0) return | |
// don't redirect if `target="_blank"` | |
if (e.currentTarget && e.currentTarget.getAttribute) {const target = e.currentTarget.getAttribute('target') | |
if (/\b_blank\b/i.test(target)) return | |
} | |
// this may be a Weex event which doesn't have this method | |
if (e.preventDefault) {e.preventDefault() | |
} | |
return true | |
} | |
function findAnchor (children) {if (children) { | |
let child | |
for (let i = 0; i < children.length; i++) {child = children[i] | |
if (child.tag === 'a') {return child} | |
if (child.children && (child = findAnchor(child.children))) {return child} | |
} | |
} | |
} |
结语
到这里,vue-router 的源码剖析就告一段落了,虽然没有逐行去理解作者的思想,但也算是整体上捋顺了项目的运行原理,理解了原理也就更方便我们日常的需求开发了。最后,谢谢大家喜欢。