Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
dwqs opened this issue Jun 29, 2017 · 5 comments
Open

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

dwqs opened this issue Jun 29, 2017 · 5 comments

Comments

@dwqs
Copy link
Owner

dwqs commented Jun 29, 2017

在前端框架 React、Vue.js 和 Angular 三足鼎立的年代, Vue.js 因其易用、易学、学习成本低等特点已经成为了广大前端er的新宠, 而其对应的路由 vue-router 也是简单好用, 功能强大. 本文将结合 Vue.js 来分析 vue-router 的整体流程.

本文分析的 vue-router 的版本为 2.6.0, vue 的版本为 2.3.3.

目录结构

[email protected] 的整体目录结构如下:

|——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 等)进行具体的分析.

@HecateDK
Copy link

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

@qianlongo
Copy link

mark

1 similar comment
@jawil
Copy link

jawil commented Sep 11, 2017

mark

@YangPengFe1
Copy link

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

@BryanAdamss
Copy link

match那一块,其实挺复杂的

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants