vue-router 源代码全流程分析「长文」

说明

以下内容均是依托 vue-router 2.0.0 版本展开分析。
此篇文章,是自己对 vue-router 实现分析的记录性文章,如有任何错误,欢迎指正,相互交流。

基础使用
import Vue from ‘vue’;
import VueRouter from ‘vue-router’;

Vue.use(VueRouter);

const Home = { template: ‘

home
’ };
const Foo = { template: ‘
foo
’ };
const Bar = { template: ‘
bar
’ };
const Child = { template: ‘
Child
’ };

const router = new VueRouter({
mode: ‘history’,
// base: __dirname,
base: ‘/’, // 默认 ‘/’
routes: [
{ path: ‘/’, component: Home },
{ path: ‘/foo’, component: Foo },
{
path: ‘/bar’,
component: Bar,
children: [{ path: ‘child’, component: Child }]
}
]
});

const template = `

Basic

  • /
  • /foo
  • /bar
  • /bar
`;

new Vue({
router,
template
}).$mount(’#app’);
复制代码根据上述基础使用,我们大概可以梳理一个基本流程出来:

注册插件:

将 $router 和 $route 注入所有启用路由的子组件。
安装 和 。

定义路由组件。
new VueRouter(options) 创建一个路由器, 传入相关配置。
创建并挂载根实例,确保注入路由器。路由组件将在 中呈现。

下面我们就根据上述流程步骤,一步一步解析,vue-router 代码实现。
注册插件(vue-router)
首先 Vue.use(VueRouter); 这段代码间接执行 VueRouter 暴露的 install 方法,下面来看看 install 具体实现:(由于有完整的注释,故略去文字叙述)
install
import View from ‘./components/view’;
import Link from ‘./components/link’;

/**

  • 安装 Vue.js 插件 install 方法调用时,会将 Vue 作为参数传入。
  • @export
  • @param {*} Vue
  • @returns

*/
export function install(Vue) {
// 防止插件被多次安装 - 当 install 方法被同一个插件多次调用,插件将只会被安装一次。
if (install.installed) return;
install.installed = true;

// 在 Vue 原型上添加 r o u t e r 属 性 ( V u e R o u t e r ) 并 代 理 到 t h i s . router 属性( VueRouter )并代理到 this. router(VueRouter)this.root._router
Object.defineProperty(Vue.prototype, 'KaTeX parse error: Expected '}', got 'EOF' at end of input: … return this.root._router;
}
});

// 在 Vue 原型上添加 r o u t e 属 性 ( 当 前 路 由 对 象 ) 并 代 理 到 t h i s . route 属性( 当前路由对象 )并代理到 this. route()this.root._route
Object.defineProperty(Vue.prototype, 'KaTeX parse error: Expected '}', got 'EOF' at end of input: … return this.root._route;
}
});

