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

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

function install (Vue) {
//如果已经挂载了,跳过
  if (install.installed && _Vue === Vue) { return }
//如果未挂载过, installed属性改为true
  install.installed = true;
//获取当前vue
  _Vue = Vue;

  var isDef = function (v) { return v !== undefined; };

  var registerInstance = function (vm, callVal) {
    var i = vm.$options._parentVnode;
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal);
    }
  };
//在vue的beforeCreate钩子和destroyed钩子中新增关于路由的处理
  Vue.mixin({
    beforeCreate: function beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this;
        this._router = this.$options.router; //new VueRouter()
        this._router.init(this); //如果Vue实例参数中,有指定的router属性,执行init初始化
//调用vue.util里的方法,通过get和set劫持this._route,router-view实时更新
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      }
      registerInstance(this, this);
    },
    destroyed: function destroyed () {
      registerInstance(this);
    }
  });
//给vue新增$router, $route属性,并指定get方法返回值,$route属性。于是就有了,this.$router和this.$route对象可以使用。
  Object.defineProperty(Vue.prototype, '$router', {
//this._routerRoot => this (即vue)
//this._router =>new VueRouter()
    get: function get () { return this._routerRoot._router } 
  });

  Object.defineProperty(Vue.prototype, '$route', {
    get: function get () { return this._routerRoot._route }
  });
//新增子组件RouterView, RouterLink
  Vue.component('RouterView', View);
  Vue.component('RouterLink', Link);

  var strats = Vue.config.optionMergeStrategies;
  // 默认一下组件内导航钩子跟created方法一样
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}

对于配置了路由的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:[]})



var VueRouter = function VueRouter (options) {
  if ( options === void 0 ) options = {};

  this.app = null;
  this.apps = [];
  this.options = options;
  this.beforeHooks = [];
  this.resolveHooks = [];
  this.afterHooks = [];
//调用createRouteMap方法,遍历routes,执行addRouteRecord(递归)生成的record对象存入
//pathList pathMap nameMap。

//createRouteMap最后 return { pathList: pathList, pathMap: pathMap, nameMap: nameMap }
  this.matcher = createMatcher(options.routes || [], this);

  var mode = options.mode || 'hash';
//如果当前环境不支持history模式,强制切换到hash模式
  this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false;
  if (this.fallback) {
    mode = 'hash';
  }
//如果不是浏览器环境,切换到abstract模式
  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:
      {
        assert(false, ("invalid mode: " + mode));
      }
  }
};

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

     1》对routes数组进行匹配处理;

            

     2》根据mode,创建不同的实例, HTML5History, HashHistory, AbstractHistory

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

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

VueRouter.prototype.init = function init (app /* Vue component instance */) {
    var this$1 = this;

  "development" !== 'production' && assert(
    install.installed,
    "not installed. Make sure to call `Vue.use(VueRouter)` " +
    "before creating root instance."
  );
    //app 当前vue
  this.apps.push(app);
  
  app.$once('hook:destroyed', function () {
    // 监听到destroyed钩子时,从apps数组删除该vue实例
    var index = this$1.apps.indexOf(app);
    if (index > -1) { this$1.apps.splice(index, 1); }
    if (this$1.app === app) { this$1.app = this$1.apps[0] || null; }
  });

  if (this.app) {
    return
  }
    //this.app 为当前vue
  this.app = app;

  var history = this.history;
//根据history的原型类型,自行对应的transitionTo方法
  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation());
  } else if (history instanceof HashHistory) {
    var setupHashListener = function () {
      history.setupListeners();
    };
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener,
      setupHashListener
    );
  }

  history.listen(function (route) {
    this$1.apps.forEach(function (app) {
      app._route = route;
    });
  });
};

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

HashHistory构造方法:HashHistory继承自History

// 在VueRouter构造函数中,this.history = new HashHistory(this, options.base, this.fallback); 

//options.base指的是new VueRouter时传入的base属性

var HashHistory = /*@__PURE__*/(function (History$$1) {
//router -> VueRouter
  function HashHistory (router, base, fallback) {
    History$$1.call(this, router, base);
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash();
  }
 if ( History$$1 ) HashHistory.__proto__ = History$$1;
  HashHistory.prototype = Object.create( History$$1 && History$$1.prototype );
  HashHistory.prototype.constructor = HashHistory;
  ...
  return HashHistory;
}(History));

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

1.HashHistory.prototype.setupListeners

