vue-router源碼閱讀學習

如同分析vuex源碼咱們首先經過一個簡單例子進行了解vue-router是如何使用的,而後在分析在源碼中是如何實現的vue

示例

下面示例來自於example/basica/app.jsnode

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

const router = new VueRouter({
  mode: 'history',
  base: __dirname,
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar }
  ]
})

new Vue({
  router,
  template: `
    <div id="app">
      <h1>Basic</h1>
      <ul>
        <li><router-link to="/">/</router-link></li>
        <li><router-link to="/foo">/foo</router-link></li>
        <li><router-link to="/bar">/bar</router-link></li>
        <router-link tag="li" to="/bar" :event="['mousedown', 'touchstart']">
          <a>/bar</a>
        </router-link>
      </ul>
      <router-view class="view"></router-view>
    </div>
  `
}).$mount('#app')

複製代碼

首先調用Vue.use(VueRouter),Vue.use()方法是Vue用來進行插件安裝的方法,這裏主要用來安裝VueRouter。而後實例化了VueRouter,咱們來看看VueRouter這個構造函數到底作了什麼。 從源碼入口文件src/index.js開始看vue-router

import type { Matcher } from './create-matcher'

export default class VueRouter {

  constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)

    let mode = options.mode || 'hash'
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode

    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:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }

  init (app: any /* Vue component instance */) {

    this.apps.push(app)

    // main app already initialized.
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history

    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }

    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }

  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]
      })
    }))
  }

}


複製代碼

代碼一步步看,先從constructor函數的實現,首先進行初始化咱們來看看這些初始化條件分別表明的是什麼vuex

  • this.app表示當前Vue實例
  • this.apps表示全部app組件
  • this.options表示傳入的VueRouter的選項
  • this.resolveHooks表示resolve鉤子回調函數的數組,resolve用於解析目標位置
  • this.matcher建立匹配函數

代碼中有createMatcher()函數,來看看他的實現數組

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

   function match (
    raw,
    currentRoute,
    redirectedFrom
  ) {
    var location = normalizeLocation(raw, currentRoute, false, router);
    var name = location.name;
    // 命名路由處理
    if (name) {
      // nameMap[name]的路由記錄
      var record = nameMap[name];
      ...
    	location.path = fillParams(record.path, location.params, ("named route \"" + name + "\""));
    	// _createRoute用於建立路由
    	return _createRoute(record, location, redirectedFrom)
    } else if (location.path) {
      // 普通路由處理
    }
    // no match
    return _createRoute(null, location)
  }

  return {
    match: match,
    addRoutes: addRoutes
  }
}
複製代碼

createMatcher()有兩個參數routes表示建立VueRouter傳入的routes配置信息,router表示VueRouter實例。createMatcher()的做用就是傳入的routes經過createRouteMap建立對應的map,和一個建立map的方法。 咱們先來看看createRouteMap()方法的定義瀏覽器

function createRouteMap (
  routes,
  oldPathList,
  oldPathMap,
  oldNameMap
) {
  // 用於控制匹配優先級
  var pathList = oldPathList || [];
  // name 路由 map
  var pathMap = oldPathMap || Object.create(null);
  // name 路由 map
  var nameMap = oldNameMap || Object.create(null);
  // 遍歷路由配置對象增長路由記錄
  routes.forEach(function (route) {
    addRouteRecord(pathList, pathMap, nameMap, route);
  });

  // 確保通配符老是在pathList的最後,保證最後匹配
  for (var i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0]);
      l--;
      i--;
    }
  }

  return {
    pathList: pathList,
    pathMap: pathMap,
    nameMap: nameMap
  }
}
複製代碼

createRouteMap()有4個參數:routes表明的配置信息,oldPathList包含全部路徑的數組用於匹配優先級,oldNameMap表示name map,oldPathMap表示path map。createRouteMap就是更新pathList,nameMap和pathMap。nameMap到底表明的是什麼呢?它是包含路由記錄的一個對象,每一個屬性值名是每一個記錄的path屬性值,屬性值就是具備這個path屬性值的路由記錄。這兒有一個叫路由記錄的東西,這是什麼意思呢?路由記錄就是 routes 配置數組中的對象副本(還有在 children 數組),路由記錄都是包含在matched屬性中例如緩存

const router = new VueRouter({
  routes: [
    // 下面的對象就是 route record
    { path: '/foo', component: Foo,
      children: [
        // 這也是個 route record
        { path: 'bar', component: Bar }
      ]
    }
  ]
})
複製代碼

