vue-router源码浅显分析(一)--Vue.use(VueRouter), new VueRouter(options),HashHistory

朴灿烈づ我的快乐病毒、 2022-03-18 08:44 434阅读 0赞

1,vue中使用vueRouter需要通过vue.use(vueRouter),众所周知,这里实际上调用了VueRouter的install方法,对Vue进行了扩展,vueRouter的install方法如下:

  1. function install (Vue) {
  2. //如果已经挂载了,跳过
  3. if (install.installed && _Vue === Vue) { return }
  4. //如果未挂载过, installed属性改为true
  5. install.installed = true;
  6. //获取当前vue
  7. _Vue = Vue;
  8. var isDef = function (v) { return v !== undefined; };
  9. var registerInstance = function (vm, callVal) {
  10. var i = vm.$options._parentVnode;
  11. if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
  12. i(vm, callVal);
  13. }
  14. };
  15. //在vue的beforeCreate钩子和destroyed钩子中新增关于路由的处理
  16. Vue.mixin({
  17. beforeCreate: function beforeCreate () {
  18. if (isDef(this.$options.router)) {
  19. this._routerRoot = this;
  20. this._router = this.$options.router; //new VueRouter()
  21. this._router.init(this); //如果Vue实例参数中,有指定的router属性,执行init初始化
  22. //调用vue.util里的方法,通过get和set劫持this._route,router-view实时更新
  23. Vue.util.defineReactive(this, '_route', this._router.history.current);
  24. } else {
  25. this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
  26. }
  27. registerInstance(this, this);
  28. },
  29. destroyed: function destroyed () {
  30. registerInstance(this);
  31. }
  32. });
  33. //给vue新增$router, $route属性,并指定get方法返回值,$route属性。于是就有了,this.$router和this.$route对象可以使用。
  34. Object.defineProperty(Vue.prototype, '$router', {
  35. //this._routerRoot => this (即vue)
  36. //this._router =>new VueRouter()
  37. get: function get () { return this._routerRoot._router }
  38. });
  39. Object.defineProperty(Vue.prototype, '$route', {
  40. get: function get () { return this._routerRoot._route }
  41. });
  42. //新增子组件RouterView, RouterLink
  43. Vue.component('RouterView', View);
  44. Vue.component('RouterLink', Link);
  45. var strats = Vue.config.optionMergeStrategies;
  46. // 默认一下组件内导航钩子跟created方法一样
  47. strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
  48. }

对于配置了路由的vue实例,install方法中做了如下两步操作:

1. this._router = this.$options.router; 获取VueRouter实例(传入的router:router 对象, router = new VueRouter({routes:[]}))

2. this._router.init(this) 重点, 执行VueRouter的init方法

———————————————————-分割线——————————————————-

第一步: new VueRouter({routes:[]})

  1. var VueRouter = function VueRouter (options) {
  2. if ( options === void 0 ) options = {};
  3. this.app = null;
  4. this.apps = [];
  5. this.options = options;
  6. this.beforeHooks = [];
  7. this.resolveHooks = [];
  8. this.afterHooks = [];
  9. //调用createRouteMap方法,遍历routes,执行addRouteRecord(递归)生成的record对象存入
  10. //pathList pathMap nameMap。
  11. //createRouteMap最后 return { pathList: pathList, pathMap: pathMap, nameMap: nameMap }
  12. this.matcher = createMatcher(options.routes || [], this);
  13. var mode = options.mode || 'hash';
  14. //如果当前环境不支持history模式,强制切换到hash模式
  15. this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false;
  16. if (this.fallback) {
  17. mode = 'hash';
  18. }
  19. //如果不是浏览器环境,切换到abstract模式
  20. if (!inBrowser) {
  21. mode = 'abstract';
  22. }
  23. this.mode = mode;
  24. //根据mode值创建不同的实例,生成不同的history对象
  25. switch (mode) {
  26. case 'history':
  27. this.history = new HTML5History(this, options.base);
  28. break
  29. case 'hash':
  30. this.history = new HashHistory(this, options.base, this.fallback);
  31. break
  32. case 'abstract':
  33. this.history = new AbstractHistory(this, options.base);
  34. break
  35. default:
  36. {
  37. assert(false, ("invalid mode: " + mode));
  38. }
  39. }
  40. };

所以构造函数中做了两件事:

  1. 1》对routes数组进行匹配处理;
  2. 2》根据mode,创建不同的实例, HTML5History HashHistory, AbstractHistory

第二步: VueRouter.prototype.init初始化, 主要是根据this.history的原型类型,执行对应的transitionTo方法。

