阅读 892

Vue源码-Vue-Router

前端路由是构建单页面应用的关键技术,它可以让浏览器URL变化但是不请求服务器的前提下,让页面重新渲染出我们想要的结果。Vue-Router是Vue应用的前端路由插件,让我们来看看它的实现原理。

路由例子

为了可以更好的阅读源码,我们可以用一个路由的简单例子来看下关键步骤的结果:

// main.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';

Vue.use(VueRouter);

const Foo = {
  template: `
  <div>
    <p>Foo</p>
    <router-link to="/foo/bar">Go to Bar</router-link>
    <router-view></router-view>
  </div>`
};

const Bar = {
  template: `<div><p>Bar</p></div>`
};

const routes = [
  {
    path: '/foo',
    component: Foo,
    children: [
      {
        path: 'bar',
        component: Bar
      }
    ]
  }
];

const router = new VueRouter({
  routes
});

new Vue({
  el: '#app',
  render: h => h(App),
  router
});
复制代码

对应App组件的代码:

<template>
  <div class="app">
    <h1>Vue Router App</h1>
    <p>
      <router-link to="/foo">Go to foo</router-link>
    </p>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'app'
};
</script>
复制代码

插件安装

Vue为所有插件提供一个Vue.use()来安装注册插件,这个方法会调用插件导出对象的install方法,并把Vue函数作为该函数第一个参数传递。在src/install.js文件中是关于Vue Router的安装程序。安装的过程主要有关键的几步:

  • 通过Vue.mixin()全局混入beforeCreatedestroyed钩子:
Vue.mixin({
  beforeCreate () {
    if (isDef(this.$options.router)) {
      // new Vue 实例
      this._routerRoot = this
      this._router = this.$options.router
      this._router.init(this) // 初始化
      Vue.util.defineReactive(this, '_route', this._router.history.current)
    } else {
      this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
    }
    registerInstance(this, this)
  },
  destroyed () {
    registerInstance(this)
  }
})
复制代码

beforeCreate钩子中,对于根实例,设置_routerRoot为实例本身,_router为VueRouter实例并调用init方法进行初始化,然后通过defineReactive_route属性进行响应处理,这个是路径导航导致视图渲染的关键。对于组件实例,通过父子链关系this.$parent && this.$parent._routerRoot设置_routerRoot属性。在函数最后调用registerInstance主要是把组件的实例和路由规则进行绑定,这个之后会知道用处。

  • Vue.prototype挂载属性
// 每个组件可以vm.$router获取VueRouter实例
Object.defineProperty(Vue.prototype, '$router', {
  get () { return this._routerRoot._router }
})

// 每个组件可以vm.$route获取当前的路由路径Route
Object.defineProperty(Vue.prototype, '$route', {
  get () { return this._routerRoot._route }
})
复制代码

这就是为什么我们能在每个组件内通过vm.$routervm.$route方法路由实例和当前路由路径的原因

  • 全局注册路由组件
// 全局注册router-view和router-link组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
复制代码

之后我们就可以在任何组件内使用router-link进行路由跳转,使用router-view进行路由组件的挂载。

VueRouter

在进行插件安装后,然后会声明路由配置规则,并通过new VueRouter(options)新建路由实例。在src/index.js定义VueRouter类,在构造函数中先初始化一些属性:

this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
复制代码

紧接着是对路由模式mode的处理:

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
复制代码

