vue-router原始碼閱讀學習

B_Cornelius發表於2018-02-06

如同分析vuex原始碼我們首先通過一個簡單例子進行了解vue-router是如何使用的,然後在分析在原始碼中是如何實現的

示例

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

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開始看

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函式的實現,首先進行初始化我們來看看這些初始化條件分別代表的是什麼

  • 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()的實現

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的建構函式

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()用於渲染。很簡單我也懶得一一再說。

小結

文章由入口檔案入手,推匯出本篇文章。由於篇幅限制,程式碼進行了一定的省略,將一些比較簡單的程式碼進行了省略。

相關文章