// 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
Vue.mixin({
/**
* 混入 Vue 创建前钩子
* 1.取传入 Vue 构造函数的路由配置参数并调用 init 方法。
* 2.在 Vue 根实例添加 _router 属性( VueRouter 实例)
* 3.执行路由实例的 init 方法并传入 Vue 实例
* 4.把 (KaTeX parse error: Expected '}', got 'EOF' at end of input: … if (this.options.router) {
this._router = this.$options.router;
this._router.init(this);
Vue.util.defineReactive(this, ‘_route’, this._router.history.current);
}
}
});

// 注册全局组件
Vue.component(‘router-view’, View);
Vue.component(‘router-link’, Link);
}
复制代码上述 就是 vue-router 暴露给 Vue 的注册方法。这里特别说明一下:defineReactive Vue 构建响应式的核心方法。在研究注册的两个全局组件: 和 之前,我们先讨论 VueRouter 构造函数,因为它们其中涉及到 VueRouter 的很多方法。
代码接着执行,接下来就是为 vue-router 装填配置并实例化。之后把实例化的结果传入 Vue 。
const router = new VueRouter({
// 选择路由模式
mode: ‘history’,
// 应用的基路径。默认值: “/” 例如,如果整个单页应用服务在 /app/ 下,然后 base 就应该设为 “/app/”.
base: ‘/’, // 默认 ‘/’
// 路由配置表
routes: [
{ path: ‘/’, component: Home },
{ path: ‘/foo’, component: Foo },
{
path: ‘/bar’,
component: Bar,
children: [{ path: ‘child’, component: Child }]
}
]
});

new Vue({
router
}).$mount(’#app’);
复制代码接下来我们就来看看定义路由的 VueRouter 构造函数。
定义路由:VueRouter 构造函数
/* @flow */

import { install } from ‘./install’;
import { createMatcher } from ‘./create-matcher’;
import { HashHistory } from ‘./history/hash’;
import { HTML5History } from ‘./history/html5’;
import { AbstractHistory } from ‘./history/abstract’;
import { inBrowser, supportsHistory } from ‘./util/dom’;
import { assert } from ‘./util/warn’;

export default class VueRouter {
static install: () => void;

app: any; // Vue 实例
options: RouterOptions; // 路由配置
mode: string; // 路由模式,默认 hash
history: HashHistory | HTML5History | AbstractHistory;
match: Matcher; // 一个数组,包含当前路由的所有嵌套路径片段的路由记录。?
fallback: boolean; // 当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。
beforeHooks: Array; // 前置钩子集合
afterHooks: Array any>; // 后置钩子集合

constructor(options: RouterOptions = {}) {
this.app = null;
this.options = options;
this.beforeHooks = [];
this.afterHooks = [];
this.match = createMatcher(options.routes || []); // 匹配器

/******* 确定路由模式 - 默认为 hash *******/
let mode = options.mode || 'hash';
// 如果传入的模式为 ·history· 在浏览器环境下不支持 history 模式,则强制回退到 hash 模式
this.fallback = mode === 'history' && !supportsHistory;
if (this.fallback) {
  mode = 'hash';
}
// 在非浏览器环境下,采用 abstract 模式
if (!inBrowser) {
  mode = 'abstract';
}
this.mode = mode;

}

/**

  • 当前路由
  • @readonly
  • @type {?Route}
  • @memberof VueRouter
    */
    get currentRoute(): ?Route {
    return this.history && this.history.current;
    }

/**

  • 初始化
  • @param {Any} app Vue component instance
    */
    init(app: any) {
    // 断言有没有安装插件,如果没有抛出错误提示
    assert(
    install.installed,
    没有安装。在创建根实例之前,请确保调用 Vue.use(VueRouter)。
    );
this.app = app;
const { mode, options, fallback } = this;
// 根据不同模式实例化不同基类,无效模式下抛出错误
switch (mode) {
  case 'history':
    this.history = new HTML5History(this, options.base);
    break;
  case 'hash':
    this.history = new HashHistory(this, options.base, fallback);
    break;
  case 'abstract':
    this.history = new AbstractHistory(this);
    break;
  default:
    assert(false, `invalid mode: ${mode}`);
}

// 调用 history 属性下 listen 方法?
this.history.listen(route => {
  this.app._route = route;
});

}

/**

  • Router 实例方法 beforeEach 全局前置的导航守卫。
  • 当一个导航触发时,全局前置守卫按照创建顺序调用。
  • 守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。
  • @param {Function} fn (to, from, next) => {}
  • @memberof VueRouter

*/
beforeEach(fn: Function) {
this.beforeHooks.push(fn);
}

/**

  • Router 实例方法 afterEach 全局后置钩子
  • @param {Function} fn (to, from) => {}
  • @memberof VueRouter
    */
    afterEach(fn: Function) {
    this.afterHooks.push(fn);
    }

/**

  • 编程式导航 push 导航到对应的 location
  • 这个方法会向 history 栈添加一个新的记录,
  • 所以,当用户点击浏览器后退按钮时,则回到之前的 location。
  • @param {RawLocation} location
  • @memberof VueRouter
    */
    push(location: RawLocation) {
    this.history.push(location);
    }

/**

  • 编程式导航 replace 导航到对应的 location
  • 它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
  • @param {RawLocation} location
  • @memberof VueRouter
    */
    replace(location: RawLocation) {
    this.history.replace(location);
    }

/**

  • 在 history 记录中向前或者后退多少步,类似 window.history.go(n)。
  • @param {number} n
  • @memberof VueRouter
    */
    go(n: number) {
    this.history.go(n);
    }

/**

  • 后退
  • @memberof VueRouter
    */
    back() {
    this.go(-1);
    }

/**

  • 前进
  • @memberof VueRouter
    */
    forward() {
    this.go(1);
    }

/**

  • 获取匹配到的组件列表
  • @returns {Array}
  • @memberof VueRouter
    */
    getMatchedComponents(): Array {
    if (!this.currentRoute) {
    return [];
    }
    return [].concat.apply(
    [],
    this.currentRoute.matched.map(m => {
    return Object.keys(m.components).map(key => {
    return m.components[key];
    });
    })
    );
    }
    }

// 添加 install 方法
VueRouter.install = install;

// 在浏览器环境下且 Vue 构造函数存在的情况下调用 use 方法注册插件(插件预装)
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter);
}
复制代码由上述代码实现分析,首先声明了一些属性、方法、和常用的API;接着添加 install 方法和执行 Vue.use 方法注册插件。
其中对相关代码做了详细的注释,无需再此重复论述,但其中有一些方法将在后面涉及时,做深入分析。继续分析前,这里需要先对这段代码进行解释:
this.match = createMatcher(options.routes || []); // 创建匹配器
复制代码/**

  • 创建匹配器
  • @export
  • @param {Array} routes
  • @returns {Matcher}
    */
    export function createMatcher(routes: Array): Matcher {
    const { pathMap, nameMap } = createRouteMap(routes);

function match(
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {

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 {

}

return match;
}

复制代码上述代码首先根据传入的路由配置表,创建新的映射表并从中解构出路径映射表、名称映射表,之后返回内建函数 match。这里暂时先不对其内部实现做详细介绍,之后再被调用处详细论述。
初始化
我们知道在 VueRouter - install 注册插件这个方法时,混入了一个全局的生命周期函数 beforeCreate, 代码如下:
export function install(Vue) {

Vue.mixin({
beforeCreate() {
if (this.KaTeX parse error: Expected '}', got 'EOF' at end of input: …_router = this.options.router;
this._router.init(this);
Vue.util.defineReactive(this, ‘_route’, this._router.history.current);
}
}
});

}
复制代码我们发现其中执行了 vue-router 提供的 init 方法并传入 Vue 组件实例。那么接下来我们就来看看 init 做了哪些事情。
init 方法简析

export default class VueRouter {
static install: () => void;

app: any; // vue 实例
options: RouterOptions; // 路由配置
mode: string; // 路由模式,默认 hash
history: HashHistory | HTML5History | AbstractHistory;
match: Matcher; // 一个数组,包含当前路由的所有嵌套路径片段的路由记录 。
fallback: boolean; // 回退 当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。
beforeHooks: Array; // 前置钩子集合
afterHooks: Array any>; // 后置钩子集合

constructor(options: RouterOptions = {}) {

}

/**

  • 初始化
  • @param {Any} app Vue component instance
    */
    init(app: any) {
    // 断言有没有安装插件,如果没有抛出错误提示
    assert(
    install.installed,
    没有安装。在创建根实例之前,请确保调用 Vue.use(VueRouter)。
    );
this.app = app;
const { mode, options, fallback } = this;
// 根据不同模式实例化不同基类,无效模式下抛出错误
switch (mode) {
  case 'history':
    this.history = new HTML5History(this, options.base);
    break;
  case 'hash':
    this.history = new HashHistory(this, options.base, fallback);
    break;
  case 'abstract':
    this.history = new AbstractHistory(this);
    break;
  default:
    assert(false, `invalid mode: ${mode}`);
}

// 执行父类监听函数,注册回调
this.history.listen(route => {
  this.app._route = route;
});

}

}

复制代码上述代码实现:

首先断言有没有安装插件,如果没有抛出错误提示。
在 VueRouter 上添加 Vue 实例.
根据不同模式实例化不同基类,无效模式下抛出错误
执行父类监听函数,注册回调:

在路由改变时替换 vue 实例上 _route 当前匹配的路由对象属性
响应式值得改变,从而触发视图的重新渲染
在 中拿到匹配的路由对象,渲染匹配到的路由组件。完成跳转。

根据上述基础示例,选用的 history 模式。我们先看一下该模式的实现。
HTML5History
/**

  • h5 - history 模式

  • @export

  • @class HTML5History

  • @extends {History}
    */
    export class HTML5History extends History {
    constructor(router: VueRouter, base: ?string) {
    // 调用父类,并传入VueRouter路由实例和基础路径
    super(router, base);

    // 跳转核心方法 跳转到跟=基础路径
    this.transitionTo(getLocation(this.base));

    const expectScroll = router.options.scrollBehavior;
    // 添加 popstate 监听函数
    window.addEventListener(‘popstate’, e => {
    _key = e.state && e.state.key;
    const current = this.current;
    this.transitionTo(getLocation(this.base), next => {
    if (expectScroll) {
    this.handleScroll(next, current, true);
    }
    });
    });

    // 若存在滚动行为配置,则添加 scroll 监听函数
    if (expectScroll) {
    window.addEventListener(‘scroll’, () => {
    saveScrollPosition(_key);
    });
    }
    }

/**

  • 前进对应步数
  • @param {number} n
  • @memberof HTML5History
    */
    go(n: number) {
    // 通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面
    window.history.go(n);
    }

/**

  • 导航到不同的 location 向 history 栈添加一个新的记录
  • @param {RawLocation} location
  • @memberof HTML5History
    */
    push(location: RawLocation) {
    // 拿到当前路由对象
    const current = this.current;
    // 调用跳转核心方法
    this.transitionTo(location, route => {
    pushState(cleanPath(this.base + route.fullPath));
    this.handleScroll(route, current, false);
    });
    }

/**

  • 导航到不同的 location 替换掉当前的 history 记录。
  • @param {RawLocation} location
  • @memberof HTML5History
    */
    replace(location: RawLocation) {
    const current = this.current;
    this.transitionTo(location, route => {
    replaceState(cleanPath(this.base + route.fullPath));
    this.handleScroll(route, current, false);
    });
    }

/**

  • 更新 URL
  • @memberof HTML5History
    */
    ensureURL() {
    if (getLocation(this.base) !== this.current.fullPath) {
    replaceState(cleanPath(this.base + this.current.fullPath));
    }
    }

/**

  • 处理页面切换时,滚动位置
  • @param {Route} to 将要去的路由对象
  • @param {Route} from 当前路由对象
  • @param {boolean} isPop 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用
  • @memberof HTML5History
    */
    handleScroll(to: Route, from: Route, isPop: boolean) {
    const router = this.router;
    // 若当前 Vue 组件实例不存在,直接return
    if (!router.app) {
    return;
    }
// 取路由 滚动行为 配置参数。 若不存在直接 return
// 使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样
// 只在html5历史模式下可用; 默认没有滚动行为; 返回false以防止滚动.
// { x: number, y: number }
// { selector: string, offset? : { x: number, y: number }}
const behavior = router.options.scrollBehavior;
if (!behavior) {
  return;
}
// 断言 其必须是函数,否则抛出异常
assert(typeof behavior === 'function', `scrollBehavior must be a function`);

// 等到重新渲染完成后再滚动
router.app.$nextTick(() => {
  // 获取滚动位置
  let position = getScrollPosition(_key);
  // 获取回调返回的滚动位置的对象信息
  const shouldScroll = behavior(to, from, isPop ? position : null);
  // 若不存在直接 return
  if (!shouldScroll) {
    return;
  }
  const isObject = typeof shouldScroll === 'object';
  // 处理模拟“滚动到锚点”的行为
  if (isObject && typeof shouldScroll.selector === 'string') {
    const el = document.querySelector(shouldScroll.selector);
    if (el) {
      position = getElementPosition(el);
    } else if (isValidPosition(shouldScroll)) {
      position = normalizePosition(shouldScroll);
    }
  } else if (isObject && isValidPosition(shouldScroll)) {
    position = normalizePosition(shouldScroll);
  }

  if (position) {
    // 把内容滚动到指定的坐标
    window.scrollTo(position.x, position.y);
  }
});

}
}
复制代码对 HTML5History 实现简析:

根据 init 方法 model: history 实例化 new HTML5History(this, options.base) 调用构造函数并传入 VueRouter 和 应用的基路径。
构造函数

调用父类 super(router, base),并传入 VueRouter 路由实例和基础路径;
调用核心过渡跳转方法 跳转到应用的基路径;
定义 popstate 监听函数, 并在回调里做相应跳转处理;
若在实例化 VueRouter 传入滚动行为配置 scrollBehavior 则添加滚动监听事件。回调为:滚动到上次标记位置点。

定义相应 Router 实例方法;和一些父类里调用子类实现的方法。

对于其提供的方法有很详细的注释信息,故接下来我们直接来看看父类的实现,其它两种模式也是继承了这个基类 History。
History

/**

  • History 基类
  • @export
  • @class History
    */
    export class History {
    router: VueRouter;
    base: string;
    current: Route;
    pending: ?Route;
    cb: (r: Route) => void;

// 以下这些方法由子类去实现
go: (n: number) => void;
push: (loc: RawLocation) => void;
replace: (loc: RawLocation) => void;
ensureURL: () => void; // 更新URL

constructor(router: VueRouter, base: ?string) {
// VueRouter 实例
this.router = router
// 应用的基路径
this.base = normalizeBase(base)
// 从一个表示 “nowhere” 的 route 对象开始
this.current = START
// 等待状态标志
this.pending = null
}

/**

  • 注册回调
  • @param {Function} cb
  • @memberof History
    */
    listen(cb: Function) {
    this.cb = cb;
    }

/**

  • 核心跳转方法
  • @param {RawLocation} location
  • @param {Function} [cb]
  • @memberof History
    */
    transitionTo(location: RawLocation, cb?: Function) { … }

// 最终过渡
confirmTransition(route: Route, cb: Function) { … }

// 路由更新
updateRoute(route: Route) { … }
}

/**

  • 规范化应用的基路径
  • @param {?string} base
  • @returns {string}
    */
    function normalizeBase(base: ?string): string {
    if (!base) {
    if (inBrowser) {
    // respect tag
    // HTML 元素 指定用于一个文档中包含的所有相对 URL 的根 URL。一份中只能有一个 元素。
    const baseEl = document.querySelector(‘base’)
    base = baseEl ? baseEl.getAttribute(‘href’) : ‘/’
    } else {
    base = ‘/’
    }
    }
    // 确保有开始斜杠
    if (base.charAt(0) !== ‘/’) {
    base = ‘/’ + base
    }
    // 去除末尾斜杠
    return base.replace(//$/, ‘’)
    }

复制代码
上述是 History 基类中所有代码实现。同样是添加几个属性,和一些跳转所需的核心方法。这里只需要大概了解其内部大概实现,之后会详细论述。
回到 HTML5History 构造函数的执行代码 this.transitionTo(getLocation(this.base)),调用核心过渡跳转方法 跳转到应用的基路径。

核心跳转方法 transitionTo
根据上述例子初始化时调用(HTML5History - this.transitionTo(getLocation(this.base));),这里的入参是:location: /, cb: undefined

/**

  • 核心跳转方法
  • @param {RawLocation} location 地址
  • @param {Function} [cb] 回调
  • @memberof History
    */
    transitionTo(location: RawLocation, cb?: Function) {
    // 获取路由匹配信息,传入 location 和 current属性
    const route = this.router.match(location, this.current)
    // 调用最终跳转方法,并传入路由对象信息,和回调
    // 回调:更新路由,执行传入回调, 更新 URL
    this.confirmTransition(route, () => {
    this.updateRoute(route)
    cb && cb(route)
    this.ensureURL()
    })
    }

复制代码根据上述代码实现简析:

传入地址和 current 属性,current 属性在调用 super 在父类里初始时被赋值为 START
// vue-router/src/history/base.js
// 从一个表示 “nowhere” 的 route 对象开始
this.current = START;
复制代码
START 代码实现及结果展示
START
// 表示初始状态的起始路径
export const START = createRoute(null, {
path: ‘/’
});

/**

  • 创建一个路由对象
  • @export
  • @param {?RouteRecord} record
  • @param {Location} location
  • @param {Location} [redirectedFrom]
  • @returns {Route}
    */
    export function createRoute(
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
    ): Route {
    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),
    matched: record ? formatMatch(record) : []
    };
    // 这里暂时不说
    if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom);
    }
    return Object.freeze(route);
    }