它默认值为hash。对于设置了history模式,并且浏览器不支持history.pushState并且没有设置不允许自动降级fallback=false,会自动用hash模式替换。如果不在浏览器端会采用abstract,比如在node环境,它主要是用数组的方式来模拟浏览记录栈。最后根据不同的mode来新建History实例,它是路由切换和记录管理的类:

 // 根据mode创建不同的history实例
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}`)
    }
}
复制代码

它们都定义在src/history文件下,不同模式的histroy类都继承定义在base.js的基类。

init

在之前路由安装中会通过在全局混入beforeCreate钩子并组件根实例会调用router实例的init方法:

this._router.init(this) // 初始化
复制代码

init方法首先是对注册路由的Vue实例的管理:

this.apps.push(app)
if (this.app) {
  return
}

this.app = 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在构造阶段已经根据不同mode初始化了,所以它调用不同方式进行初始根路由的跳转。transitionTo是跳转到指定路由位置的入口,setupListeners方法是监听浏览器的url变化,里面实现细节我们后文会分析。

matcher

路由实例有一个关键的属性matcher,它表示一个路由匹配器,是对路由记录和新路由匹配的管理。在构造函数中通过createMatcher方法进行初始化:

this.matcher = createMatcher(options.routes || [], this)
复制代码

这个方法定义在src/create-matcher.js,它定义了一些处理路由记录的方法并最终导出一个具有matchaddRoutes方法的对象:

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  // 创建RouteRecord映射
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  
  // ...
  
  return {
    match,
    addRoutes
  }
}
复制代码

addRoutes方法是添加路由配置对象,因为我们开发过程中并不是所有路由规则都是事先定义好的。主要是调用createRouteMap方法把routes路由配置规则生成路由记录并存在对应的变量中。路由记录RouteRecord是对我们路由配置对象的格式化对象,它的类型定义:

declare type RouteRecord = {
  path: string;
  regex: RouteRegExp;
  components: Dictionary<any>;
  instances: Dictionary<any>;
  name: ?string;
  parent: ?RouteRecord;
  redirect: ?RedirectOption;
  matchAs: ?string;
  beforeEnter: ?NavigationGuard;
  meta: any;
  props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
}
复制代码

来看下createRouteMap方法定义:

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // 这里优先获取传入的存取对象
  const pathList: Array<string> = oldPathList || []
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  // 遍历每个路由配置对象, 生成对象的RouteRecord对象
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })

  // 把通配符记录挪到最后
  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
  }
}
复制代码

这里一开始获取传入的存取对象否则新建一些默认值,这就是我们在之后可以添加路由配置的关键。接着遍历每个路由规则配置对象调用addRouteRecord方法。这个方法就是生成一个路由记录对象并存在对应的位置

const record: RouteRecord = {
  path: normalizedPath,
  // 利用path-to-regexp库创建路径正则表达式
  regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
  components: route.components || { default: route.component },
  instances: {},
  name,
  parent,
  matchAs,
  redirect: route.redirect,
  beforeEnter: route.beforeEnter,
  meta: route.meta || {},
  props:
    route.props == null
      ? {}
      : route.components
        ? route.props
        : { default: route.props }
}

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

if (name) {
  if (!nameMap[name]) {
    nameMap[name] = record
  } 
}
复制代码

对于嵌套的路由规则,还要遍历chidlren中每个路由规则并递归调用addRouteRecord方法:

if (route.children) {
  route.children.forEach(child => {
    const childMatchAs = matchAs
      ? cleanPath(`${matchAs}/${child.path}`)
      : undefined
    addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
  })
}
复制代码

最终,pathList就是所有路由规则路径的数组,pathMap就是路径到路由记录的映射,nameMap就是路由名称到路由记录的映射。

对于我们的例子,生成的结果如下:

接着,我们看下match方法的定义。match方法是根据当前的route和响应跳转的位置location计算得出新的route。首先,要对新的跳转位置进行格式化:

const location = normalizeLocation(raw, currentRoute, false, router)
复制代码

因为我们在route-linkto属性或者router.push方法传的可以是一个字符串或者对象,统一把它格式化成location对象。对于它的类型定义:

declare type Location = {
  _normalized?: boolean;
  name?: string;
  path?: string;
  hash?: string;
  query?: Dictionary<string>;
  params?: Dictionary<string>;
  append?: boolean;
  replace?: boolean;
}
复制代码

对应有name属性的location,我们直接在nameMap中可以拿到对应的路由记录。然后处理下路由参数生成路由最终的路径:

if (name) {
  // 根据name拿到路由记录
  const record = nameMap[name]
  if (!record) return _createRoute(null, location)

  const paramNames = record.regex.keys
    .filter(key => !key.optional)
    .map(key => key.name)

  if (typeof location.params !== 'object') {
    location.params = {}
  }

  if (currentRoute && typeof currentRoute.params === 'object') {
    for (const key in currentRoute.params) {
      if (!(key in location.params) && paramNames.indexOf(key) > -1) {
        location.params[key] = currentRoute.params[key]
      }
    }
  }

  // 生成完成的路径字符串
  location.path = fillParams(record.path, location.params, `named route "${name}"`)
  return _createRoute(record, location, redirectedFrom)
}
复制代码

对于只有path属性的位置信息,必须遍历每个记录进行正则匹配,匹配到了还要提取path中的参数到params对象中:

for (let i = 0; i < pathList.length; i++) {
  const path = pathList[i];
  const record = pathMap[path];
  if (matchRoute(record.regex, location.path, location.params)) {
    // 匹配成功
    return _createRoute(record, location, redirectedFrom)
  }
}
复制代码

两种情况找到对于的路由记录后都会调用_createRoute方法生成路由对象route

function _createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: Location
): Route {
  if (record && record.redirect) {
    return redirect(record, redirectedFrom || location)
  }
  if (record && record.matchAs) {
    return alias(record, location, record.matchAs)
  }
  return createRoute(record, location, redirectedFrom, router)
}
复制代码

其中redirectalias是对重定向和别名的处理,在它们里面或者正常情况都会调用createRoute来返回一个路由路径对象。

export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}
  try {
    query = clone(query)
  } catch (e) {}

  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  return Object.freeze(route)
}
复制代码

路由对象route和位置信息location最大区别它包含一个路由规则匹配的路径信息matched。它通过formatMatch方法生成:

// 构建route中matched,先父后子
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record)
    record = record.parent
  }
  return res
}
复制代码

很明显它会通过不断查找记录的父亲,然后丢到数组头部,所以matched的匹配顺序保持先父后子。这个属性在后面的视图渲染尤其重要。最终,我们的match方法就根据新位置信息和当前路由对象得出新的路由对象。

history

在路由的编程导航中,我们可以通过router.push()方法来跳转一个新的路径并渲染新的视图。我们从入口看下整个流程。push是不同类型的history自身的实现,对于HashHistory的定义在src/history/hash.js:

// 编程跳转
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(
    location,
    route => {
      pushHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    },
    onAbort
  )
}
复制代码

这个方法主要调用了基类定义的transitionTo方法:

transitionTo (
  location: RawLocation,
  onComplete?: Function,
  onAbort?: Function
) {
  const route = this.router.match(location, this.current) // 根据跳转的location计算出新的route
  this.confirmTransition(
    route,
    () => {
      // 更新route
      this.updateRoute(route)
      onComplete && onComplete(route)
      this.ensureURL()

      // fire ready cbs once
      if (!this.ready) {
        this.ready = true
        this.readyCbs.forEach(cb => {
          cb(route)
        })
      }
    },
    err => {
      if (onAbort) {
        onAbort(err)
      }
      if (err && !this.ready) {
        this.ready = true
        this.readyErrorCbs.forEach(cb => {
          cb(err)
        })
      }
    }
  )
}
复制代码

导航守卫

transitionTo方法中先调用match方法计算出新的路由对象,然后调用confirmTransition方法,这个方法主要是处理导航守卫的逻辑。首先会判断新的路由对象是否和老路由对象相等,相等的话会调用aboart函数并退出:

const current = this.current;
const abort = err => {
  if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {
    if (this.errorCbs.length) {
      this.errorCbs.forEach(cb => {
        cb(err);
      });
    } else {
      warn(false, 'uncaught error during route navigation:');
      console.error(err);
    }
  }
  onAbort && onAbort(err);
};
// 相同的route
if (
  isSameRoute(route, current) &&
  // in the case the route map has been dynamically appended to
  route.matched.length === current.matched.length
) {
  this.ensureURL();
  return abort(new NavigationDuplicated(route));
}
复制代码

接着根据新老路由对象计算出需要更新,进入和离开的路由记录:

const { updated, deactivated, activated } = resolveQueue(
  this.current.matched,
  route.matched
)
复制代码

resolveQueue方法的定义:

// 根据新旧的matched得出不同类型的RouteRecord部分
// 比如 /foo/bar 和 /foo/baz 的 matched比较
// /foo =>  updated  /foo/baz =>  activated   /foo/bar => deactivated
function resolveQueue (
  current: Array<RouteRecord>,
  next: Array<RouteRecord>
): {
  updated: Array<RouteRecord>,
  activated: Array<RouteRecord>,
  deactivated: Array<RouteRecord>
} {
  let i
  const max = Math.max(current.length, next.length)
  for (i = 0; i < max; i++) {
    if (current[i] !== next[i]) {
      break
    }
  }
  return {
    updated: next.slice(0, i),
    activated: next.slice(i),
    deactivated: current.slice(i)
  }
}
复制代码

有了这三个变量我们就可以轻松提取路由记录对应的路由钩子,包括离开,更新或者进入的钩子:

// 构建导航守卫函数队列
const queue: Array<?NavigationGuard> = [].concat(
  // 组件beforeRouteLeave钩子
  extractLeaveGuards(deactivated),
  // 全局beforeEach
  this.router.beforeHooks,
  // 组件beforeRouteUpdate钩子
  extractUpdateHooks(updated),
  // 路由配置的组件beforeEnter钩子
  activated.map(m => m.beforeEnter),
  // 解析异步组件的钩子
  resolveAsyncComponents(activated)
)
复制代码

这个钩子函数数组会作为runQueue函数的参数进行调用:

export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => {
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}
复制代码

这个方法从零开始遍历路由钩子,对于每个钩子都会作为fn函数参数调用,并且在第二个函数参数中递归调用进行下个钩子的执行逻辑,很明显第二个参数就类似我们钩子函数的nextfn是一个定义的迭代器来执行钩子函数:

// 执行导航守卫钩子
const iterator = (hook: NavigationGuard, next) => {
  if (this.pending !== route) {
    return abort();
  }
  try {
    hook(route, current, (to: any) => {
      if (to === false || isError(to)) {
        // next(false) -> abort navigation, ensure current URL
        // next(false)情况
        this.ensureURL(true);
        abort(to);
      } else if (
        typeof to === 'string' ||
        (typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string'))
      ) {
        // next('/') or next({ path: '/' }) -> redirect
        // 在导航守卫中next重定向
        abort();
        if (typeof to === 'object' && to.replace) {
          this.replace(to);
        } else {
          this.push(to);
        }
      } else {
        // confirm transition and pass on the value
        // 执行queue中的下一个钩子
        next(to);
      }
    });
  } catch (e) {
    abort(e);
  }
};
复制代码

这个函数很简单,它先执行我们的路由钩子函数,然后判断传给next函数的参数,如果是false的情况直接调用abort函数中止导航,如果是字符串还要进行跳转到新的路径,最后才成功执行下一个钩子函数。在runQueue方法的钩子队列执行完后会执行下面的回调逻辑:

const postEnterCbs = [];
const isValid = () => this.current === route;
// 提取组件的beforeRouteEnter
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
// 全局beforeresolve
const queue = enterGuards.concat(this.router.resolveHooks);
runQueue(queue, iterator, () => {
  if (this.pending !== route) {
    return abort();
  }
  this.pending = null;
  onComplete(route);
  if (this.router.app) {
    // 在route-view组件更新完后执行beforeRouteEnter钩子传给next函数的回调
    this.router.app.$nextTick(() => {
      postEnterCbs.forEach(cb => {
        cb();
      });
    });
  }
});
复制代码

其中postEnterCbs是对beforeRouteEnter钩子中传给next回调参数的处理,它会在$nextTick视图渲染后执行,这个时候就能获取对应组件的实例对象。在执行完全局的beforeresolve钩子后,会先执行onComplete(route)方法执行成功的回调。在这个方法中会执行updateRoute来更新路由:

// 更新route
updateRoute (route: Route) {
  const prev = this.current
  this.current = route
  // 修改app._route,通知视图更新
  this.cb && this.cb(route) 
  // 执行全局beforeEach
  this.router.afterHooks.forEach(hook => {
    hook && hook(route, prev)
  })
}
复制代码

这个方法先更新this.current为新的路由对象,然后执行cb函数来设置注册路由的app的_route方法。它在init方法中进行定义:

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

// base.js
listen (cb: Function) {
  this.cb = cb
}
复制代码

因为在app根实例中对_route进行了响应式处理,所以重新设置就会触发它的setter,从而就会触发它的依赖进行更新,至于这些依赖其实就是用到route-view组件的实例的渲染watcher,然后这些实例就会重新执行render函数,根据最新的route对象来计算要渲染的路由组件,最后路由出口挂载最新的视图。在updateRoute方法最后,会执行全局的afterEach钩子。

url更新

在执行updateRoute方法后,还会执行传给transitionTo方法的成功回调。对于HashHistory对象,会执行pushHash方法来更新最新的路由路径:

function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}
复制代码

这个方法先判断浏览器是否支持pushState,支持的话通过getUrl方法得到完成的url并执行pushState设置。否则利用window.location.hash来设置浏览器url的hash:

// 根据path获取完成的url
function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

export function pushState (url?: string, replace?: boolean) {
  const history = window.history
  try {
    if (replace) {
    // 传递之前的数据对象
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      history.replaceState(stateCopy, '', url)
    } else {
      history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}
复制代码

这样当我们进行路由导航,浏览器的url就会更着变化。还有一种情况就是我们手动输入url,要监听响应的事件进行路由导航更新视图,对于hash类型它是在init中的setupListeners函数进行绑定:

setupListeners () {
  const router = this.router
  // 监听url变化事件
  window.addEventListener(
    supportsPushState ? 'popstate' : 'hashchange',
    () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      // 根据最新的hash进行路由切换
      this.transitionTo(getHash(), route => {
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    }
  )
}
复制代码

其中ensureSlash函数是处理不存在任何路径的情况,它会自动替换成#/

// 不存在hash的情况,默认为#/
function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}
复制代码

路由组件

router-view

Vue会在router-view组件中根据当前的路由对象渲染出正确的组件,那这一过程是怎么实现的。直接来看下这个组件的定义的渲染函数:

render(_, { props, children, parent, data }) { 
  data.routerView = true
  const h = parent.$createElement
  // 默认为defualt
  const name = props.name
  const route = parent.$route
  const cache = parent._routerViewCache || (parent._routerViewCache = {})
}
复制代码

因为router-view是一个函数组件,它在渲染的过程中是不存在组件实例的。对比正常的组件渲染,它在组件占位节点的init钩子中新建组件实例,然后调用实例的render函数进行渲染。函数组件的render函数是在创建占位节点时候就直接执行了,也就是说当前的实例就是在route-view组件的环境,也就是传给函数组件render函数第二个参数的parent属性。

类比我们的例子,第一个route-view执行时的parent就是App组件的实例,第二个就是Foo组件的实例。

紧接着是计算当前router-view的深度,它是判断当前所在的组件的占位符vnode是否有routerView属性来进行判断:

let depth = 0;
while (parent && parent._routerRoot !== parent) {
  // route-view所在组件环境的占位vnode
  const vnodeData = parent.$vnode && parent.$vnode.data;
  if (vnodeData) {
    if (vnodeData.routerView) {
      depth++;
    }
  }
  parent = parent.$parent;
}
data.routerViewDepth = depth;
复制代码

比如我们例子的第二个router-view所在组件是Foo,所以parent.$vnode就是Foo组件的占位vnode,因为它是挂载在第一个router-view的,所以它的routerViewtrue,于是depth为1;

在求出深度后,就可以在当前route对象的匹配路径中的matched属性得到渲染的组件对象components,然后根据具体的视图名称拿到对应的组件对象:

const matched = route.matched[depth]
// render empty node if no matched route
if (!matched) {
  cache[name] = null
  return h()
}

const component = cache[name] = matched.components[name]
复制代码

然后在要挂载的组件占位vnode上的data设置registerRouteInstance函数,这个函数会在组件的beforeCreate钩子中调用,并且设置匹配到的路由记录的instances属性,这样才能在路由的beforeRouteEnter中拿到对应的组件实例:

data.registerRouteInstance = (vm, val) => {
  // val could be undefined for unregistration
  const current = matched.instances[name]
  if (
    (val && current !== vm) ||
    (!val && current === vm)
  ) {
    matched.instances[name] = val
  }
}
复制代码

最后返回将要挂载的组件的占位vnode:

return h(component, data, children)
复制代码

因为我们在router-viewrender函数中用到了vm.$route属性,所以当前实例的渲染watcher会作为依赖进行收集,也就是说router-view组件所在的环境实例的render函数在路由切换时会重新执行。也就是为什么当我们进行路由导航时视图会更新的原因。

router-link

router-link组件是用来进行跳转的全局组件,它内部的实现原理也是调用了router.push路由方法进行导航。来看下render函数:

const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(
  this.to,
  current,
  this.append
)
复制代码

首先调用router.resolve方法计算出新的路由对象:

resolve (
  to: RawLocation,
  current?: Route,
  append?: boolean
): {
  location: Location,
  route: Route,
  href: string,
  // for backwards compat
  normalizedTo: Location,
  resolved: Route
} {
  current = current || this.history.current
  const location = normalizeLocation(
    to,
    current,
    append,
    this
  )
  const route = this.match(location, current)
  const fullPath = route.redirectedFrom || route.fullPath
  const base = this.history.base
  const href = createHref(base, fullPath, this.mode)
  return {
    location,
    route,
    href,
    // for backwards compat
    normalizedTo: location,
    resolved: route
  }
}
复制代码

然后是一堆处理点击连接样式的。接着定义一个守卫函数guardEvent,它会在某些情况直接返回不进行导航:

// 守卫函数
function guardEvent (e) {
  // don't redirect with control keys
  if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
  // don't redirect when preventDefault called
  if (e.defaultPrevented) return
  // don't redirect on right click
  if (e.button !== undefined && e.button !== 0) return
  // don't redirect if `target="_blank"`
  if (e.currentTarget && e.currentTarget.getAttribute) {
    const target = e.currentTarget.getAttribute('target')
    if (/\b_blank\b/i.test(target)) return
  }
  // this may be a Weex event which doesn't have this method
  if (e.preventDefault) {
    e.preventDefault()
  }
  return true
}
复制代码

然后定义连接点击的处理函数,它会根据配置判断调用push还是replace方法:

const handler = e => {
  if (guardEvent(e)) {
    if (this.replace) {
      router.replace(location, noop)
    } else {
      router.push(location, noop)
    }
  }
}

const on = { click: guardEvent };
if (Array.isArray(this.event)) {
// 处理配置的其他事件
  this.event.forEach(e => {
    on[e] = handler;
  });
} else {
  on[this.event] = handler;
}
复制代码

对于tag不是a标签的情况,还要处理slot里面是否有a标签的情况,有的话把data附加到它身上,否则赋值在最外层上。最后返回tag创建的vnode:

return h(this.tag, data, this.$slots.default)
复制代码

总结

到此,Vue-Router的源码就大致分析完了,其实里面有很多实现细节还没扣,比如怎么格式化一个location,怎么提取对应组件的路由钩子,滚动的处理等,但是这不影响路由变更到视图渲染的主流程。整个流程可以概括下图:

关注下面的标签,发现更多相似文章
评论