Vue源碼解析(六)-vue-router

先上一段簡單的demo,本文根據此demo進行解析前端

Vue.use(VueRouter)
const router = new VueRouter({
    routes: [
        { path: '/home', component: {template: '<div>home</div>'}}
    ]
})
new Vue({
    'el':'#app',
    router,
    template: `
    <div id="app">
      <h1>Basic</h1><router-view class="view"></router-view>
    </div>
    `
})

vue源碼解析(五)中介紹過,Vue.use(VueRouter)其實主要調用了VueRouter.install(Vue)方法vue

function install (Vue) {
  //掛載全局的鉤子函數到Vue,vue對象初始化會調用下面的函數
  Vue.mixin({
    beforeCreate: function beforeCreate () {
      if (isDef(this.$options.router)) {
        // _routerRoot爲當前vue對象
        this._routerRoot = this;
        // _router爲new Vue傳入的VueRouter對象
        this._router = this.$options.router;
        //調用 VueRouter.protoype.init,後面介紹
        this._router.init(this);
        // 設置響應式的_route,this._router.history.current爲當前頁面的路由信息
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      }
    }
  });
  //全局註冊組件
  Vue.component('router-view', View);
  Vue.component('router-link', Link);
}
// 全局組件router-view的參數
var View = {
  name: 'router-view',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render: function render (_, ref){}
}

install方法主要是掛載鉤子函數和全局註冊組件,全局註冊的組件router-view的值以下,
clipboard.pngnode

再看一下router = new VueRouter的過程segmentfault

var VueRouter = function VueRouter (options) {
  this.options = options;
  //默認使用hash進行前端路由
  var mode = options.mode || 'hash';
  switch (mode) {
    case 'history':
      this.history = new HTML5History(this, options.base);
      break
    case 'hash':
      this.history = new HashHistory(this, options.base, this.fallback);
      break
  }
}
//  HashHistory 和 HTML5History都是繼承History
var HashHistory = (function (History$$1) {
  function HashHistory (router, base, fallback) {
    History$$1.call(this, router, base);
  }

  if ( History$$1 ) HashHistory.__proto__ = History$$1;
  HashHistory.prototype = Object.create( History$$1 && History$$1.prototype );
  HashHistory.prototype.constructor = HashHistory;
  return HashHistory;
}(History));

var History = function History (router, base) {
  // VueRouter實例對象
  this.router = router;
  //base路徑
  this.base = normalizeBase(base);
  //當前路由信息,此時是一個空值
  this.current = START;
};

new Vue的過程當中會觸發掛載的beforeCreate函數,主要是調用了this._router.init(this);
爲了更清晰的解析整個流程,假定咱們如今訪問的頁面路徑是/home,而且是hash的方式進行路由瀏覽器

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

  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation());
  } else if (history instanceof HashHistory) {
    var setupHashListener = function () {
      //監聽瀏覽器地址的變動,並調用transitionTo「跳轉」到新的路由頁面
      history.setupListeners();
    };
    //初始化時調用一次transitionTo,根據當前瀏覽器地址欄裏的path(/home)來激活對應的路由
    history.transitionTo(
      // 初始化值爲「/home」
      history.getCurrentLocation(),
      setupHashListener    
    );
  }
};
HashHistory.prototype.setupListeners = function setupListeners () {
   //監聽瀏覽器地址的變動
   window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', function () {
      //實現頁面內容的變動,getHash()爲變動後的hash路徑
      this$1.transitionTo(getHash())
   }
}
//將頁面轉換到當前真實的路由
History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) {
  var this$1 = this;
  // 根據location("/home")獲得route對象{name: undefined, meta: {…}, path: "/home", hash: "", query: {…}, …}
  var route = this.router.match(location, this.current);
  //confirmTransition實現比較複雜,本文不作介紹,主要會執行下面的回調函數
  this.confirmTransition(route, function () {
    //將histoty.current值更新爲route
    this$1.updateRoute(route);
    //執行onComplete(setupHashListener)
    onComplete && onComplete(route);
    //更新瀏覽器url地址
    this$1.ensureURL();
  }
};

function match (){
  遍歷路由配置(本文只有一項配置{ path: '/home', component: {template: '<div>home</div>'}})
  for (var i = 0; i < pathList.length; i++) {
    var path = pathList[i];
    var record$1 = pathMap[path];
    //判斷當前路徑是否有匹配的路由配置
    if (matchRoute(record$1.regex, location.path, location.params)) {
      return _createRoute(record$1, location, redirectedFrom)
    }
  }
  // no match
  return _createRoute(null, location)
}

function createRoute (record){
  var route = {
    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) : []
  };
  return Object.freeze(route)
}

根據上面的代碼邏輯能夠分析得出,vue對象初始化時會掛載屬性vm._router(記錄了整個應用的路由配置信息)和vm._route(記錄了當前的路由信息)。vm._route是響應式的,當瀏覽器路由改變時,vm._route的值也會相應的改變
vm._route的做用是清楚了,但頁面內容的變化是怎麼實現的呢?下面再介紹下router-view的做用。
Vue源碼解析(四)-components組件介紹過,vue初始化時根據template函數生成render函數,本文render函數會調用vm._c('router-view'),_createElement判斷router-view是註冊過的組件,所以以組件的方式生成vnode,可是router-view生成vnode的過程與Vue源碼解析(四)中的方法又有區別app

function _createElement(){
    //本例tag=‘router-view’,‘router-view’在components屬性中註冊過,所以以組件的方式生成vnode
    if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      //Ctor是router-view的構造函數VueComponent(Vue.component('router-view', View)註冊)
      vnode = createComponent(Ctor, data, context, children, tag);
    }
}

function createComponent (Ctor){
    //Ctor 此時已是構造函數 , 不須要再調用Vue.extend生成
    var baseCtor = context.$options._base;
    if (isObject(Ctor)) {
      Ctor = baseCtor.extend(Ctor);
    }
    // router-view是functional component(見上文圖中view的option的值),與用戶自定義的component的vnode生成方法有區別
    if (isTrue(Ctor.options.functional)) {
      return createFunctionalComponent(Ctor, propsData, data, context, children)
    }
    //用戶自定義component的vnode構造方法
    var vnode = new VNode(
      ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
      data, undefined, undefined, undefined, context,
      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children:         children } );
}

function createFunctionalComponent (Ctor){
   var options = Ctor.options;
   //主要是調用View.render方法,前文提到過
   var vnode = options.render.call(null, renderContext._c, renderContext);
   return vnode;
}

var View = {
  name: 'router-view',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render: function render (_, ref) {
    //Vue實例化對象vm
    var parent = ref.parent;
    // vm._route
    var route = parent.$route;

    var depth = 0;
    //上文提到的createRoute中生成的路由匹配信息
    var matched = route.matched[depth];
    
    // _createElement方法
    var h = parent.$createElement;
    // render empty node if no matched route
    if (!matched) {
      return h()
    }
    // 本文component爲{template: "<div>home</div>", _Ctor: {…}, inject: {…}}
    var component = matched.components[name];
    
    //從新調用_createElement,此次是以常規方式生成vnode,後續vnode將渲染成template中的內容
    return h(component, data, children)
  }
};
相關文章
相關標籤/搜索