dwqs/blog

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

dwqs opened this issue · 5 comments

dwqs commented

在前端框架 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 映射

主要关注点就是 componentshistory 目录以及 create-matcher.jscreate-route-map.jsindex.jsinstall.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 配置的 pathalias 以及 name 来生成对应的路由记录, 方便后续匹配对应.

History 实例化

VueRouter 提供了 HTML5HistoryHashHistory 以及 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 赋值, 针对于 HTML5HistoryHashHistory 特殊处理,因为在这两种模式下才有可能存在进入时候的不是默认页, 需要根据当前浏览器地址栏里的 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 改变之后的回调, 会在 confirmTransitiononComplete 回调中调用, 其作用就是更新下当前应用实例的 _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 等)进行具体的分析.

下次可以安利一下react-router吗?

mark

jawil commented

mark

传说中的用github 的 issue 写博客。 有markdown 也有评论。666。

match那一块,其实挺复杂的