在上面代碼中用一段代碼用於給每一個route添加路由記錄,那麼路由記錄的實現是如何的呢,下面是addRouteReord()的實現bash

function addRouteRecord (
  pathList,
  pathMap,
  nameMap,
  route,
  parent,
  matchAs
) {
  var path = route.path;
  var name = route.name;
  var normalizedPath = normalizePath(
    path,
    parent
  );

  var record = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    instances: {},
    name: name,
    parent: parent,
    matchAs: matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props: route.props == null
      ? {}
      : route.components
        ? route.props
        : { default: route.props }
  };

  if (route.children) {
    route.children.forEach(function (child) {
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs);
    });
  }

  if (route.alias !== undefined) {
    // 若是有別名的狀況
  }

  if (!pathMap[record.path]) {
    pathList.push(record.path);
    pathMap[record.path] = record;
  }
}
複製代碼

addRouteRecord()這個函數的參數我都懶得說什麼意思了,新增的parent也表示路由記錄,首先獲取path,name。而後經過normalizePath()規範格式,而後就是record這個對象的創建,而後遍歷routes的子元素添加路由記錄若是有別名的狀況還須要考慮別名的狀況而後更新path Map。服務器

History

咱們在回到VueRouter的構造函數中,往下看是模式的選擇,一共這麼幾種模式一種history,hash和abstract三種。· 默認hash: 使用URL hash值做爲路由,支持全部瀏覽器 · history: 依賴HTML5 History API和服務器配置 · abstract:支持全部 JavaScript 運行環境,如 Node.js 服務器端。若是發現沒有瀏覽器的 API,路由會自動強制進入這個模式。 默認是hash,路由經過「#」隔開,可是若是工程中有錨連接或者路由中有hash值,原先的「#」就會對頁面跳轉產生影響;因此就須要使用history模式。 在應用中咱們經常使用的基本都是history模式,下面咱們來看看HashHistory的構造函數app

var History = function History (router, base) {
  this.router = router;
  this.base = normalizeBase(base);
  this.current = START;
  this.pending = null;
  this.ready = false;
  this.readyCbs = [];
  this.readyErrorCbs = [];
  this.errorCbs = [];
};
複製代碼

由於hash和history有一些相同的地方,因此HashHistory會在History構造函數上進行擴展下面是各個屬性所表明的意義:

  • this.router表示VueRouter實例
  • this.base表示應用的基路徑。例如,若是整個單頁應用服務在 /app/ 下,而後 base 就應該設爲 "/app/"。normalizeBase()用於格式化base
  • this.current開始時的route,route使用createRoute()建立
  • this.pending表示進行時的route
  • this.ready表示準備狀態
  • this.readyCbs表示準備回調函數

creatRoute()在文件src/util/route.js中,下面是他的實現

function createRoute (
  record,
  location,
  redirectedFrom,
  router
) {
  var stringifyQuery$$1 = router && router.options.stringifyQuery;

  var query = location.query || {};
  try {
    query = clone(query);
  } catch (e) {}

  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) : []
  };
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery$$1);
  }
  return Object.freeze(route)
}
複製代碼

createRoute有三個參數,record表示路由記錄,location,redirectedFrom表示url地址信息對象,router表示VueRouter實例對象。經過傳入的參數,返回一個凍結的route對象,route對象裏邊包含了一些有關location的屬性。History包含了一些基本的方法,例如比較重要的方法有transitionTo(),下面是transitionTo()的具體實現。

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

  var route = this.router.match(location, this.current);
  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); });
    }
  });
};
複製代碼

首先match獲得匹配的route對象,route對象在以前已經提到過。而後使用confirmTransition()確認過渡,更新route,ensureURL()的做用就是更新URL。若是ready爲false,更改ready的值,而後對readyCbs數組進行遍歷回調。下面來看看HTML5History的構造函數