//是Android 2.或者Android 4.0且是Mobile Safari,不是Chorme, Windows Phone的返回false,否则返回true
var supportsPushState = inBrowser && (function () {
  var ua = window.navigator.userAgent;

  if (
    (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
    ua.indexOf('Mobile Safari') !== -1 &&
    ua.indexOf('Chrome') === -1 &&
    ua.indexOf('Windows Phone') === -1
  ) {
    return false
  }

  return window.history && 'pushState' in window.history
})();
  • 获取用户定义的scrollBehavior
  • 给window添加事件监听addEventListener, 如果supportsPushState,支持pushState,监听popstate, 否则监听hashchange;
  • 监听回调里,调用实例的transitionTo
window.addEventListener(
      supportsPushState ? 'popstate' : 'hashchange',
      function () {
        var current = this$1.current;
        if (!ensureSlash()) {
          return
        }
        this$1.transitionTo(getHash(), function (route) {
          if (supportsScroll) {
            handleScroll(this$1.router, route, current, true);
          }
          if (!supportsPushState) {
            replaceHash(route.fullPath);
          }
        });
      }
    );

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

                1.获取window.location.href

                 2. 获取‘#’的索引值index,并截取index+1后的内容

                 3. 从截取的内容里获取‘?’查询参数的开始索引 seatchIndex

                              3.1如果没有查询参数, 在获取一次‘#’索引,确保获取了所有的hash值,并进行decodeURI编码,并返回

                               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

              replaceHash(path)做了以下工作: 如果supportsPushState,执行replaceState(getUrl(path));否则window.location.replace(getUrl(path));

              pushState(url replace) : 调用window.history方法,如果replace传入true,history.replaceState, 否则history.pushState

4.HashHistory.prototype.go(n)

window.history.go(n);

5.HashHistory.prototype.ensureURL(push)

if (getHash() !== current) {

    push ? pushHash(current) : replaceHash(current);

}

6.HashHistory.prototype.getCurrentLocation

     return getHash()

-----------------------------------------------------------------------------------------------------------------------------------

综上:

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

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

//new HashHistory()时 传入的是router-》 当前VueRouter实例, base 实例初始时传入的base属性

var History = function History (router, base) {
  this.router = router; //当前VueRouter实例
  this.base = normalizeBase(base);
  // start with a route object that stands for "nowhere"
  this.current = START;   // {path: '/'}
  this.pending = null;
  this.ready = false;
  this.readyCbs = [];
  this.readyErrorCbs = [];
  this.errorCbs = [];
};

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

//transitionTo 方法
History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) {
    var this$1 = this;
    //重点1 
    //location => 当前要push或者replace的路由 
    // this.current => {path: '/'}
    var route = this.router.match(location, this.current); 

//调用this.confirmTransition方法,完成路由跳转, 重点2
  this.confirmTransition(route, function () {
    this$1.updateRoute(route);
    onComplete && onComplete(route);
    this$1.ensureURL();

    // fire ready cbs once
    if (!this$1.ready) {
      this$1.ready = true;
//执行回调
      this$1.readyCbs.forEach(function (cb) { cb(route); });
    }
  }, function (err) {
    if (onAbort) {
      onAbort(err);
    }
    if (err && !this$1.ready) {
      this$1.ready = true;
      this$1.readyErrorCbs.forEach(function (cb) { cb(err); });
    }
  });
};

重点1: 

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

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

function createMatcher (
  routes,
  router
) {
  var ref = createRouteMap(routes);
  var pathList = ref.pathList;
  var pathMap = ref.pathMap;
  var nameMap = ref.nameMap;

//匹配路由,并创建路由
  function match (
    raw,
    currentRoute,
    redirectedFrom
  ) { 
    var location = normalizeLocation(raw, currentRoute, false, router);
    var name = location.name;
    if(name){
        var record = nameMap[name] || null ;
        ...
    } else {
       record = null
        }
    return  _createRoute(record , location)

    }

//重定向或者创建route
  function _createRoute (
    record,
    location,
    redirectedFrom
  ) {
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    return createRoute(record, location, redirectedFrom, router)
  }

 function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap);
  }
  ...
  return {
    match: match,
    addRoutes: addRoutes
  }
}

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


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

  

重点2:

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

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

History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {
    var this$1 = this;

  var current = this.current;
//终止路由
  var abort = function (err) {
    ...
    onAbort && onAbort(err);
  };
//相同的路由,不跳转
  if (
    isSameRoute(route, current) &&
    route.matched.length === current.matched.length
  ) {
    this.ensureURL();
    return abort()
  }
//处理路由队列,获取当前需要更新的路由,失效的路由,和要激活的路由
  var ref = resolveQueue(this.current.matched, route.matched);
    var updated = ref.updated;
    var deactivated = ref.deactivated;
    var activated = ref.activated;

  var queue = [].concat(
    extractLeaveGuards(deactivated),
    this.router.beforeHooks,
    extractUpdateHooks(updated),
    activated.map(function (m) { return m.beforeEnter; }),
    resolveAsyncComponents(activated)
  );

  this.pending = route;
  var iterator = function (hook, next) {
    if (this$1.pending !== route) {
      return abort()
    }
    try {
      hook(route, current, function (to) {
        if (to === false || isError(to)) {
          // next(false) -> abort navigation, ensure current URL
          this$1.ensureURL(true);
          abort(to);
        } else if (
          typeof to === 'string' ||
          (typeof to === 'object' && (
            typeof to.path === 'string' ||
            typeof to.name === 'string'
          ))
        ) {
          // next('/') or next({ path: '/' }) -> redirect
          abort();
//路由跳转 , 调用实例的replace方法或者push方法, 更改url地址
          if (typeof to === 'object' && to.replace) {
            this$1.replace(to);
          } else {
            this$1.push(to);
          }
        } else {
          //执行路由的next方法
          next(to);
        }
      });
    } catch (e) {
      abort(e);
    }
  };
 //执行路由钩子和组件的激活及失活操作
  runQueue(queue, iterator, function () {
    var postEnterCbs = [];
    var isValid = function () { return this$1.current === route; };
    // wait until async components are resolved before
    // extracting in-component enter guards
    var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
    var queue = enterGuards.concat(this$1.router.resolveHooks);
    runQueue(queue, iterator, function () {
      if (this$1.pending !== route) {
        return abort()
      }
      this$1.pending = null;
      onComplete(route);
      if (this$1.router.app) {
//如果指定了app, 执行app的$nextTick方法
        this$1.router.app.$nextTick(function () {
          postEnterCbs.forEach(function (cb) { cb(); });
        });
      }
    });
  });
};

所以 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(当前路由对象)属性

      2.3. 给vue新增子组件 RouterView和RouterLink

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的改变; 执行路由的导航钩子,并进行组件的显示隐藏更新操作。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Vue,VueRouter)