/**

  • 格式化匹配
  • @param {?RouteRecord} record
  • @returns {Array}
    */
    function formatMatch(record: ?RouteRecord): Array {
    const res = [];
    while (record) {
    res.unshift(record);
    record = record.parent;
    }
    return res;
    }

/**

  • 获取完整路径
  • @param {*} { path, query = {}, hash = ‘’ }
  • @returns
    */
    function getFullPath({ path, query = {}, hash = ‘’ }) {
    return (path || ‘/’) + stringifyQuery(query) + hash;
    }
    复制代码START 结果如下:
    START = {
    fullPath: “/”,
    hash: “”,
    matched: [],
    meta: {},
    name: null,
    params: {},
    path: “/”,
    query: {},
    proto: Object,
    ,
    }
    复制代码

首先调用 VueRouter 类的 match 属性, 该属性在初始化 VueRouter被赋值为一个方法。
// vue-router/src/index.js
this.match = createMatcher(options.routes || []);
复制代码
createMatcher 生成 match 的代码实现
createMatcher 实现

首先调用 createRouteMap 传入路由映射表,解构出路径、名称映射表
定义内建方法 match redirect _createRoute 最后 return match

/**

  • 创建匹配器

  • @export

  • @param {Array} routes

  • @returns {Matcher}
    */
    export function createMatcher(routes: Array): Matcher {

    const { pathMap, nameMap } = createRouteMap(routes);

    // 匹配函数
    function match(
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
    ): Route {

    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 {

    }

    return match;
    }

复制代码createRouteMap

首先创建 name、path 映射对象
对路由表内部每一项进行处理
最终返回包含 路径/名称的映射表

/**

  • 创建路由映射表
  • @export
  • @param {Array} routes
  • @returns {{
  • pathMap: Dictionary,
  • nameMap: Dictionary
  • }}
    */
    export function createRouteMap(
    routes: Array
    ): {
    pathMap: Dictionary,
    nameMap: Dictionary
    } {
    const pathMap: Dictionary = Object.create(null);
    const nameMap: Dictionary = Object.create(null);

// 对路由表内部每一项进行处理
routes.forEach(route => {
addRouteRecord(pathMap, nameMap, route);
});

return {
pathMap,
nameMap
};
}
复制代码addRouteRecord
/**

  • 添加路由记录
  • @param {Dictionary} pathMap 路径映射表
  • @param {Dictionary} nameMap 名称映射表
  • @param {RouteConfig} route 路由项
  • @param {RouteRecord} [parent] 父路由项
  • @param {string} [matchAs]
    */
    function addRouteRecord(
    pathMap: Dictionary,
    nameMap: Dictionary,
    route: RouteConfig,
    parent?: RouteRecord,
    matchAs?: string
    ) {
    // 解构路径和名称,若路径不存在,则抛出异常
    const { path, name } = route;
    assert(path != null, 在路由配置中需要“path”。);

// 定义路由记录构建选项
const record: RouteRecord = {
path: normalizePath(path, parent), // 规范化之后的路径
components: route.components || { default: route.component }, // 路由组件
instances: {},
name, // 路由的名称
parent, // 父路由
matchAs,
redirect: route.redirect, // 重定向
beforeEnter: route.beforeEnter, // 进入前钩子函数,形如:(to: Route, from: Route, next: Function) => void;
meta: route.meta || {} // 路由元信息
};

// 是否存在嵌套路由
if (route.children) {
// 如果路由已命名并具有默认子路由,则发出警告。
// 如果用户按名称导航到此路由,则不会呈现默认的子节点(GH问题#629)。
if (process.env.NODE_ENV !== ‘production’) {
if (
route.name &&
route.children.some(child => /^/?KaTeX parse error: Expected '}', got 'EOF' at end of input: … `命名路由'{route.name}'有一个默认的子路由。
当导航到这个命名路由(:to="{name: ‘${
route.name
}’")时,将不会呈现默认的子路由。
从此路由中删除该名称,并使用已命名链接的默认子路由的名称。`
);
}
}
// 若存在子路由,递归处理子路由表
route.children.forEach(child => {
addRouteRecord(pathMap, nameMap, child, record);
});
}

// 是否存在别名配置 string | Array
if (route.alias) {
// 处理数组情况
if (Array.isArray(route.alias)) {
// 递归处理别名配置项
route.alias.forEach(alias => {
addRouteRecord(
pathMap,
nameMap,
{ path: alias },
parent,
record.path
);
});
} else {
addRouteRecord(
pathMap,
nameMap,
{ path: route.alias },
parent,
record.path
);
}
}

// 分别项路径,名称映射表里新增记录
pathMap[record.path] = record;
if (name) nameMap[name] = record;
}

/**

  • 规范化路径
  • @param {string} path
  • @param {RouteRecord} [parent]
  • @returns {string}
    */
    function normalizePath(path: string, parent?: RouteRecord): string {
    path = path.replace(// / , ′ ′ ) ; / / 替 换 字 符 结 尾 为 ′ / ′ = > ′ ′ 如 : ′ / f o o / ′ = > ′ / f o o ′ i f ( p a t h [ 0 ] = = = ′ / ′ ) r e t u r n p a t h ; i f ( p a r e n t = = n u l l ) r e t u r n p a t h ; r e t u r n c l e a n P a t h ( ‘ /, ''); // 替换字符结尾为'/' => '' 如:'/foo/' => '/foo' if (path[0] === '/') return path; if (parent == null) return path; return cleanPath(` /,);///=>/foo/=>/fooif(path[0]===/)returnpath;if(parent==null)returnpath;returncleanPath({parent.path}/${path}`); // 替换 ‘//’ => ‘/’ 如:‘router//foo//’ => ‘router/foo/’
    }
    复制代码

上述代码主要对路由配置的每一项进行处理,最终写入相应的路径,名称映射表。

上述基础示例代码路由配置项处理之后为:
pathMap:{
‘’: {
beforeEnter: undefined,
components: {
default: { template: “

home
” },
proto: Object
},
instances: {},
matchAs: undefined,
meta: {},
name: undefined,
parent: undefined,
path: “”,
redirect: undefined,
proto: Object,
},
‘/bar’: {
beforeEnter: undefined,
components: {
default: {template: “
bar
”},
proto: Object
},
instances: {},
matchAs: undefined,
meta: {},
name: undefined,
parent: undefined,
path: “/bar”,
redirect: undefined,
proto: Object
},
‘/bar/child’: {
beforeEnter: undefined,
components: {
default: {template: “
Child
”},
proto: Object
},
instances: {},
matchAs: undefined,
meta: {},
name: undefined,
parent: {path: “/bar”, … },
path: “/bar/child”,
redirect: undefined,
proto: Object
},
‘/foo’: {
beforeEnter: undefined,
components: {
default: {template: “
foo
”},
proto: Object
},
instances: {},
matchAs: undefined,
meta: {},
name: undefined,
parent: undefined,
path: “/foo”,
redirect: undefined,
proto: Object
}
}

nameMap:{}

复制代码

知道了 const { pathMap, nameMap } = createRouteMap(routes); 解构的实现及结果,咱们继续看 match 的代码实现
function match(
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute);
const { name } = location;

if (name) {
// 若存在名称,从名称映射表中取对应记录
const record = nameMap[name];
if (record) {
// 处理路径
location.path = fillParams(
record.path,
location.params,
named route "${name}"
);
return _createRoute(record, location, redirectedFrom);
}
} else if (location.path) {
location.params = {};
for (const path in pathMap) {
if (matchRoute(path, location.params, location.path)) {
return _createRoute(pathMap[path], location, redirectedFrom);
}
}
}
// 没有匹配直接传入 null。
return _createRoute(null, location);
}
复制代码

规范化目标路由的链接

normalizeLocation 代码实现
normalizeLocation
/**

  • 规范化目标路由的链接
  • @export
  • @param {RawLocation} raw 目标路由的链接
  • @param {Route} [current] 当前路由
  • @param {boolean} [append] 是否在当前 (相对) 路径前添加基路径
  • @returns {Location}
    */
    export function normalizeLocation(
    raw: RawLocation,
    current?: Route,
    append?: boolean
    ): Location {
    // 处理目标路由的链接(to),我们知道其支持多种写法
    // ‘home’
    // { path: ‘home’ }
    // { path: /user/${userId} }
    // { name: ‘user’, params: { userId: 123 }}
    // { path: ‘register’, query: { plan: ‘private’ }}
    const next: Location = typeof raw === ‘string’ ? { path: raw } : raw;
    // 若已经被规范化或存在name属性直接返回 next
    if (next.name || next._normalized) {
    return next;
    }
    // 解析路径 返回 { path, query, hash }
    const parsedPath = parsePath(next.path || ‘’);
    // current.path - 字符串,对应当前路由的路径,总是解析为绝对路径
    const basePath = (current && current.path) || ‘/’;
    // 获取最终路径地址
    const path = parsedPath.path
    ? resolvePath(parsedPath.path, basePath, append)
    : (current && current.path) || ‘/’;
    // 获取查询参数
    const query = resolveQuery(parsedPath.query, next.query);
    // 当前路由的 hash 值 (带 #) ,如果没有 hash 值,则为空字符串
    let hash = next.hash || parsedPath.hash;
    if (hash && hash.charAt(0) !== ‘#’) {
    hash = #${hash};
    }

return {
_normalized: true,
path,
query,
hash
};
}
复制代码

normalizeLocation 所涉及的函数调用的代码实现

parsePath 代码实现
解析路径
/**

  • 解析路径
  • @export
  • @param {string} path
  • @returns {{
  • path: string;
  • query: string;
  • hash: string;
  • }}
    */
    export function parsePath(
    path: string
    ): {
    path: string,
    query: string,
    hash: string
    } {
    let hash = ‘’;
    let query = ‘’;

// 是否存在 #
const hashIndex = path.indexOf(’#’);
if (hashIndex >= 0) {
hash = path.slice(hashIndex); // 截取 hash 值
path = path.slice(0, hashIndex); // 截取路径
}

// 是否存在查询参数
const queryIndex = path.indexOf(’?’);
if (queryIndex >= 0) {
query = path.slice(queryIndex + 1); // 截取参数
path = path.slice(0, queryIndex); // 截取路径
}

return {
path,
query,
hash
};
}
复制代码

resolvePath 代码实现
导出处理之后的路径地址
/**

  • 导出处理之后的路径地址
  • @export
  • @param {string} relative 相对路径
  • @param {string} base 基础路径
  • @param {boolean} [append] 是否在当前 (相对) 路径前添加基路径
  • @returns {string}
    */
    export function resolvePath(
    relative: string,
    base: string,
    append?: boolean
    ): string {
    if (relative.charAt(0) === ‘/’) {
    return relative;
    }

if (relative.charAt(0) === ‘?’ || relative.charAt(0) === ‘#’) {
return base + relative;
}

// ‘/vue-router/releases’ => ["", “vue-router”, “releases”]
const stack = base.split(’/’);

// 删除后段
// - 没有附加
// - 附加到尾随斜杠(最后一段为空)
if (!append || !stack[stack.length - 1]) {
stack.pop();
}

// resolve 相对路径
// ‘/vue-router/releases’.replace(/^//, ‘’) => “vue-router/releases”
// ‘vue-router/releases’.split(’/’) => [“vue-router”, “releases”]
const segments = relative.replace(/^//, ‘’).split(’/’);
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
if (segment === ‘.’) {
continue;
} else if (segment === ‘…’) {
stack.pop();
} else {
stack.push(segment);
}
}

// 确保领先的削减 ensure leading slash
if (stack[0] !== ‘’) {
stack.unshift(’’);
}

return stack.join(’/’);
}
复制代码

resolveQuery 代码实现
导出处理之后的路径地址
/**

  • 导出查询参数
  • @export
  • @param {?string} query
  • @param {Dictionary} [extraQuery={}]
  • @returns {Dictionary}
    */
    export function resolveQuery(
    query: ?string,
    extraQuery: Dictionary = {}
    ): Dictionary {
    if (query) {
    let parsedQuery;
    try {
    parsedQuery = parseQuery(query);
    } catch (e) {
    warn(false, e.message);
    parsedQuery = {};
    }
    for (const key in extraQuery) {
    parsedQuery[key] = extraQuery[key];
    }
    return parsedQuery;
    } else {
    return extraQuery;
    }
    }

/**

  • 解析查询参数
  • @param {string} query
  • @returns {Dictionary}
    */
    function parseQuery(query: string): Dictionary {
    const res = Object.create(null);

// 匹配 ?、#、& 开头的字符串 如:’?id=1’.match(/^(?|#|&)/) => ["?", “?”, index: 0, input: “?id=1”, groups: undefined]
// ‘?id=1&name=cllemon’.replace(/^(?|#|&)/, ‘’) => id=1&name=cllemon
query = query.trim().replace(/^(?|#|&)/, ‘’);

if (!query) {
return res;
}

// 如上例: => [“id=1”, “name=cllemon”]
query.split(’&’).forEach(param => {
// 匹配 ”+“
// 如上例:“id=1” => [“id”, “1”]
const parts = param.replace(/+/g, ’ ‘).split(’=’);
// 如上例:[“id”, “1”] => ‘id’
// 解码由 decode 等于 decodeURIComponent() 方法用于 encodeURIComponent 方法或者其它类似方法编码的部分统一资源标识符(URI)。
const key = decode(parts.shift());
// 如上例:[“1”]
const val = parts.length > 0 ? decode(parts.join(’=’)) : null;

if (res[key] === undefined) {
  res[key] = val;
} else if (Array.isArray(res[key])) {
  res[key].push(val);
} else {
  res[key] = [res[key], val];
}

});

return res;
}
复制代码

fillParams 填充参数
const regexpCompileCache: {
[key: string]: Function
} = Object.create(null);

/**

  • 填充参数
  • @param {string} path
  • @param {?Object} params
  • @param {string} routeMsg
  • @returns {string}
    */
    function fillParams(
    path: string,
    params: ?Object,
    routeMsg: string
    ): string {
    try {
    // 第三方库 path-to-regexp: 将路径字符串(例如/user/:name)转换为正则表达式
    // compile : 用于将字符串转换为有效路径。
    // 如: const toPath = Regexp.compile(’/user/:id’)
    // toPath({ id: 123 }) //=> “/user/123”
    const filler =
    regexpCompileCache[path] ||
    (regexpCompileCache[path] = Regexp.compile(path));
    return filler(params || {}, { pretty: true });
    } catch (e) {
    assert(false, missing param for ${routeMsg}: ${e.message});
    return ‘’;
    }
    }
    复制代码

_createRoute 根据不同的配置项信息调用不同的路由创建处理函数
function _createRoute(
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
// 重定向处理函数
if (record && record.redirect) {
return redirect(record, redirectedFrom || location);
}
// 别名处理处理函数
if (record && record.matchAs) {
return alias(record, location, record.matchAs);
}
// createRoute 在 “START 代码实现及结果展示” 中以有论述
return createRoute(record, location, redirectedFrom);
}
复制代码

最终 route(通过 match 函数返回的值) 值为:
route = {
fullPath: ‘/’,
hash: ‘’,
matched: [
{
beforeEnter: undefined,
components: {
default: {
template: ‘

home

}
},
instances: {},
matchAs: undefined,
meta: {},
name: undefined,
parent: undefined,
path: ‘’,
redirect: undefined
}
],
meta: {},
name: undefined,
params: {},
path: ‘/’,
query: {},
proto: Object
};
复制代码最终跳转方法 confirmTransition

/**

  • 确认跳转
  • @param {Route} route 目录路由信息
  • @param {Function} cb
  • @memberof History
    */
    confirmTransition(route: Route, cb: Function) {
    const current = this.current
    // 是不是相同路由
    if (isSameRoute(route, current)) {
    this.ensureURL()
    return
    }

const {
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)

// 执行队列
const queue: Array = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// enter guards beforeEnter: (to, from, next) => {}
activated.map(m => m.beforeEnter),
// 异步组件
resolveAsyncComponents(activated)
)

this.pending = route
// 迭代方法
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) return
// route: 即将要进入的目标 路由对象 current: 当前导航正要离开的路由 next: 调用该方法来 resolve 这个钩子
hook(route, current, (to: any) => {
// to === false: 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL()
} else if (typeof to === ‘string’ || typeof to === ‘object’) {
// next(’/’) 或者 next({ path: ‘/’ }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。
// next(’/’) or next({ path: ‘/’ }) -> redirect
this.push(to)
} else {
// 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
// 确认转换并传递值
next(to)
}
})
}
// 队列执行函数
runQueue(queue, iterator, () => {
const postEnterCbs = []
// 在提取组件内的 enter 守卫之前,请等待异步组件被解析
runQueue(extractEnterGuards(activated, postEnterCbs), iterator, () => {
if (this.pending === route) {
this.pending = null
cb(route)
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => cb())
})
}
})
})
}