var HTML5History = (function (History$$1) {
  function HTML5History (router, base) {
    var this$1 = this;

    History$$1.call(this, router, base);

    var initLocation = getLocation(this.base);
    window.addEventListener('popstate', function (e) {
      var current = this$1.current;
      var location = getLocation(this$1.base);
      if (this$1.current === START && location === initLocation) {
        return
      }
    });
  }

  if ( History$$1 ) HTML5History.__proto__ = History$$1;
  HTML5History.prototype = Object.create( History$$1 && History$$1.prototype );
  HTML5History.prototype.constructor = HTML5History;


  HTML5History.prototype.push = function push (location, onComplete, onAbort) {
    var this$1 = this;

    var ref = this;
    var fromRoute = ref.current;
    this.transitionTo(location, function (route) {
      pushState(cleanPath(this$1.base + route.fullPath));
      handleScroll(this$1.router, route, fromRoute, false);
      onComplete && onComplete(route);
    }, onAbort);
  };

  HTML5History.prototype.replace = function replace (location, onComplete, onAbort) {
    var this$1 = this;

    var ref = this;
    var fromRoute = ref.current;
    this.transitionTo(location, function (route) {
      replaceState(cleanPath(this$1.base + route.fullPath));
      handleScroll(this$1.router, route, fromRoute, false);
      onComplete && onComplete(route);
    }, onAbort);
  };


  return HTML5History;
}(History))
複製代碼

在HTML5History()中代碼屢次用到了getLocation()那咱們來看看他的具體實現吧

function getLocation (base) {
  var path = window.location.pathname;
  if (base && path.indexOf(base) === 0) {
    path = path.slice(base.length);
  }
  return (path || '/') + window.location.search + window.location.hash
}
複製代碼

用一個簡單的地址來解釋代碼中各個部分的含義。例如http://example.com:1234/test/test.htm#part2?a=123,window.location.pathname=>/test/test.htm=>?a=123,window.location.hash=>#part2。 把咱們繼續回到HTML5History()中,首先繼承history構造函數。而後監聽popstate事件。當活動記錄條目更改時,將觸發popstate事件。須要注意的是調用history.pushState()或history.replaceState()不會觸發popstate事件。咱們來看看HTML5History的push方法。location表示url信息,onComplete表示成功後的回調函數,onAbort表示失敗的回調函數。首先獲取current屬性值,replaceState和pushState用於更新url,而後處理滾動。模式的選擇就大概講完了,咱們回到入口文件,看看init()方法,app表明的是Vue的實例,現將app存入this.apps中,若是this.app已經存在就返回,若是不是就賦值。this.history是三種的實例對象,而後分狀況進行transtionTo()操做,history方法就是給history.cb賦值穿進去的回調函數。 下面看getMatchedComponents(),惟一須要注意的就是咱們屢次提到的route.matched是路由記錄的數據,最終返回的是每一個路由記錄的components屬性值的值。

Router-View

最後講講router-view

var View = {
  name: 'router-view',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render: function render (_, ref) {
    var props = ref.props;
    var children = ref.children;
    var parent = ref.parent;
    var data = ref.data;
    // 解決嵌套深度問題
    data.routerView = true;

    var h = parent.$createElement;
    var name = props.name;
    // route
    var route = parent.$route;
    // 緩存
    var cache = parent._routerViewCache || (parent._routerViewCache = {});

    // 組件的嵌套深度
    var depth = 0;
    // 用於設置class值
    var 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;

    if (inactive) {
      return h(cache[name], data, children)
    }

    var matched = route.matched[depth];
    if (!matched) {
      cache[name] = null;
      return h()
    }

    var component = cache[name] = matched.components[name];

    data.registerRouteInstance = function (vm, val) {
      // val could be undefined for unregistration
      var current = matched.instances[name];
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val;
      }
    }

    ;(data.hook || (data.hook = {})).prepatch = function (_, vnode) {
      matched.instances[name] = vnode.componentInstance;
    };
    var propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]);
    if (propsToPass) {
      propsToPass = data.props = extend({}, propsToPass);
      var attrs = data.attrs = data.attrs || {};
      for (var key in propsToPass) {
        if (!component.props || !(key in component.props)) {
          attrs[key] = propsToPass[key];
          delete propsToPass[key];
        }
      }
    }

    return h(component, data, children)
  }
};
複製代碼

router-view比較簡單,functional爲true使組件無狀態 (沒有 data ) 和無實例 (沒有 this 上下文)。他們用一個簡單的 render 函數返回虛擬節點使他們更容易渲染。props表示接受屬性,下面來看看render函數,首先獲取數據,而後緩存,_inactive用於處理keep-alive狀況,獲取路由記錄,註冊Route實例,h()用於渲染。很簡單我也懶得一一再說。

小結

文章由入口文件入手,推導出本篇文章。因爲篇幅限制,代碼進行了必定的省略,將一些比較簡單的代碼進行了省略。

相關文章
相關標籤/搜索