vue-router源码浏览进修

犹如剖析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()用于衬着。很简朴我也懒得逐一再说。

小结

文章由进口文件入手,推导出本篇文章。因为篇幅限定,代码举行了肯定的省略,将一些比较简朴的代码举行了省略。

    原文作者:云中歌
    原文地址: https://segmentfault.com/a/1190000013177857
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