复制代码

到此,路由核心基类已经全部梳理完毕

上述代码逻辑很清晰,有详细的注释,这里直接略过文字描述,上述代码所涉及的函数调用 :

resolveQueue 的代码实现
/**

  • 比对当前路由和目标路由,导出失活和激活路由信息
  • @param {Array} current
  • @param {Array} next
  • @returns {{
  • activated: Array,
  • deactivated: Array
  • }}
    */
    function resolveQueue(
    current: Array,
    next: Array
    ): {
    activated: Array,
    deactivated: Array
    } {
    let i;
    const max = Math.max(current.length, next.length);
    for (i = 0; i < max; i++) {
    if (current[i] !== next[i]) {
    break;
    }
    }
    return {
    activated: next.slice(i),
    deactivated: current.slice(i)
    };
    }
    复制代码

extractLeaveGuards 的代码实现
/**

  • 提取离开的路由对象
  • @param {Array} matched
  • @returns {Array}
    */
    function extractLeaveGuards(matched: Array): Array {
    // 返回反转之后的数组元素
    return flatMapComponents(matched, (def, instance) => {
    // 提取匹配路由的组件路由守卫钩子函数
    const guard = def && def.beforeRouteLeave;
    if (guard) {
    return function routeLeaveGuard() {
    return guard.apply(instance, arguments);
    };
    }
    }).reverse();
    }
    复制代码