例如:项目中用的hash模式,那么在VueRouter构造方法中会创建出HashHistory实例,init方法中执行HashHistory的transitionTo方法

  1. VueRouter.prototype.init = function init (app /* Vue component instance */) {
  2. var this$1 = this;
  3. "development" !== 'production' && assert(
  4. install.installed,
  5. "not installed. Make sure to call `Vue.use(VueRouter)` " +
  6. "before creating root instance."
  7. );
  8. //app 当前vue
  9. this.apps.push(app);
  10. app.$once('hook:destroyed', function () {
  11. // 监听到destroyed钩子时,从apps数组删除该vue实例
  12. var index = this$1.apps.indexOf(app);
  13. if (index > -1) { this$1.apps.splice(index, 1); }
  14. if (this$1.app === app) { this$1.app = this$1.apps[0] || null; }
  15. });
  16. if (this.app) {
  17. return
  18. }
  19. //this.app 为当前vue
  20. this.app = app;
  21. var history = this.history;
  22. //根据history的原型类型,自行对应的transitionTo方法
  23. if (history instanceof HTML5History) {
  24. history.transitionTo(history.getCurrentLocation());
  25. } else if (history instanceof HashHistory) {
  26. var setupHashListener = function () {
  27. history.setupListeners();
  28. };
  29. history.transitionTo(
  30. history.getCurrentLocation(),
  31. setupHashListener,
  32. setupHashListener
  33. );
  34. }
  35. history.listen(function (route) {
  36. this$1.apps.forEach(function (app) {
  37. app._route = route;
  38. });
  39. });
  40. };

-—————-以hash模式 HashHistory实例为例——————-

HashHistory构造方法:HashHistory继承自History

  1. // 在VueRouter构造函数中,this.history = new HashHistory(this, options.base, this.fallback);
  2. //options.base指的是new VueRouter时传入的base属性
  3. var HashHistory = /*@__PURE__*/(function (History$$1) {
  4. //router -> VueRouter
  5. function HashHistory (router, base, fallback) {
  6. History$$1.call(this, router, base);
  7. // check history fallback deeplinking
  8. if (fallback && checkFallback(this.base)) {
  9. return
  10. }
  11. ensureSlash();
  12. }
  13. if ( History$$1 ) HashHistory.__proto__ = History$$1;
  14. HashHistory.prototype = Object.create( History$$1 && History$$1.prototype );
  15. HashHistory.prototype.constructor = HashHistory;
  16. ...
  17. return HashHistory;
  18. }(History));

在上面的HashHistory自执行方法中,指定了HashHistory继承自History,并指定了HashHistory一些原型方法:

1.HashHistory.prototype.setupListeners

  1. //是Android 2.或者Android 4.0且是Mobile Safari,不是Chorme, Windows Phone的返回false,否则返回true
  2. var supportsPushState = inBrowser && (function () {
  3. var ua = window.navigator.userAgent;
  4. if (
  5. (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
  6. ua.indexOf('Mobile Safari') !== -1 &&
  7. ua.indexOf('Chrome') === -1 &&
  8. ua.indexOf('Windows Phone') === -1
  9. ) {
  10. return false
  11. }
  12. return window.history && 'pushState' in window.history
  13. })();
  • 获取用户定义的scrollBehavior
  • 给window添加事件监听addEventListener, 如果supportsPushState,支持pushState,监听popstate, 否则监听hashchange;
  • 监听回调里,调用实例的transitionTo

    window.addEventListener(

    1. supportsPushState ? 'popstate' : 'hashchange',
    2. function () {
    3. var current = this$1.current;
    4. if (!ensureSlash()) {
    5. return
    6. }
    7. this$1.transitionTo(getHash(), function (route) {
    8. if (supportsScroll) {
    9. handleScroll(this$1.router, route, current, true);
    10. }
    11. if (!supportsPushState) {
    12. replaceHash(route.fullPath);
    13. }
    14. });
    15. }
    16. );

注释: getHash() 做了以下工作: 获取hash值和查询参数

  1. 1.获取window.location.href
  2. 2. 获取‘\#’的索引值index,并截取index+1后的内容
  3. 3. 从截取的内容里获取‘?’查询参数的开始索引 seatchIndex
  4. 3.1如果没有查询参数, 在获取一次‘\#’索引,确保获取了所有的hash值,并进行decodeURI编码,并返回
  5. 3.2如果有查询参数,把hash和查询参数一起decodeURI 并返回

2.HashHistory.prototype.push(location, onComplete, onAbort)

this.transitionTo(), 跳转到指定路由, 回调用执行pushHash, handleScroll, onComplete

3.HashHistory.prototype.replace

this.transitionTo(), 跳转到指定路由, 回调用执行replaceHash, handleScroll, onComplete

注释pushHash(path)做了以下工作: 如果supportsPushState, 执行pushState(完整url); 否则window.location.hash= path

  1. **replaceHash(path)**做了以下工作: 如果supportsPushState,执行replaceState(getUrl(path));否则window.location.replace(getUrl(path));
  2. **pushState(url replace) **: 调用window.history方法,如果replace传入truehistory.replaceState, 否则history.pushState

4.HashHistory.prototype.go(n)

window.history.go(n);

5.HashHistory.prototype.ensureURL(push)

  1. if (getHash() !== current) {
  2. push ? pushHash(current) : replaceHash(current);
  3. }

6.HashHistory.prototype.getCurrentLocation

  1. return getHash()

-—————————————————————————————————————————————————————————————————

综上:

在hashHistory的原型方法,setupListeners(), push(location, onComplete, onAbort), replace(location, onComplete, onAbort),中都调用了this.transitionTo(),而这个方法是在History的原型上定义的。

———————————下面先分析一下History做了什么———————————-

  1. //new HashHistory()时 传入的是router-》 当前VueRouter实例, base 实例初始时传入的base属性
  2. var History = function History (router, base) {
  3. this.router = router; //当前VueRouter实例
  4. this.base = normalizeBase(base);
  5. // start with a route object that stands for "nowhere"
  6. this.current = START; // {path: '/'}
  7. this.pending = null;
  8. this.ready = false;
  9. this.readyCbs = [];
  10. this.readyErrorCbs = [];
  11. this.errorCbs = [];
  12. };

最重要的方法,通用的transitionTo 方法

  1. //transitionTo 方法
  2. History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) {
  3. var this$1 = this;
  4. //重点1
  5. //location => 当前要push或者replace的路由
  6. // this.current => {path: '/'}
  7. var route = this.router.match(location, this.current);
  8. //调用this.confirmTransition方法,完成路由跳转, 重点2
  9. this.confirmTransition(route, function () {
  10. this$1.updateRoute(route);
  11. onComplete && onComplete(route);
  12. this$1.ensureURL();
  13. // fire ready cbs once
  14. if (!this$1.ready) {
  15. this$1.ready = true;
  16. //执行回调
  17. this$1.readyCbs.forEach(function (cb) { cb(route); });
  18. }
  19. }, function (err) {
  20. if (onAbort) {
  21. onAbort(err);
  22. }
  23. if (err && !this$1.ready) {
  24. this$1.ready = true;
  25. this$1.readyErrorCbs.forEach(function (cb) { cb(err); });
  26. }
  27. });
  28. };

