# Router 对象实例化

在使用vue-router时,首先会调用new VueRouter来实例化一个router对象,VueRouter的构造函数在项目的src/router.js文件中

# 构造函数

export default class {
  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}`);
        }
    }
  }
}

分析下构造函数,共处理了以下几个事情

  • 初始化对象的matcher属性
  • 根据不同场景确认对象model属性
  • 根据model不同,实例化了对象history属性

# matcher

createMatcher对应的代码在src/create-matcher.js内,先分析部分核心代码

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {

    const { pathList, pathMap, nameMap } = createRouteMap(routes)

    function match(){},
    function addRoute(){},
    function getRoutes(){},
    function addRoutes(){},
    return {
      match,
      addRoute,
      getRoutes,
      addRoutes
    }
}

可以看到,该方法内部调用了createRouteMap方法,然后返回了一个包含4个函数的对象。这里分析下createRouteMap这个方法

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>,
  parentRoute?: RouteRecord
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // the path list is used to control path matching priority
  const pathList: Array<string> = oldPathList || []
  // $flow-disable-line
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
  })

  // ensure wildcard routes are always at the end
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }

  return {
    pathList,
    pathMap,
    nameMap
  }
}

这里首先初始化了pathList pathMap nameMap这3个变量,然后循环传入的routes配置,调用addRouteRecord方法。

接着处理通配符*,通过循环pathList来确保*始终保持在数组的最后。逻辑也很简单,如果匹配到通配符,则先将该通配符移除数组,再添加到数组的最后一位。同时将前后2个指针,向前移动一位。这样即可确保通配置路由在数组的最后位置。最后在返回之前定义的3个变量。

接着看下addRouteRecord的实现

function addRouteRecord(
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string,
) {
  const { path, name } = route;

  const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {};
  const normalizedPath = normalizePath(
    path,
    parent,
    pathToRegexpOptions.strict,
  );

  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive;
  }

  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    alias: route.alias
      ? typeof route.alias === 'string'
        ? [route.alias]
        : route.alias
      : [],
    instances: {},
    enteredCbs: {},
    name,
    parent,
    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((child) => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined;
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs);
    });
  }

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

  if (route.alias !== undefined) {
    const aliases = Array.isArray(route.alias) ? route.alias : [route.alias];
    for (let i = 0; i < aliases.length; ++i) {
      const alias = aliases[i];
      const aliasRoute = {
        path: alias,
        children: route.children,
      };
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/', // matchAs
      );
    }
  }

  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record;
    }
  }
}

首先调用了normalizePath方法,得到规范化的path。核心就是添加parent path,处理空格,处理最后的/等,比较简单

接着定义变量record,这里看下compileRouteRegex方法,这个方法主要调用了path-to-regexp这个库来生成正则,不做具体分析。

接着循环children属性,递归的调用addRouteRecord方法,并传入当前环境中的record作为parent参数以及当前的pathList pathMap nameMap变量,这样就能把树形结构的配置打平成一个一维数组

接着判断当前pathMap有无当前环境下的record对象,如无,则添加到pathList pathMap中去

在接着处理路由的alias,就是循环alias,将alias作为path,循环调用addRouteRecord方法。注意,这里只传入的route对象只有pathchildren属性。这个的addRouteRecord方法传入了第6个参数matchAs,这个是用来做alias匹配的,后面会说到

最后将尝试将当前record添加到nameMap中去

至此,pathList pathMap nameMap就构建完成了。

# model

逻辑很简单,默认modelhash。如果用户传入的modelhistory,但当前不支持pushState且用户配置fallback成立,则回退为hash。若不在浏览器环境中model设置为abstract

# history

根据model的不同类型,实例化不同的路由对象,这里着重分析下HashHistory这一常用类型。相关代码位于src/history/hash.js

export class HashHistory extends History {
  constructor(router: Router, base: ?string, fallback: boolean) {
    super(router, base);
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return;
    }
    ensureSlash();
  }
}

这里暂前只分析构造函数相关内容。可以看到HashHistory首先是继承了History这个类,这个等下分析。

可以看见HashHistory的构造函数首先处理了historyfallback的情况。如果fallback为真,则调用checkFallback方法,这个方法核心就是,修改当前页面locationhash模式。

接着调用ensureSlash方法,这个方法确保location的结构是/#/,而不是/#

接着看下基类History的代码

export class History {
  constructor(router: Router, base: ?string) {
    this.router = router;
    this.base = normalizeBase(base);
    // start with a route object that stands for "nowhere"
    this.current = START;
    this.pending = null;
    this.ready = false;
    this.readyCbs = [];
    this.readyErrorCbs = [];
    this.errorCbs = [];
    this.listeners = [];
  }
}

这里主要分析下current属性,这是一个route对象,当前为根路由/对象,route对象的结构如下:

declare type Route = {
  path: string;
  name: ?string;
  hash: string;
  query: Dictionary<string>;
  params: Dictionary<string>;
  fullPath: string;
  matched: Array<RouteRecord>;
  redirectedFrom?: string;
  meta?: any;
};

当前current属性为

{
  "path": "/",
  "hash": "",
  "query": {},
  "params": {},
  "fullPath": "/",
  "matched": []
}

其余的之后分析

# 总结

  • VueRouter在实例化时,首先初始化了matcher属性,这是一个匹配器,包含了4个方法,同时在匹配器创建时,完成了对routes的解析,生成了pathList pathMap nameMap3个私有变量,用来简化路由匹配
  • 接着根据允许环境和fallback等配置确认路由的model
  • 最后根据model来实例化了history属性。HashHistory继承与History。初始化阶段主要完成了对location的规范化操作