resolveAsyncComponents 的代码实现
/**

  • 加载异步组件

  • @param {Array} matched

  • @returns {Array}
    */
    function resolveAsyncComponents(
    matched: Array
    ): Array {
    return flatMapComponents(matched, (def, _, match, key) => {
    // 如果它是一个函数并且没有附加 Vue 选项,
    // 那么假设它是一个异步组件解析函数
    // 我们没有使用 Vue 的默认异步解析机制
    // 因为我们希望在解析传入组件之前停止导航。
    if (typeof def === ‘function’ && !def.options) {
    return (to, from, next) => {
    const resolve = resolvedDef => {
    match.components[key] = resolvedDef;
    next();
    };

     const reject = reason => {
       warn(false, `Failed to resolve async component ${key}: ${reason}`);
       next(false);
     };
    
     const res = def(resolve, reject);
     if (res && typeof res.then === 'function') {
       res.then(resolve, reject);
     }
    

    };
    }
    });
    }
    复制代码

runQueue 的代码实现
/**

  • 执行对列

  • @export

  • @param {Array} queue

  • @param {Function} fn

  • @param {Function} cb
    */
    export function runQueue(
    queue: Array,
    fn: Function,
    cb: Function
    ) {
    const step = index => {
    if (index >= queue.length) {
    cb();
    } else {
    if (queue[index]) {
    fn(queue[index], () => {
    step(index + 1);
    });
    } else {
    step(index + 1);
    }
    }
    };
    step(0);
    }
    复制代码

    extractEnterGuards 的代码实现
    /**

  • 提取进入的路由对象

  • @param {Array} matched

  • @param {Array} cbs

  • @returns {Array}
    */
    function extractEnterGuards(
    matched: Array,
    cbs: Array
    ): Array {
    return flatMapComponents(matched, (def, _, match, key) => {
    // 提取组件内的守卫
    // 如:beforeRouteEnter (to, from, next) {}
    // 在渲染该组件的对应路由被 confirm 前调用, 不!能!获取组件实例 this, 因为当守卫执行前,组件实例还没被创建.
    const guard = def && def.beforeRouteEnter;
    if (guard) {
    return function routeEnterGuard(to, from, next) {
    // 不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
    // 如:next(vm => { // 通过 vm 访问组件实例 })
    return guard(to, from, cb => {
    next(cb);
    if (typeof cb === ‘function’) {
    cbs.push(() => {
    cb(match.instances[key]);
    });
    }
    });
    };
    }
    });
    }
    复制代码

