IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    VueRouter 2.x 源码学习 · 看不见我的美 · 是你瞎了眼

    馬腊咯稽发表于 2022-01-06 00:00:00
    love 0
    VueRouter 2.x 源码学习

    Vue.use(Router)

    Vue.use 会调用 Router 的静态 install 方法:

    1. 保存 Vue 构造函数,方便其他组件调用 Vue 的工具函数;
    2. 在每个 Vue 实例中混入钩子:
      1. 定义 _routerRoot、_router、_route(getter/setter);
      2. 调用 Router 实例的 init 方法;
      3. 注册路由实例。
    3. 在 Vue 原型上挂载 $router、$route;
    4. 注册 router-view 和 router-link 组件。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    
    export let _Vue; // 别的地方有用到 _Vue.extend
    export function install(Vue) {
     // 防止重复 install
     if (install.installed) return;
     install.installed = true;
     // 保存 Vue
     _Vue = Vue;
     const isDef = v => v !== undefined;
     const registerInstance = (vm, callVal) => {
     let i = vm.$options._parentVnode;
     if (
     isDef(i) &&
     isDef((i = i.data)) &&
     isDef((i = i.registerRouteInstance))
     ) {
     /**
     * 调用 vm.$options._parentVnode.data.registerRouteInstance
     * data.registerRouteInstance 存在于 router-view 中
     */
     i(vm, callVal);
     }
     };
     // 混入钩子
     Vue.mixin({
     beforeCreate() {
     // new Vue({router, ...}),挂载根实例
     if (isDef(this.$options.router)) {
     this._routerRoot = this;
     // 传入的 new Router({mode, routes, ...})
     this._router = this.$options.router;
     // 调用 _router 的 init 方法
     this._router.init(this);
     /**
     * defineReactive(obj, key, val, customSetter, shallow)
     * 将 this._route 转化为 getter/setter,render 被调用时
     * router.view 会访问 this._route,触发 getter
     * 当 this._route 更新后,setter 调用
     * router.view 渲染匹配的组件
     */
     Vue.util.defineReactive(this, '_route', this._router.history.current);
     } else {
     // 向上查找 _routerRoot
     this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
     }
     registerInstance(this, this);
     },
     destroyed() {
     registerInstance(this);
     }
     });
     // vm.$router = this._routerRoot._router = new Router({mode, routes, ...})
     Object.defineProperty(Vue.prototype, '$router', {
     get() {
     return this._routerRoot._router;
     }
     });
     // vm.$route = this._routerRoot._route
     Object.defineProperty(Vue.prototype, '$route', {
     get() {
     return this._routerRoot._route;
     }
     });
     // 注册 router-view 和 router-link 组件
     Vue.component('router-view', View);
     Vue.component('router-link', Link);
     const strats = Vue.config.optionMergeStrategies;
     strats.beforeRouteEnter = strats.beforeRouteLeave = strats.created;
    }
    

    VueRouter

     1
     2
     3
     4
     5
     6
     7
     8
     9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    
    export default class VueRouter {
     static install: () => void;
     static version: string;
     app: any;
     apps: Array<any>;
     ready: boolean;
     readyCbs: Array<Function>;
     options: RouterOptions;
     mode: string;
     history: HashHistory | HTML5History | AbstractHistory;
     matcher: Matcher;
     fallback: boolean;
     beforeHooks: Array<?NavigationGuard>;
     resolveHooks: Array<?NavigationGuard>;
     afterHooks: Array<?AfterNavigationHook>;
     constructor(options: RouterOptions = {}) {
     this.app = null;
     this.apps = [];
     this.options = options;
     this.beforeHooks = [];
     this.resolveHooks = [];
     this.afterHooks = [];
     this.matcher = createMatcher(options.routes || [], this);
     // 默认 hash 模式
     let mode = options.mode || 'hash';
     this.fallback = // 不支持 history,退回 hash
     mode === 'history' && !supportsPushState && options.fallback !== false;
     if (this.fallback) {
     mode = 'hash';
     }
     // 非浏览器环境使用 abstract 模式
     if (!inBrowser) {
     mode = 'abstract';
     }
     this.mode = mode;
     // options.base 就是"基路径"
     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:
     }
     }
     match(raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
     return this.matcher.match(raw, current, redirectedFrom);
     }
     get currentRoute(): ?Route {
     return this.history && this.history.current;
     }
     // 每个组件的 beforeCreate 会调用 init 方法,将当前实例传进来
     init(app: any) {
     // 向 this.apps 添加当前 Vue 实例
     this.apps.push(app);
     // 不要重复 init
     if (this.app) {
     return;
     }
     // _router.app = vm
     this.app = app;
     const history = this.history;
     if (history instanceof HTML5History) {
     /**
     * getCurrentLocation 就是截取掉"基路径"后的路径
     * url:https://router.vuejs.org/zh/api/?abc=123#hahaha,base: /zh
     * getCurrentLocation => /api/?abc=123#hahaha
     */
     history.transitionTo(history.getCurrentLocation());
     } else if (history instanceof HashHistory) {
     // 绑定 hashchange 事件
     const setupHashListener = () => {
     history.setupListeners();
     };
     history.transitionTo(
     /**
     * getCurrentLocation 就是截取掉 # 后边的值
     * url:https://router.vuejs.org/zh/api/?abc=123#hahaha,base: /zh
     * getCurrentLocation => hahaha
     */
     history.getCurrentLocation(),
     setupHashListener,
     setupHashListener
     );
     }
     /**
     * transitionTo 的 onComplete 会调用 listen 里的函数
     * 路由改变的时候,所有 Vue 实例里的 _route 都要改变
     */
     history.listen(route => {
     this.apps.forEach(app => {
     app._route = route;
     });
     });
     }
     beforeEach(fn: Function): Function {
     return registerHook(this.beforeHooks, fn);
     }
     beforeResolve(fn: Function): Function {
     return registerHook(this.resolveHooks, fn);
     }
     afterEach(fn: Function): Function {
     return registerHook(this.afterHooks, fn);
     }
     onReady(cb: Function, errorCb?: Function) {
     this.history.onReady(cb, errorCb);
     }
     onError(errorCb: Function) {
     this.history.onError(errorCb);
     }
     push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
     this.history.push(location, onComplete, onAbort);
     }
     replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
     this.history.replace(location, onComplete, onAbort);
     }
     go(n: number) {
     this.history.go(n);
     }
     back() {
     this.go(-1);
     }
     forward() {
     this.go(1);
     }
     getMatchedComponents(to?: RawLocation | Route): Array<any> {
     const route: any = to
     ? to.matched
     ? to
     : this.resolve(to).route
     : this.currentRoute;
     if (!route) {
     return [];
     }
     return [].concat.apply(
     [],
     route.matched.map(m => {
     return Object.keys(m.components).map(key => {
     return m.components[key];
     });
     })
     );
     }
     resolve(
     to: RawLocation,
     current?: Route,
     append?: boolean
     ): {
     location: Location;
     route: Route;
     href: string;
     normalizedTo: Location;
     resolved: Route;
     } {
     const location = normalizeLocation(
     to,
     current || this.history.current,
     append,
     this
     );
     const route = this.match(location, current);
     const fullPath = route.redirectedFrom || route.fullPath;
     const base = this.history.base;
     const href = createHref(base, fullPath, this.mode);
     return {
     location,
     route,
     href,
     normalizedTo: location,
     resolved: route
     };
     }
     addRoutes(routes: Array<RouteConfig>) {
     this.matcher.addRoutes(routes);
     if (this.history.current !== START) {
     this.history.transitionTo(this.history.getCurrentLocation());
     }
     }
    }
    

    BaseHistory

     1
     2
     3
     4
     5
     6
     7
     8
     9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    
    export class History {
     router: Router;
     base: string;
     current: Route;
     pending: ?Route;
     cb: (r: Route) => void;
     ready: boolean;
     readyCbs: Array<Function>;
     readyErrorCbs: Array<Function>;
     errorCbs: Array<Function>;
     /**
     * 需要子类去实现的方法:
     * +go: (n: number) => void;
     * +push: (loc: RawLocation) => void;
     * +replace: (loc: RawLocation) => void;
     * +ensureURL: (push?: boolean) => void;
     * +getCurrentLocation: () => string;
     */
     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 = [];
     }
     listen(cb: Function) {
     // 给 this.cb 赋值,会在 updateRoute 调用 this.cb
     this.cb = cb;
     }
     onReady(cb: Function, errorCb: ?Function) {
     if (this.ready) {
     cb();
     } else {
     this.readyCbs.push(cb);
     if (errorCb) {
     this.readyErrorCbs.push(errorCb);
     }
     }
     }
     onError(errorCb: Function) {
     this.errorCbs.push(errorCb);
     }
     // 用于路由切换
     transitionTo(
     location: RawLocation,
     onComplete?: Function,
     onAbort?: Function
     ) {
     // 匹配目标 location 的 route
     const route = this.router.match(location, this.current);
     // 确认切换(异步)
     this.confirmTransition(
     route,
     // onComplete
     () => {
     this.updateRoute(route); // 更新 route
     onComplete && onComplete(route);
     this.ensureURL();
     if (!this.ready) {
     this.ready = true;
     this.readyCbs.forEach(cb => {
     cb(route);
     });
     }
     },
     // onAbort
     err => {
     if (onAbort) {
     onAbort(err);
     }
     if (err && !this.ready) {
     this.ready = true;
     this.readyErrorCbs.forEach(cb => {
     cb(err);
     });
     }
     }
     );
     }
     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 {
     console.error(err);
     }
     }
     onAbort && onAbort(err);
     };
     if (
     // 如果 route 和 current 是相同路径(path、hash、query、params 一样)
     isSameRoute(route, current) &&
     // 并且匹配的长度一致
     route.matched.length === current.matched.length
     ) {
     // 就没必要跳转
     this.ensureURL();
     return abort();
     }
     // 找出 currentRecord 和 nextRecord 中的相同与不同
     const {
     updated, // 相同但需要更新的
     deactivated, // currentRecord 独有的
     activated // nextRecord 独有的
     } = resolveQueue(
     this.current.matched, // currentRecord 数组
     route.matched // nextRecord 数组
     );
     /**
     * 导航被触发
     * 在失活的组件里调用 beforeRouteLeave 守卫
     * 调用全局的 beforeEach 守卫
     * 在重用的组件里调用 beforeRouteUpdate 守卫
     * 在路由配置里调用 beforeEnter
     * 解析异步路由组件
     * 在被激活的组件里调用 beforeRouteEnter
     * 调用全局的 beforeResolve 守卫
     * 导航被确认
     * 调用全局的 afterEach 钩子
     * 触发 DOM 更新
     * 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入
     */
     const queue: Array<?NavigationGuard> = [].concat(
     // 组件内部的 beforeRouteLeave
     extractLeaveGuards(deactivated),
     // 全局 beforeEach
     this.router.beforeHooks,
     // 组件内部的 beforeRouteUpdate
     extractUpdateHooks(updated),
     // routes 路由表里写的 beforeEnter
     activated.map(m => m.beforeEnter),
     // 解析异步路由组件,会等待异步组件的加载,加载完成之后再导航
     resolveAsyncComponents(activated)
     );
     this.pending = route;
     const iterator = (hook: NavigationGuard, next) => {
     // next 被调用之后,会去拿数组里的下一个钩子函数
     if (this.pending !== route) {
     return abort();
     }
     try {
     // 有点 to、from、next 那味儿了
     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('/') 或 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, // queue 里的钩子会被一个一个的拿到 iterator 里执行
     () => {
     // queue 里的钩子被全部调用完成之后,会执行此函数
     const postEnterCbs = [];
     const isValid = () => this.current === route;
     // 获取组件里 beforeRouteEnter 钩子
     const enterGuards = extractEnterGuards(
     activated,
     postEnterCbs,
     isValid
     );
     // 拼接上全局 beforeResolve 钩子
     const queue = enterGuards.concat(this.router.resolveHooks);
     runQueue(queue, iterator, () => {
     // 新的 queue 里的钩子被全部调用完成之后,会执行此函数
     if (this.pending !== route) {
     return abort();
     }
     this.pending = null;
     onComplete(route);
     if (this.router.app) {
     // 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入
     this.router.app.$nextTick(() => {
     postEnterCbs.forEach(cb => {
     cb();
     });
     });
     }
     });
     }
     );
     }
     updateRoute(route: Route) {
     const prev = this.current;
     this.current = route;
     // 调用 this.cb(init 的时候,由 history.listen 赋值)
     this.cb && this.cb(route);
     this.router.afterHooks.forEach(hook => {
     hook && hook(route, prev);
     });
     }
    }
    export function runQueue(
     queue: Array<?NavigationGuard>,
     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);
    }
    

    HTML5History

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    
    export class HTML5History extends History {
     constructor(router: Router, base: ?string) {
     super(router, base);
     const expectScroll = router.options.scrollBehavior;
     if (expectScroll) {
     // popstate 时,保存滚动位置
     setupScroll();
     }
     // 当浏览器行为触发页面前进后退时,重新调用 transitionTo 完成页面切换
     window.addEventListener('popstate', e => {
     const current = this.current;
     this.transitionTo(getLocation(this.base), route => {
     if (expectScroll) {
     /**
     * handleScroll(router, to, from, isPop)
     * 调用 getScrollPosition 获取滚动位置(保存在 history 的 state 里)
     * 调用 router.options.scrollBehavior(to, from, position) 判断是否需要滚动
     * 调用 window.scrollTo 进行滚动
     */
     handleScroll(router, route, current, true);
     }
     });
     });
     }
     go(n: number) {
     window.history.go(n);
     }
     push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
     const { current: fromRoute } = this;
     this.transitionTo(
     location,
     route => {
     pushState(cleanPath(this.base + route.fullPath));
     handleScroll(this.router, route, fromRoute, false);
     onComplete && onComplete(route);
     },
     onAbort
     );
     }
     replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
     const { current: fromRoute } = this;
     this.transitionTo(
     location,
     route => {
     replaceState(cleanPath(this.base + route.fullPath));
     handleScroll(this.router, route, fromRoute, false);
     onComplete && onComplete(route);
     },
     onAbort
     );
     }
     ensureURL(push?: boolean) {
     if (getLocation(this.base) !== this.current.fullPath) {
     const current = cleanPath(this.base + this.current.fullPath);
     push ? pushState(current) : replaceState(current);
     }
     }
     getCurrentLocation(): string {
     return getLocation(this.base);
     }
    }
    

    HashHistory

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    
    export class HashHistory extends History {
     constructor(router: Router, base: ?string, fallback: boolean) {
     super(router, base);
     /**
     * checkFallback 会把 HTML5History 格式的链接转换为 HashHistory 格式的链接
     * 然后 location.replace 掉
     * url:https://router.vuejs.org/zh/api/?abc=123#hahaha,base: /zh
     * checkFallback => https://router.vuejs.org/zh/#/api/?abc=123#hahaha
     */
     if (fallback && checkFallback(this.base)) {
     return;
     }
     // /zh#/api/?abc=123#hahaha replace 成为 /zh/#/api/?abc=123#hahaha
     ensureSlash(); // 保证 # 前边有 /
     }
     // 延迟到应用挂载,防止事件过早触发
     setupListeners() {
     window.addEventListener('hashchange', () => {
     if (!ensureSlash()) {
     return;
     }
     this.transitionTo(getHash(), route => {
     replaceHash(route.fullPath);
     });
     });
     }
     push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
     this.transitionTo(
     location,
     route => {
     // 赋值 window.location.hash
     pushHash(route.fullPath);
     onComplete && onComplete(route);
     },
     onAbort
     );
     }
     replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
     this.transitionTo(
     location,
     route => {
     // 调用 window.location.replace
     replaceHash(route.fullPath);
     onComplete && onComplete(route);
     },
     onAbort
     );
     }
     go(n: number) {
     window.history.go(n);
     }
     ensureURL(push?: boolean) {
     const current = this.current.fullPath;
     if (getHash() !== current) {
     push ? pushHash(current) : replaceHash(current);
     }
     }
     getCurrentLocation() {
     return getHash();
     }
    }
    

    matcher

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    
    export function createMatcher(
     routes: Array<RouteConfig>, // options.routes
     router: VueRouter // VueRouter 实例
    ): Matcher {
     // 把传入的路由表转换为路由映射表
     const { pathList, pathMap, nameMap } = createRouteMap(routes);
     // addRoutes 就是把新来的 routes 添加到 pathList, pathMap, nameMap 里
     function addRoutes(routes) {
     createRouteMap(routes, pathList, pathMap, nameMap);
     }
     // 根据 RawLocation 和 currentRoute 返回一个 route
     function match(
     raw: RawLocation, // url 或者 Location 对象
     currentRoute?: Route,
     redirectedFrom?: Location
     ): Route {
     const location = normalizeLocation(raw, currentRoute, false, router);
     const { name } = location;
     if (name) {
     const record = nameMap[name]; // 获取 name 对应的 record
     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];
     const record = pathMap[path];
     if (matchRoute(record.regex, location.path, location.params)) {
     return _createRoute(record, location, redirectedFrom);
     }
     }
     }
     return _createRoute(null, location);
     }
     // ...
     return {
     match,
     addRoutes
     };
    }
    

    route-map

    传入 option.routes(开发者写的路由表)返回一个数组两个对象:

    1. 所有路由的 path 组成的数组;
    2. path 到 record 的映射关系;
    3. name 到 record 的映射关系。
     1
     2
     3
     4
     5
     6
     7
     8
     9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    
    export function createRouteMap(
     routes: Array<RouteConfig>,
     oldPathList?: Array<string>,
     oldPathMap?: Dictionary<RouteRecord>,
     oldNameMap?: Dictionary<RouteRecord>
    ) {
     // 用来控制路径匹配优先级
     const pathList: Array<string> = oldPathList || [];
     const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null);
     const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null);
     routes.forEach(route => {
     addRouteRecord(pathList, pathMap, nameMap, route);
     });
     // 确保 path 为通配符的路由一直在结尾
     for (let i = 0, l = pathList.length; i < l; i++) {
     if (pathList[i] === '*') {
     pathList.push(pathList.splice(i, 1)[0]);
     l--;
     i--;
     }
     }
     return {
     pathList, // routePath[]
     pathMap, // {path: record, ...}
     nameMap // {name: rocord, ...}
     };
    }
    function addRouteRecord(
     pathList: Array<string>,
     pathMap: Dictionary<RouteRecord>,
     nameMap: Dictionary<RouteRecord>,
     route: RouteConfig,
     parent?: RouteRecord,
     matchAs?: string
    ) {
     const { path, name } = route;
     // 去除 path 最后的 /,并拼接上 parent.path
     const normalizedPath = normalizePath(path, parent);
     const pathToRegexpOptions: PathToRegexpOptions =
     route.pathToRegexpOptions || {};
     if (typeof route.caseSensitive === 'boolean') {
     pathToRegexpOptions.sensitive = route.caseSensitive;
     }
     // 根据传入的 route 生成一个 record
     const record: RouteRecord = {
     path: normalizedPath, // 规范化之后的 path,带上父级路由的那种
     regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 将 path 解析成正则表达式
     // 一条 path 可以对应多个 components,通过 router-view 的 name 属性来判断渲染出口
     components: route.components || { default: route.component },
     instances: {},
     name,
     parent, // ParentRouteRecord
     matchAs,
     redirect: route.redirect,
     beforeEnter: route.beforeEnter,
     meta: route.meta || {},
     props:
     route.props == null
     ? {}
     : route.components
     ? route.props
     : { default: route.props }
     };
     if (route.children) {
     console.warn(
     '如果 route 有 name,没有重定向,有一个默认的子路由,如果通过 name 进行导航,默认的子路由不会渲染'
     );
     route.children.forEach(child => {
     const childMatchAs = matchAs
     ? cleanPath(`${matchAs}/${child.path}`)
     : undefined;
     // record 作为 children 的 parent,重新调用 addRouteRecord
     addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs);
     });
     }
     // 如果路由有别名,同样会生成一条 record
     if (route.alias !== undefined) {
     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
     );
     });
     }
     if (!pathMap[record.path]) {
     pathList.push(record.path);
     pathMap[record.path] = record;
     }
     if (name) {
     if (!nameMap[name]) {
     nameMap[name] = record;
     }
     }
    }
    

    router-link

     1
     2
     3
     4
     5
     6
     7
     8
     9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    
    export default {
     name: 'router-link',
     props: {
     to: {
     type: [String, Object],
     required: true
     },
     // router-link 默认就是 a 标签
     tag: {
     type: String,
     default: 'a'
     },
     exact: Boolean,
     append: Boolean,
     replace: Boolean,
     activeClass: String,
     exactActiveClass: String,
     event: {
     type: [String, Array],
     default: 'click'
     }
     },
     render(h: Function) {
     const router = this.$router;
     const current = this.$route;
     const { location, route, href } = router.resolve(
     this.to,
     current,
     this.append
     );
     const classes = {};
     const globalActiveClass = router.options.linkActiveClass;
     const globalExactActiveClass = router.options.linkExactActiveClass;
     // 不同状态的类名处理
     const activeClassFallback =
     globalActiveClass == null ? 'router-link-active' : globalActiveClass;
     const exactActiveClassFallback =
     globalExactActiveClass == null
     ? 'router-link-exact-active'
     : globalExactActiveClass;
     const activeClass =
     this.activeClass == null ? activeClassFallback : this.activeClass;
     const exactActiveClass =
     this.exactActiveClass == null
     ? exactActiveClassFallback
     : this.exactActiveClass;
     const compareTarget = location.path
     ? createRoute(null, location, null, router)
     : route;
     classes[exactActiveClass] = isSameRoute(current, compareTarget);
     classes[activeClass] = this.exact
     ? classes[exactActiveClass]
     : isIncludedRoute(current, compareTarget);
     const handler = e => {
     /**
     * 按下 meta、alt、ctrl、shift 时不起作用
     * 调用 preventDefault 时不起作用
     * target=_blank 时不起作用
     * 右键点击时不起作用
     */
     if (guardEvent(e)) {
     // 调用 replace 和 push
     if (this.replace) {
     router.replace(location);
     } else {
     router.push(location);
     }
     }
     };
     const on = { click: guardEvent };
     if (Array.isArray(this.event)) {
     this.event.forEach(e => {
     on[e] = handler;
     });
     } else {
     on[this.event] = handler;
     }
     const data: any = {
     class: classes
     };
     // 创建 a 链接之后,会在 a 链接上绑定 href 属性和 click 事件
     if (this.tag === 'a') {
     data.on = on;
     data.attrs = { href };
     } else {
     // 找到子元素中的 a 标签,在上面加 listener 和 href
     const a = findAnchor(this.$slots.default);
     if (a) {
     // in case the <a> is a static node
     a.isStatic = false;
     const extend = _Vue.util.extend;
     const aData = (a.data = extend({}, a.data));
     aData.on = on;
     const aAttrs = (a.data.attrs = extend({}, a.data.attrs));
     aAttrs.href = href;
     } else {
     // 没 a 标签,那就把 listener 加到自己身上
     data.on = on;
     }
     }
     return h(this.tag, data, this.$slots.default);
     }
    };
    

    router-view

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    
    export default {
     name: 'router-view',
     functional: true,
     props: {
     name: {
     type: String,
     default: 'default'
     }
     },
     render(_, { props, children, parent, data }) {
     data.routerView = true;
     // 借用父级的 h 函数,酱紫 router-view 渲染的组件可以解析具名插槽
     const h = parent.$createElement;
     // 当同一层级同时有多个 router-view 存在时,会通过 name 属性来确定渲染哪个组件
     const name = props.name;
     const route = parent.$route;
     const cache = parent._routerViewCache || (parent._routerViewCache = {});
     // 当前 router-view 所处的层级(嵌套深度,上层还有几个 router-view),顺便检查当前是否 active
     let depth = 0;
     let inactive = false;
     while (parent && parent._routerRoot !== parent) {
     if (parent.$vnode && parent.$vnode.data.routerView) {
     depth++;
     }
     if (parent._inactive) {
     inactive = true;
     }
     parent = parent.$parent;
     }
     data.routerViewDepth = depth;
     // 如果是 inactive 状态,渲染前一个视图
     if (inactive) {
     return h(cache[name], data, children);
     }
     // 当前 route 下,对应层级有没有要渲染的组件
     const matched = route.matched[depth];
     // 没有匹配上,就渲染空节点
     if (!matched) {
     cache[name] = null;
     return h();
     }
     // 获取匹配到的组件
     const component = (cache[name] = matched.components[name]);
     // data 上保存了一个函数,被实例注入的生命周期钩子调用
     data.registerRouteInstance = (vm, val) => {
     const current = matched.instances[name];
     if ((val && current !== vm) || (!val && current === vm)) {
     matched.instances[name] = val;
     }
     };
     (data.hook || (data.hook = {})).prepatch = (_, vnode) => {
     matched.instances[name] = vnode.componentInstance;
     };
     // 解析 props
     data.props = resolveProps(route, matched.props && matched.props[name]);
     // 调用 h 函数,渲染匹配到的组件
     return h(component, data, children);
     }
    };
    

    总之,VueRouter 在初始化的时候,会向组件 mixin 一些钩子函数;在 beforeCreate 中,会借助 Vue.util.defineReactive API 将 vm._route 转换为 getter/setter;又因为 router-view 在调用 h 函数渲染组件的时候,会访问 vm._route.matched,router-view 所在实例的 renderWatcher 形成对 vm._route 的依赖。所以,每当 vm._route 变化,router-view 会自动匹配自己要渲染的组件。

    router-link 默认会渲染出一个绑定了事件的 a 标签,用户点击时,会调用 transitionTo 进行导航;transitionTo 会计算出最新的 route,调用 History API 更新 URL,调用路由钩子,更新 vm._route,触发 router-view 更新。

    至于 popstate 事件,主要用来处理浏览器的行为(history.pushState/replaceState 并不会触发 popstate)并调用 transitionTo 更新页面。



沪ICP备19023445号-2号
友情链接