vue-router 源码分析 - 整体流程
在前端框架 React、Vue.js 和 Angular 三足鼎立的年代, Vue.js 因其易用、易学、学习成本低等特点已经成为了广大前端 er 的新宠,而其对应的路由 vue-router 也是简单好用,功能强大。本文将结合 Vue.js 来分析 vue-router
的整体流程。
本文分析的 vue-router 的版本为 2.6.0, vue 的版本为 2.3.3。
目录结构
vue-router@2.6.0 的整体目录结构如下:
|——vue-router
|——build // 构建脚本
|——dist // 输出目录
|——docs // 文档
|——examples // 示例
|——flow // 类型声明
|——src // 项目源码
|——components // 组件(view/link)
|——history // Router 处理
|——util // 工具库
|——index.js // Router 入口
|——install.js // Router 安装
|——create-matcher.js // Route 匹配
|——create-route-map.js // Route 映射
主要关注点就是 components
、 history
目录以及 create-matcher.js
、 create-route-map.js
、 index.js
、 install.js
等文件. 下面以一个小 demo 来分析 vue-router
的整体流程。
入口
首先看 demo 入口的代码部分:
// 1.包引入
import Vue from 'vue';
import VueRouter from "vue-router";
// 2.作为插件使用:
Vue.use(VueRouter);
// 3.引入各组件
const App = r => require.ensure([], () => r(require('./app')), 'app');
const Hello = r => require.ensure([], () => r(require('./hello), 'hello');
import Info from './info'
const Wrap = {template: '<router-view></router-view>'};
// 4.创建 VueRouter 实例
const router = new VueRouter({
mode: 'history',
base: __dirname,
routes: [
{
path: '/',
component: Wrap,
children: [
{
path: 'index',
component: App,
alias: '',
name: 'index'
},
{
path: 'hello',
name: 'hello',
alias: ['hello/index'],
components: {
default: Hello,
info: Info
}
}
]
}
]
});
// 5.创建 Vue 实例,启动应用
const app = new Vue({
router,
...Wrap
}).$mount('#app');
(2 和 4 并无特定的顺序关系)
插件安装
在上述代码的第 2 步中,利用了 Vue.js
的插件机制来安装 vue-router
, 这有三个作用:
- 通过全局的混合方式来初始化 VueRouter
- 给当前应用下的所有组件注入
$router
和$route
对象 - 提供
<router-view>
和<router-link>
组件
Vue.js
通过 use(plugin)
来安装插件时,会调用 plugin 的 install
方法,如果没有该方法, 则将 plugin 本身作为函数来调用. 其实现如下:
# src/core/global-api/use.js
Vue.use = function (plugin: Function | Object) {
// ...
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
// ...
}
VueRouter 是在 src/index.js
中导出的,提供了静态的 install
方法:
// 引入 install
import {install} from './install'
// ...
import {inBrowser} from './util/dom'
// ...
export default class VueRouter {
// 静态属性
static install: () => void;
static version: string;
// ...
}
// 静态属性赋值
VueRouter.install = install
VueRouter.version = '__VERSION__'
// 自动使用插件
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
这是 Vue.js 插件的常规开发方式,给 plugin 对象增加 install 方法,然后在 install 中实现具体逻辑. 此外,并作浏览器环境检测, 如果是在浏览器环境并且存在 window.Vue
就自动使用 plugin.
浏览器环境的检测很简单:
// src/util/dom.js
export const inBrowser = typeof window !== 'undefined'
install
作为一个单独的模块存在:
// src/install.js
// 引入 router-view 和 router-link 组件
import View from './components/view'
import Link from './components/link'
// export 一个私有 Vue 引用
export let _Vue
export function install(Vue){
if (install.installed) return
install.installed = true
// 赋值私有 Vue 引用
_Vue = Vue
const isDef = v => v !== undefined
//...
const registerInstance = (vm, callVal) => {
// 至少存在一个 VueComponent 时, _parentVnode 属性才存在
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
// https://github.com/dwqs/blog/issues/54#View 组件
i(vm, callVal)
}
}
Vue.mixin({
beforeCreate () {
// 判断是否传入了 router
if (isDef(this.$options.router)) {
// 将 router 的根组件指向 Vue 实例
this._routerRoot = this
this._router = this.$options.router
// 初始化 router
this._router.init(this)
// 定义响应式的 _route 对象
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 2.6.0 新增: 确保 this._routerRoot 有值
// 用于查找 router-view 组件的层次判断
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 注册 VueComponent,进行 observer 处理
registerInstance(this, this)
},
destroyed () {
// 取消 VueComponent 的注册
registerInstance(this)
}
})
// 定义 $router 和 $route 的 getter
Object.defineProperty(Vue.prototype, '$router', {
get () {
return this._routerRoot._router
}
})
Object.defineProperty(Vue.prototype, '$route', {
get () {
return this._routerRoot._route
}
})
// 注册组件
Vue.component('router-view', View)
Vue.component('router-link', Link)
// 钩子的合并策略
const strats = Vue.config.optionMergeStrategies
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.created
}
这里导出一个私有的 Vue 引用的目的是: 插件不必将 Vue.js 作为一个依赖打包,但插件的其它模块有可能要依赖 Vue 实例的一些方法,其它模块可以从这里获取到 Vue 实例引用。
在 beforeCreate mixin
中,在创建 Vue 实例时,如果判断传入了 router(不传入 router, 在渲染 router-view
组件时会因获取不到 matched
属性而出错), 就将 router 赋值给私有属性 _router
, 便于后续的初始化和 getter 定义。
在 Vue.js 应用中,所有组件都是 Vue 实例的扩展,也就意味着所有的组件都可以访问到这个实例原型上定义的属性. 所以, VueRouter 将 $route
和 $router
属性定义在了 Vue 实例的原型上。
Router 实例化
在应用入口文件中,对 VueRouter 进行了实例化,并将其作为参数传给 Vue 实例的 options
. VueRouter 类的入口在 src/index.js
:
import {install} from './install'
//...
import {HashHistory} from './history/hash'
import {HTML5History} from './history/html5'
import {AbstractHistory} from './history/abstract'
import type {Matcher} from './create-matcher'
export default class VueRouter{
// ...
constructor(options: RouterOptions = {}){
// ...
this.options = options
// 钩子
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 创建路由匹配对象
this.matcher = createMatcher(options.routes || [], this)
// 对 mode 作检测
// options.fallback 是 2.6.0 新增,表示是否对不支持 HTML5 history 的浏览器采用降级处理
// https://github.com/vuejs/vue-router/releases/tag/v2.6.0
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
// 兼容不支持 history 的浏览器
mode = 'hash'
}
if (!inBrowser) {
// 非浏览器环境
mode = 'abstract'
}
this.mode = 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}`)
}
}
}
// 返回匹配的 route
match(raw: RawLocation,
current?: Route,
redirectedFrom?: Location): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
}
在实例化时,主要作了两件事:
- 创建
matcher
对象 - 创建 history 实例
路由匹配
matcher
对象是由 src/create-matcher.js
中的 createMatcher
创建的:
// 定义 Matcher 类型
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
};
export function createMatcher(routes: Array<RouteConfig>,
router: VueRouter): Matcher {
// 根据 routes 创建路由 map
const {pathList, pathMap, nameMap} = createRouteMap(routes)
// 添加路由函数
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
// 路由匹配
function match(raw: RawLocation, currentRoute?: Route,
redirectedFrom?: Location): Route {
// ...
}
// ...
// 返回 matcher 对象
return {
match,
addRoutes
}
}
createMatcher
根据传入的 routes
配置生成对应的路由 map, 然后直接返回一个 matcher
对象。
继续来看 src/create-route-map.js
中的 createRouteMap
函数:
import Regexp from 'path-to-regexp'
import {cleanPath} from './util/path'
import {assert, warn} from './util/warn'
export function createRouteMap(routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>): {
pathList: Array<string>;
pathMap: Dictionary<RouteRecord>;
nameMap: Dictionary<RouteRecord>;
} {
// path 列表
const pathList: Array<string> = oldPathList || []
// path map 映射
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// name map 映射
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
// 遍历路由配置对象 增加路由记录
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
// 保证通配符在最后
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
}
}
// 添加路由记录
function addRouteRecord(pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string) {
// 获取 path/name
const {path, name} = route
// ...
// 序列化 path, 作 / 替换
const normalizedPath = normalizePath(path, parent)
// path-to-regexp 选项: 2.6.0 新增
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
// 对路径进行正则匹配是否区分大小写,该属性是 2.6.0 新增
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
// 创建一个路由记录对象
const record: RouteRecord = {
path: normalizedPath,
// 将 path 和 regex 作解析映射
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) {
// ...
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 增加 alias 对应的 route 记录
if (route.alias !== undefined) {
// alias 作数组处理
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
)
})
}
// 更新 map
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// 处理命名路由
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 normalizePath(path: string, parent?: RouteRecord): string {
path = path.replace(/\/$/, '')
if (path[0] === '/') return path
if (parent == null) return path
return cleanPath(`${parent.path}/${path}`)
}
cleanPath
的逻辑比较简单,只是对双 /
作正则替换
// src/util/path.js
export function cleanPath(path: string): string {
return path.replace(/\/\//g, '/')
}
从上述代码可以看出, create-route-map.js
的主要功能是根据用户的 routes
配置的 path
、 alias
以及 name
来生成对应的路由记录,方便后续匹配对应。
History 实例化
VueRouter 提供了 HTML5History
、 HashHistory
以及 AbstractHistory
三种方式,根据不同的 mode
和环境来实例化 History. 所有的 History 类都是在 src/history/
目录下,并且都继承自 src/history/base.js
:
// 获取私有的 Vue 实例
import {_Vue} from '../install'
import {START, isSameRoute} from '../util/route'
// ...
import {inBrowser} from '../util/dom'
// ...
export class History{
// ...
constructor(router: Router, base: ?string) {
this.router = router
this.base = normalizeBase(base)
// 默认的当前路由
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
}
// ...
}
// 格式化 base 值
function normalizeBase(base: ?string): string {
if (!base) {
if (inBrowser) {
// 如果未传入 base 且在浏览器环境,则获取 base 标签的属性
const baseEl = document.querySelector('base')
base = (baseEl && baseEl.getAttribute('href')) || '/'
// bugfix: https://github.com/vuejs/vue-router/releases/tag/v2.6.0
base = base.replace(/^https?:\/\/[^\/]+/, '')
} else {
// 非浏览器环境下的默认值
base = '/'
}
}
// 确保 base 以 / 开始
if (base.charAt(0) !== '/') {
base = '/' + base
}
// 去掉字符串结尾的 /
return base.replace(/\/$/, '')
}
到这, History 就实例化完成了, VueRouter 的实例化也完成了. 接下来看下 Vue.js 的实例化。
Vue 实例化
在启动 Vue.js 应用之前,需要先对其进行实例化, 并传入 VueRouter 实例:
// 5.创建 Vue 实例,启动应用
const app = new Vue({
router,
...Wrap
}).$mount('#app');
在创建 Vue 实例时,定义在 src/install.js
中的 mixin
会被调用:
// ...
const isDef = v => v !== undefined
// ...
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
// 初始化 router
this._router.init(this)
// 定义响应式的 _route 对象
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// ...
},
destroyed () {
// ...
}
})
// ...
在 beforeCreate
钩子中,会判断实例化时 options
是否包含 router
. router
在这有两个作用:
- 为
router-view
组件的渲染提供$route
- 保证
router.init
只被调用一次
对于第二点,因为 mixin beforeCreate
是全局的,其它非函数式组件(如 APP/Hello) 渲染时,该钩子会优先于组件内 beforeCreate
(如果有) 执行,但 $options
并不会有 router
属性,该属性只在 app
被实例化时传入。
如果有则进行 router
的初始化工作。
// src/index.js
// ...
export default class VueRouter{
// ...
// 实例属性
app: any;
apps: Array<any>;
//...
// Router 初始化
init(app: any /* Vue component instance */){
// ...
this.apps.push(app)
// app 是否已经初始化
if (this.app) {
return
}
// 实例赋值
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 = () => {
// 设置 hashchange 监听
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// Route 改变的回调监听
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
// ...
}
从上述代码可以看出,主要进行了 app
赋值,针对于 HTML5History
和 HashHistory
特殊处理,因为在这两种模式下才有可能存在进入时候的不是默认页,需要根据当前浏览器地址栏里的 path
或者 hash
来激活对应的路由,此时就是通过调用 transitionTo
来达到目的。注意: 这里在处理 HashHistory
时,是在 route 切换完成之后再设置 hashchange
的监听,这是为了修复 vuejs/vue-router#725 而做的。 因为如果钩子函数 beforeEnter
是异步的话, beforeEnter
钩子就会被触发两次. 因为在初始化时,如果此时的 hash
值不是以 /
开头的话就会补上 #/
, 这个过程会触发 hashchange
事件,就会再走一次生命周期钩子, 也就意味着会再次调用 beforeEnter
钩子函数。
transitionTo
的第一个参数是当前的 location
, 其实现在 src/history/base.js
中:
// ...
export class History{
// ...
transitionTo(location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 获取匹配的 Route 对象
const route = this.router.match(location, this.current)
// 确认切换
this.confirmTransition(route, () => {
// 更新 route
this.updateRoute(route)
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)
})
}
})
}
// 更新当前 route 对象
updateRoute(route: Route) {
const prev = this.current
this.current = route
// 调用 listen 的回调
this.cb && this.cb(route)
// 执行 afterEach 钩子
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
}
在 transitionTo
中,首先通过调用 VueRouter 实例的 match
方法获取到和当前 location
对应的 route
对象:
// ...
export default class VueRouter {
// ...
constructor(options: RouterOptions = {}) {
// ...
// 创建路由映射
this.matcher = createMatcher(options.routes || [], this)
// ...
}
// 返回匹配的 Route
match(raw: RawLocation,
current?: Route,
redirectedFrom?: Location): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
// ...
}
matcher.match
的实现在 src/create-matcher.js
中:
// ...
import {createRoute} from './util/route'
import {createRouteMap} from './create-route-map'
import {normalizeLocation} from './util/location'
export function createMatcher(routes: Array<RouteConfig>,
router: VueRouter): Matcher {
// ...
function match(raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location): Route {
const location = normalizeLocation(raw, currentRoute, false, router)
const {name} = location
if (name) {
// 根据 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]
// 根据 path 回去记录
const record = pathMap[path]
if (matchRoute(record.regex, location.path, location.params)) {
// 匹配成功 创建 pathMap[path] 对应的路由
return _createRoute(record, location, redirectedFrom)
}
}
}
// 没有匹配就根据 location 创建新的路由
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)
}
// matchAs 用于创建别名路由
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
}
createRoute
的实现在 src/util/route.js
中:
// ...
export function createRoute(record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter): Route {
const stringifyQuery = router && router.options.stringifyQuery
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query: location.query || {},
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
// 根据记录层级的得到所有匹配的路由记录
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
return Object.freeze(route)
}
// 起始路由
export const START = createRoute(null, {
path: '/'
})
function formatMatch(record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
// ...
回到 src/history/base.js
, 在 transitionTo
方法中获取到匹配的 route
之后,就调用了 confirmTransition
:
// ...
import {runQueue} from '../util/async'
import {START, isSameRoute} from '../util/route'
// ...
export class History {
// ...
// 确认过渡
confirmTransition(route: Route, onComplete: Function, onAbort?: Function) {
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
) {
this.ensureURL()
return abort()
}
// 交叉比跳转前的路由记录和将要跳转的路由记录
// 以便可以确切的知道 哪些组件需要更新 哪些不需要更新
const {
updated,
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)
// 待执行的各种钩子更新队列
const queue: Array<?NavigationGuard> = [].concat(
// 提取组件的 beforeRouteLeave 钩子
extractLeaveGuards(deactivated),
// 全局的 beforeEach 钩子
this.router.beforeHooks,
// 提取组件的 beforeRouteUpdate 钩子
extractUpdateHooks(updated),
// 组件的 beforeRouteEnter 钩子
activated.map(m => m.beforeEnter),
// 异步组件处理
resolveAsyncComponents(activated)
)
// 保存下一个路由
this.pending = route
const iterator = (hook: NavigationGuard, next) => {
// 不相等则终止
if (this.pending !== route) {
return abort()
}
try {
// 导航钩子
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
// next(false) -> 终止导航
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: '/' }) -> 重定向
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// 路由跳转
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 执行各种钩子队列
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// 等待异步组件 OK 时,执行组件内的钩子
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()
})
})
}
})
})
}
}
从上述代码可知,整个过程就是执行组件的各种钩子以及处理异步组件问题. 再回到之前看的 init
, 最后调用了 history.listen
方法:
// Route 改变的回调监听
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
listen
设置了 Route 改变之后的回调,会在 confirmTransition
的 onComplete
回调中调用,其作用就是更新下当前应用实例的 _route
值. 在前文的分析中, _route
属性被定义为一个 reactive 属性,初始值是当前的路由对象:
// ...
// 初始化 router
this._router.init(this)
// 定义响应式的 _route 对象
Vue.util.defineReactive(this, '_route', this._router.history.current)
// ...
history
的改变会去更新 _route
, 进而触发 Vue 实例的更新机制,调用 render
去重新渲染界面。
总结
vue-router
的整体流程就分析到这了. 由于篇幅有限,省略了很多细节, 但不影响对整个流程的了解,后续会再针对具体的模块(组件/History 等) 进行具体的分析。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论