/**

  • 导航到不同的 location 向 history 栈添加一个新的记录
  • @param {RawLocation} location
  • @memberof HTML5History
    */
    push(location: RawLocation) {
    // 拿到当前路由对象
    const current = this.current
    // 调用跳转核心方法
    this.transitionTo(location, route => {
    pushState(cleanPath(this.base + route.fullPath))
    this.handleScroll(route, current, false)
    })
    }
    复制代码

最后我们以路由实例的跳转方法(push)来梳理一下路由跳转的过程:

首先拿到当前路由信息。

调用 History 提供的 transitionTo 过渡跳转的方法,传入要跳转的路径信息 location (由上文分析,此处是一个经过规范化之后的路径信息),以及一个参数为路由信息的回调函数(主要用来更新浏览器 URL 和处理用户滚动行为的方法「前文已分析」)。

transitionTo 方法上拿到目标路径匹配的路由信息,调用 confirmTransition 并传入匹配路由信息及一个回调函数,该回调函数主要做:调用 updateRoute 更新路由,执行 transitionTo 内传入的回调,以及更新 URL。

confirmTransition 内首先过滤掉相同路由;然后调用 resolveQueue 方法传入当前路由匹配信息及目标路由匹配信息解构出 deactivated, activated ; 然后构造执行队列(处理全局钩子函数,异步组件解析等);然后定义执行方法;最后执行队列执行函数(具体实现看上述代码解析)。最终在队列内执行 transitionTo 传入的回调,更新路由,执行监听函数:更新 URL;更新 vue 根实例的 _route 属性,由值的改变,进而触发视图的重新渲染
在 中拿到匹配的路由对象,渲染匹配到的路由组件。完成跳转。