重点1:

var route = this.router.match(location, this.current);

调用VueRouter实例的match方法,最终调用了createMatcher(options.routes || [], this).match(raw, current, redirectedFrom);
createMatcher方法,返回一个对象,包含match方法属性和addRoutes属性

  1. function createMatcher (
  2. routes,
  3. router
  4. ) {
  5. var ref = createRouteMap(routes);
  6. var pathList = ref.pathList;
  7. var pathMap = ref.pathMap;
  8. var nameMap = ref.nameMap;
  9. //匹配路由,并创建路由
  10. function match (
  11. raw,
  12. currentRoute,
  13. redirectedFrom
  14. ) {
  15. var location = normalizeLocation(raw, currentRoute, false, router);
  16. var name = location.name;
  17. if(name){
  18. var record = nameMap[name] || null ;
  19. ...
  20. } else {
  21. record = null
  22. }
  23. return _createRoute(record , location)
  24. }
  25. //重定向或者创建route
  26. function _createRoute (
  27. record,
  28. location,
  29. redirectedFrom
  30. ) {
  31. if (record && record.redirect) {
  32. return redirect(record, redirectedFrom || location)
  33. }
  34. if (record && record.matchAs) {
  35. return alias(record, location, record.matchAs)
  36. }
  37. return createRoute(record, location, redirectedFrom, router)
  38. }
  39. function addRoutes (routes) {
  40. createRouteMap(routes, pathList, pathMap, nameMap);
  41. }
  42. ...
  43. return {
  44. match: match,
  45. addRoutes: addRoutes
  46. }
  47. }

match方法,经过各种处理,最终返回的是createRoute()方法的返回值,也就是最终的返回值就是当前要跳转路由的路由对象,也就是 route的结果, 如下代码所示:

  1. //createRoute方法返回值大致如下, 也就是match最终返回的结果格式
  2. createRoute() => return Object.freeze(
  3. {
  4. name: location.name || (record && record.name),
  5. meta: (record && record.meta) || {},
  6. path: location.path || '/',
  7. hash: location.hash || '',
  8. query: query,
  9. params: location.params || {},
  10. fullPath: getFullPath(location, stringifyQuery$$1),
  11. matched: record ? formatMatch(record) : []
  12. }
  13. )

重点2:

route就是上面的当前路由对象

