dwqs / dwqs/blog

vue-router 源码分析-整体流程

Open
#53 5 comments 13 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
3.8k
Forks
443
PR merge metrics
No merged PRs in 30d

Description

在前端框架 React、Vue.js 和 Angular 三足鼎立的年代, Vue.js 因其易用、易学、学习成本低等特点已经成为了广大前端er的新宠, 而其对应的路由 [vue-router](https://github.com/vuejs/vue-router) 也是简单好用, 功能强大. 本文将结合 [Vue.js](https://github.com/vuejs/vue) 来分析 `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: ''};

// 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` 对象
* 提供 `` 和 `` 组件

`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) => void;
};

export function createMatcher(routes: Array,
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,
oldPathList?: Array,
oldPathMap?: Dictionary,
oldNameMap?: Dictionary): {
pathList: Array;
pathMap: Dictionary;
nameMap: Dictionary;
} {
// path 列表
const pathList: Array = oldPathList || []
// path map 映射
const pathMap: Dictionary = oldPathMap || Object.create(null)
// name map 映射
const nameMap: Dictionary = 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,
pathMap: Dictionary,
nameMap: Dictionary,
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;

//...

// 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](https://github.com/vuejs/vue-router/issues/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,
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 {
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 = [].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 等)进行具体的分析.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reading the issue's cited vue-router 2.6.0 entry points: src/index.js, src/install.js, src/create-matcher.js, src/create-route-map.js, plus the components and history directories. The issue provides an analysis but does not specify a documentation change, target file, or completion criteria; confirm the intended deliverable before proceeding.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
documentation
Issue type
Documentation
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.