注册的两个全局组件: 和

组件是一个 functional 组件,渲染路径匹配到的视图组件。 渲染的组件还可以内嵌自己的 ,根据嵌套路径,渲染嵌套组件。
Props: 默认值: “default”; 如果 设置了名称,则会渲染对应的路由配置中 components 下的相应组件。

export default {
name: ‘router-view’,

functional: true, // functional 组件

props: {
// 用于匹配渲染对应的路由配置中 components 下的相应组件
name: {
type: String,
default: ‘default’
}
},

render(h, { props, children, parent, data }) {
data.routerView = true;

const route = parent.$route;
const cache = parent._routerViewCache || (parent._routerViewCache = {});
let depth = 0;
let inactive = false; // 是否失活

// 循环寻找父节点,找到当前组件嵌套深度
while (parent) {
  if (parent.$vnode && parent.$vnode.data.routerView) {
    depth++;
  }
  // 处理 keep-alive _inactive vue 内部属性
  if (parent._inactive) {
    inactive = true;
  }
  parent = parent.$parent;
}

// 设置当前 router-view 所属层级
data.routerViewDepth = depth;
// 获取对应层级的匹配项
const matched = route.matched[depth];
// 若不存在,直接渲染为空
if (!matched) {
  return h();
}

// 若失活,直接从缓存中取
const component = inactive
  ? cache[props.name]
  : (cache[props.name] = matched.components[props.name]);

// keep-alive 非失活组件,
if (!inactive) {
  // 添加钩子函数,更新匹配的组件实例
  (data.hook || (data.hook = {})).init = vnode => {
    debugger;
    matched.instances[props.name] = vnode.child;
  };
}

return h(component, data, children);

}
};
复制代码