History.prototype.confirmTransition(route, onComplete, onAbort)接受三个参数, 当前route, 完成的回调, 终止的回调

  1. History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {
  2. var this$1 = this;
  3. var current = this.current;
  4. //终止路由
  5. var abort = function (err) {
  6. ...
  7. onAbort && onAbort(err);
  8. };
  9. //相同的路由,不跳转
  10. if (
  11. isSameRoute(route, current) &&
  12. route.matched.length === current.matched.length
  13. ) {
  14. this.ensureURL();
  15. return abort()
  16. }
  17. //处理路由队列,获取当前需要更新的路由,失效的路由,和要激活的路由
  18. var ref = resolveQueue(this.current.matched, route.matched);
  19. var updated = ref.updated;
  20. var deactivated = ref.deactivated;
  21. var activated = ref.activated;
  22. var queue = [].concat(
  23. extractLeaveGuards(deactivated),
  24. this.router.beforeHooks,
  25. extractUpdateHooks(updated),
  26. activated.map(function (m) { return m.beforeEnter; }),
  27. resolveAsyncComponents(activated)
  28. );
  29. this.pending = route;
  30. var iterator = function (hook, next) {
  31. if (this$1.pending !== route) {
  32. return abort()
  33. }
  34. try {
  35. hook(route, current, function (to) {
  36. if (to === false || isError(to)) {
  37. // next(false) -> abort navigation, ensure current URL
  38. this$1.ensureURL(true);
  39. abort(to);
  40. } else if (
  41. typeof to === 'string' ||
  42. (typeof to === 'object' && (
  43. typeof to.path === 'string' ||
  44. typeof to.name === 'string'
  45. ))
  46. ) {
  47. // next('/') or next({ path: '/' }) -> redirect
  48. abort();
  49. //路由跳转 , 调用实例的replace方法或者push方法, 更改url地址
  50. if (typeof to === 'object' && to.replace) {
  51. this$1.replace(to);
  52. } else {
  53. this$1.push(to);
  54. }
  55. } else {
  56. //执行路由的next方法
  57. next(to);
  58. }
  59. });
  60. } catch (e) {
  61. abort(e);
  62. }
  63. };
  64. //执行路由钩子和组件的激活及失活操作
  65. runQueue(queue, iterator, function () {
  66. var postEnterCbs = [];
  67. var isValid = function () { return this$1.current === route; };
  68. // wait until async components are resolved before
  69. // extracting in-component enter guards
  70. var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
  71. var queue = enterGuards.concat(this$1.router.resolveHooks);
  72. runQueue(queue, iterator, function () {
  73. if (this$1.pending !== route) {
  74. return abort()
  75. }
  76. this$1.pending = null;
  77. onComplete(route);
  78. if (this$1.router.app) {
  79. //如果指定了app, 执行app的$nextTick方法
  80. this$1.router.app.$nextTick(function () {
  81. postEnterCbs.forEach(function (cb) { cb(); });
  82. });
  83. }
  84. });
  85. });
  86. };

所以 confirmTransition方法主要做事情是:

通过resolveQueue方法,获取要更新的路由,失活的组件,要激活的组件;

完成url跳转(replace或者push方法)

url改变完成后,执行组件的更新显示(如果组件有指定的vue实例,执行vue实例的$nextTick方法)

总结:

1. 调用VueRouter.install方法,给vue安装路由

2. 安装方法install中,主要做了三件事:

2.1. 给vue的beforeCreate和destroyed钩子中新增关于路由的操作

2.2. 给vue实例定义$router(当前路由实例new VueRouter) $route(当前路由对象)属性

3. beforeCreate钩子中,获取vue实例参数中配置的router,即new VueRouter实例,执行实例的init方法;

4. new VueRouter时, 根据传入的routes创建this.Matcher用于匹配路由,根据传入的mode等参数,新增对应的实例(HTML5History, HashHistory, AbstractHistory)赋值给this.history

5.VueRouter.prototype.init方法中,主要做了两件事,

5.1 根据this.history的原型类型,分别执行对应的transitionTo方法,其中HashHistory实例需要先执行setupListeners()方法

5.2 调用history.listen()方法,获取当前route路由对象,this.$route的值

6. HTML5History, HashHistory, AbstractHistory实例都继承自History,transitionTo定义在History的原型上。

HTML5History监听了window对象的popstate事件

HashHistory监听了window对象的hashchange事件或者popstate

7. transitionTo中先获取当前路由对象,然后调用confirmTransition

8. confirmTransition主要获取当前要更新的组件,失活的组件,重新激活的组件;通过调用继承自History的实例的replace或push方法完成 url的改变; 执行路由的导航钩子,并进行组件的显示隐藏更新操作。

#

发表评论

表情:
评论列表 (有 0 条评论,434人围观)

还没有评论,来说两句吧...

相关阅读

    相关 HashMap分析()

    之前有写到ArrayList的源码分析,欢迎大家点开我的头像查看 对于HashMap这个类大家一定不陌生,想必多多少少用过或者了解过,今天我来和大家谈谈HashMap的源码,