组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to 属性指定目标地址,默认渲染成带有正确链接的 标签,可以通过配置 tag 属性生成别的标签。
比起写死的 会好一些。
更多参阅 vue-router 文档。

/* @flow */

import { cleanPath } from ‘…/util/path’;
import { createRoute, isSameRoute, isIncludedRoute } from ‘…/util/route’;
import { normalizeLocation } from ‘…/util/location’;

// 解决奇怪的 flow bug
const toTypes: Array = [String, Object];

export default {
name: ‘router-link’,
props: {
// 目标路由的链接
to: {
type: toTypes,
required: true
},
// 标签名称
tag: {
type: String,
default: ‘a’
},
// 是否激活
exact: Boolean,
// 是否在当前 (相对) 路径前添加基路径 如:/a => /b (true:/a/b; false: /b)
append: Boolean,
// true: 当点击时,会调用 router.replace() 而不是 router.push() 不会留下 history 记录
replace: Boolean,
// 链接激活时使用的 CSS 类名
activeClass: String
},

render(h: Function) {
const router = this. r o u t e r ; c o n s t c u r r e n t = t h i s . router; const current = this. router;constcurrent=this.route;
// 规范化 目标路由的链接
const to = normalizeLocation(this.to, current, this.append);
const resolved = router.match(to);
const fullPath = resolved.redirectedFrom || resolved.fullPath;
const base = router.history.base;
const href = base ? cleanPath(base + fullPath) : fullPath;
const classes = {};
const activeClass =
this.activeClass ||
router.options.linkActiveClass ||
‘router-link-active’;
const compareTarget = to.path ? createRoute(null, to) : resolved;
classes[activeClass] = this.exact
? isSameRoute(current, compareTarget)
: isIncludedRoute(current, compareTarget);

const on = {
  click: e => {
    // 阻止浏览器默认行为 防止a跳转
    e.preventDefault();
    if (this.replace) {
      router.replace(to);
    } else {
      router.push(to);
    }
  }
};

const data: any = {
  class: classes
};

if (this.tag === 'a') {
  data.on = on;
  data.attrs = { href };
} else {
  // find the first  child and apply listener and href
  const a = findAnchor(this.$slots.default);
  if (a) {
    const aData = a.data || (a.data = {});
    aData.on = on;
    const aAttrs = aData.attrs || (aData.attrs = {});
    aAttrs.href = href;
  }
}

return h(this.tag, data, this.$slots.default);

}
};

function findAnchor(children) {
if (children) {
let child;
for (let i = 0; i < children.length; i++) {
child = children[i];
if (child.tag === ‘a’) {
return child;
}
if (child.children && (child = findAnchor(child.children))) {
return child;
}
}
}
}
复制代码结语
最后的最后,Hash 和 Abstract 两种模式都是依托于 History 基类去实现,这里就不做深入分析了。
若对此感兴趣请参阅 vue-router。
更多细节部分,建议直接把 vue-router 项目拉下来,本地跑起来,根据 vue-router 提供的用例分析,一定收获满满。

你可能感兴趣的:(vue-router 源代码全流程分析「长文」)