vue-router源码阅读笔记

357 阅读10分钟

vue-router源码阅读

前言

阅读源码是一种很好提高自己技术的方式,它既能帮你查漏补缺也能使你学到新的技术。虽然一直在使用vue-router,但一直没有深入去了解过。这次的就在这里做个阅读笔记,感兴趣就看看把~

阅读源码前你需要对hashhistory的相关知识有一定的了解,此外如果你阅读过vue源码的话那就更好了,这样你对vue-router的源码也会有更深的了解

路由源码简单的思维导图

路由源码解析

路由的使用

在阅读源码前我们先来复习vue-router是如何使用的,之后我们就根据这个步骤一步步去理解源码

// router.js
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)
export default new Router({
  mode: 'hash',
  routes: [
    { path: '/foo', name: 'foo', component: Foo },
    { path: '/bar',name: 'bar', component: Bar }
  ]
})

// main.js
import router from './router'

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
  1. 路由的安装
  2. 创建 router 实例,然后传 routes 配置
  3. 创建和挂载根实例,通过 router 配置参数注入路由
  4. 使用push或replace改变路由

路由插件的安装(install.js)

vue-router是通过使用Vue.use()安装。所以会调用插件暴露install方法。vue-router插件的install方法在github.com/vuejs/vue-r…

ps: 后面的文件路径就不一一列举了,可以自行clone到本地查看

install.js

import View from './components/view'
import Link from './components/link'

export let _Vue

export function install (Vue) {
  // 确保 install 方法只调用一次,防止重复安装
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined
  
  // 注册vue-router实例,registerRouteInstance在RouterView组件内有定义
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  // 在每一个Vue实列注入钩子函数,这一步很重要,
  // 在组件初始化后,beforeCreate钩子会执行,从而初始化路由
  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this 		// 根路由为自身
        this._router = this.$options.router
        this._router.init(this) 		// 初始化路由
        // 把 _route 属性定义为响应式的,这一步很关键。
        // 视图的渲染更新都是通过对这个属性的设值,从而触发视图更新;包括点击浏览器的前进和后退按钮
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

  // 在Vue原型上添加$router属性,这样在组件实例上我们就可以以this.$router的方式访问路由实列了
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  // 全局注册组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  // 钩子函数的合并策略
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

创建路由实例(index.js)

这里我们只看它的构造函数和init函数,其他一些函数大多是暴露API,比如push、addRoutes等。就不一一介绍。

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'
    // 配置一个回调函数,当不支持history模式时,回退到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}`)
        }
    }
  }

  // ...

  // 路由初始化函数,在beforeCreate钩子执行时,如果传入了 router 实例,执行这个方法
  // 只有根 Vue 实例会保存到 this.app 中
  init (app: any /* Vue component instance */) {
    // ...

    this.apps.push(app)

    // set up app destroyed handler
    // https://github.com/vuejs/vue-router/issues/2639
    app.$once('hook:destroyed', () => {
      // clean out app from this.apps array once destroyed
      const index = this.apps.indexOf(app)
      if (index > -1) this.apps.splice(index, 1)
      // ensure we still have a main app or null if no apps
      // we do not release the router so it can be reused
      if (this.app === app) this.app = this.apps[0] || null

      if (!this.app) this.history.teardown()
    })

    // main app previously initialized
    // return as we don't need to set up new history listener
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history

    if (history instanceof HTML5History || history instanceof HashHistory) {
      const handleInitialScroll = routeOrError => {
        const from = history.current
        const expectScroll = this.options.scrollBehavior
        const supportsScroll = supportsPushState && expectScroll

        if (supportsScroll && 'fullPath' in routeOrError) {
          handleScroll(this, routeOrError, from, false)
        }
      }
      const setupListeners = routeOrError => {
        history.setupListeners()
        handleInitialScroll(routeOrError)
      }
      // 默认进行一次路由跳转,同时初始化监听事件
      history.transitionTo(
        history.getCurrentLocation(),
        setupListeners,
        setupListeners
      )
    }
    
    // 设置history基类中cb的值,以便在调用transitionTo后
    // 能方便对组件的 _route 属性进行赋值,触发组件渲染
    history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })
  }
  
}

// ...

构造函数定义了一些属性,以及调用createMatcher创建路由映射表和路由记录对象,这个对后面的路由匹配有很关键的作用。同时构造函数还会根据路由配置的mode属性初始化不同的路由对象。

VueRouter类中还有其他方法这里没有一一介绍,这些方法大多是暴露出去的API,你可以在官方文档中查到。这里只讲解了一个init方法,这个方法在先前将插件安装时有提到,它会在beforeCreate钩子执行时调用,前提时有传入router实例,而我们一般在VUE的根实例中传入router实例,所以this.app中保存的都是vue的跟实例。

字段类型声明(declarations.js)

在阅读createMatcher、base基类等js文件前最好线了解以下vue-router源码中一些类型的定义,这样你在看源码时才分得清Location、RawLocation、RouteRecord、Route等这些的区别。

// ...
// new VueRouter()时传入的路由配置
declare type RouterOptions = {
  routes?: Array<RouteConfig>;
  mode?: string;
  fallback?: boolean;
  base?: string;
  linkActiveClass?: string;
  linkExactActiveClass?: string;
  parseQuery?: (query: string) => Object;
  stringifyQuery?: (query: Object) => string;
  scrollBehavior?: (
    to: Route,
    from: Route,
    savedPosition: ?Position
  ) => PositionResult | Promise<PositionResult>;
}

declare type RedirectOption = RawLocation | ((to: Route) => RawLocation)
// 单个路由的配置
declare type RouteConfig = {
  path: string;
  name?: string;
  component?: any;
  components?: Dictionary<any>;
  redirect?: RedirectOption;
  alias?: string | Array<string>;
  children?: Array<RouteConfig>;
  beforeEnter?: NavigationGuard;
  meta?: any;
  props?: boolean | Object | Function;
  caseSensitive?: boolean;
  pathToRegexpOptions?: PathToRegexpOptions;
}
// 路由记录
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>;
}
// 跳转路由
declare type Location = {
  _normalized?: boolean;
  name?: string;
  path?: string;
  hash?: string;
  query?: Dictionary<string>;
  params?: Dictionary<string>;
  append?: boolean;
  replace?: boolean;
}

// 跳转的路由,比如调用push时传入的路由对象
declare type RawLocation = string | Location

// 路由对象。this.$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;
}

创建路由映射表,路由匹配对象

create-matcher.js

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }

  // 路由匹配
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    // ...
  }

  return {
    match,
    addRoutes
  }
}

createMatcher主要返回了两个函数,一个用于匹配路由用的的match函数,一个用于动态添加路由的addRoutes函数。此外返回函数前还调用了createRouteMap方法。可以看到addRoutes也调用了createRouteMap方法。所以先来看看createRouteMap方法。match稍后了解。

create-route-map.js

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<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)

  // 每一个路由都执行addRouteRecord方法
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })

  // 把路径时'*'的放到数组最末尾。比如:['/foo', '/bar', '*']
  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
  }
}

// 添加路由记录
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 || {}
  // 格式化路由path
  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 },
    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 (route.children) {
    // ...
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  // 填充path数组和path的map对象
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

  // 如果有别名,则为别名添加路由记录对象
  // matchAs有值的RouteRecord就是别名的RouteRecord
  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]
      if (process.env.NODE_ENV !== 'production' && alias === path) {
        warn(
          false,
          `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
        )
        // skip in dev to make it work
        continue
      }

      const aliasRoute = {
        path: alias,
        children: route.children
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      )
    }
  }

  // 如果路由有命名,则填充name map对象
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        `Duplicate named routes definition: ` +
          `{ name: "${name}", path: "${record.path}" }`
      )
    }
  }
}

可以看到createRouteMap创建了3个引用类型的变量,并且返回。分别是:pathList、pathMap、nameMap。所以在调用addRoutes动态添加路由时,这个三个变量的值也会改变。同时createRouteMap还遍历路由配置,让每个路由都执行addRouteRecord方法。addRouteRecord方法主要是创建路由记录对象并填充到pathList、pathMap、nameMap这三个变量里面。

所以createMatcher调用后除了返回addRoutes和match两个方法外,最重要的一点就是根据用户的路由配置生成对应的路由记录。

现在来看看match方法:

// 路由匹配
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    // 规范化路由
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location

    // 有name则用name去查找路由记录
    if (name) {
      const record = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {
        warn(record, `Route with name '${name}' does not exist`)
      }
      // 没有匹配的路由
      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)
    } else if (location.path) {
      // 没有name则通过path去匹配路由
      location.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)
        }
      }
    }
    // no match(没有匹配的路由)
    return _createRoute(null, location)
  }
  
  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)
  }

match方法就是用来匹配路由的。match方法无论是否匹配到路由都会调用_createRoute方法,而_createRoute最后会调用createRoute方法。createRoute这个方法最终会返回一个不可以修改的Route对象。其实这个对象就是this.$route。源码如下:

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

history和路由的跳转

在前面介绍创建路由实例时,有说到创建实例时会根据用户配置创建不同history,在vue-router中主要有两种:hashhistory。简单来说hash是带有#号的,history是不带#号的。通过看源码可以知道,无论是HashHistory还是HTML5History都继承base.js文件里的History。所以我们注重看base.js文件夹的代码

base.js

// implemented by sub-classes
+go: (n: number) => void
+push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
+replace: (
  loc: RawLocation,
  onComplete?: Function,
  onAbort?: Function
) => void
+ensureURL: (push?: boolean) => void
+getCurrentLocation: () => string
+setupListeners: Function

在基类History中有以上这段代码,依注释说所说:这些方法都是在子类实现的,父类自是定义。

这里以html5.js为例。当使用this.$router.push({ name: 'foo' })切换路由时,此时时调用VueRouter类中的push方法,而这个方法会调用html5.js里的push方法。而这个push方法会调用基类的transitionTo方法达到切换路由的效果。所以来看看路是如何切换的。

  transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    // location保存的是传入的路由,current保存的是当前的路由对象
    let route
    // catch redirect option https://github.com/vuejs/vue-router/issues/3201
    try {
      // 匹配路由,返回路由对象。这个match就是createMatcher返回的match
      route = this.router.match(location, this.current)
    } catch (e) {
      this.errorCbs.forEach(cb => {
        cb(e)
      })
      // Exception should still be thrown
      throw e
    }
    this.confirmTransition(
      route,
      () => {
        const prev = this.current
        // 更新路由,触发渲染
        this.updateRoute(route)
        // 调用导航成功后的onComplete的回调函数
        onComplete && onComplete(route)
        this.ensureURL()
        // 调用全局的 afterEach 钩子
        this.router.afterHooks.forEach(hook => {
          hook && hook(route, prev)
        })

        // 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
          // Initial redirection should still trigger the onReady onSuccess
          // https://github.com/vuejs/vue-router/issues/3225
          if (!isNavigationFailure(err, NavigationFailureType.redirected)) {
            this.readyErrorCbs.forEach(cb => {
              cb(err)
            })
          } else {
            this.readyCbs.forEach(cb => {
              cb(route)
            })
          }
        }
      }
    )
  }

  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    // 用于中断导航
    const abort = err => {
      // changed after adding errors with
      // https://github.com/vuejs/vue-router/pull/3047 before that change,
      // redirect and aborted navigation would produce an err == null
      if (!isNavigationFailure(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)
    }
    const lastRouteIndex = route.matched.length - 1
    const lastCurrentIndex = current.matched.length - 1
    // 判断是否是相同的路由,相同路由则不跳转
    if (
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      lastRouteIndex === lastCurrentIndex &&
      route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
    ) {
      this.ensureURL()
      return abort(createNavigationDuplicatedError(current, route))
    }
    
    // 当前路由记录和目标路由记录比较,解析出哪些路由的组件是需要更新,哪些不需要更新
    // updated: 不需要更新的组件路由
    // deactivated: 失活组件的路由
    // activated:要更新的组件路由
    // 注意matched存放的是路由记录对象(RouteRecord)
    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )
    
    // 把要执行的钩子函数提取到queue队列中,稍后依次执行
    const queue: Array<?NavigationGuard> = [].concat(
      // in-component leave guards
      // 提取失活组件的 beforeRouteLeave 钩子函数
      extractLeaveGuards(deactivated),
      // global before hooks
      // 全局 beforeEach 钩子函数
      this.router.beforeHooks,
      // in-component update hooks
      // 提取要更新组件的 beforeRouteUpdate 钩子函数
      extractUpdateHooks(updated),
      // in-config enter guards
      // 全局 beforeEnter 钩子函数
      activated.map(m => m.beforeEnter),
      // async components
      // 解析异步组件
      resolveAsyncComponents(activated)
    )

    this.pending = route
    // 一个迭代器函数,在runQueue时会调用
    // 这里的hook就是钩子函数,next对应runQueue中的 " () => { step(index + 1) } "函数
    // 这就是为什么官方文档说一定要调用next才会执行下一个钩子函数。
    const iterator = (hook: NavigationGuard, next) => {
      if (this.pending !== route) {
        return abort(createNavigationCancelledError(current, route))
      }
      try {
        hook(route, current, (to: any) => {
          // next(false) 中断当前的导航
          if (to === false) {
            // next(false) -> abort navigation, ensure current URL
            this.ensureURL(true)
            abort(createNavigationAbortedError(current, route))
          } else if (isError(to)) {
            // next(Error)
            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('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航
            abort(createNavigationRedirectedError(current, route))
            if (typeof to === 'object' && to.replace) {
              this.replace(to)
            } else {
              this.push(to)
            }
          } else {
            // confirm transition and pass on the value
            // 执行下个导航钩子
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }

    // 非常经典的异步函数队列化执行的模式(同步执行异步函数)、
    runQueue(queue, iterator, () => {
      // postEnterCbs 用于存放 beforeRouteEnter 钩子调用next时传入的回调函数。
      // 比如在beforeRouteEnter要访问组件实例,但此时时拿不到实例的,所以我们一般会以
      // next(vm => { ... })这种方式访问组件实列。
      const postEnterCbs = []
      const isValid = () => this.current === route
      // wait until async components are resolved before
      // extracting in-component enter guards
      // 当异步组件加载玩,会执行这个回调函数,也就是 runQueue 中的 cb
      // 这时就可以运行激活组件内钩子函数了
      const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
      const queue = enterGuards.concat(this.router.resolveHooks)
      runQueue(queue, iterator, () => {
        // 路由跳转完成
        if (this.pending !== route) {
          return abort(createNavigationCancelledError(current, route))
        }
        this.pending = null
        onComplete(route)
        if (this.router.app) {
          // 调用postEnterCbs中的函数,此时next中的回调就可以拿到组件实例了 
          this.router.app.$nextTick(() => {
            postEnterCbs.forEach(cb => {
              cb()
            })
          })
        }
      })
    })
  }

再来看看对导航钩子的处理。这里组要说说对beforeRouteEnter钩子函数的处理。我们知道在这个函数是不能获取到组件实例的,如果要获取组件实例,需要通过回调的方式获取。所以来看看源码的实现:

function extractEnterGuards (
  activated: Array<RouteRecord>,
  cbs: Array<Function>,
  isValid: () => boolean
): Array<?Function> {
  return extractGuards(
    activated,
    'beforeRouteEnter',
    (guard, _, match, key) => {
      return bindEnterGuard(guard, match, key, cbs, isValid)
    }
  )
}

extractEnterGuards的实现和extractLeaveGuards、extractUpdateHooks,差不多,不同的时前者调用的是bindEnterGuard

function bindEnterGuard (
  guard: NavigationGuard,
  match: RouteRecord,
  key: string,
  cbs: Array<Function>,
  isValid: () => boolean
): NavigationGuard {
  // 返回的routeEnterGuard函数就是iterator里执行的hook函数
  // 这里的guard函数就是我们定义在组件上的beforeRouteEnter钩子函数。
  // 当iterator执行hook时,就相当于执行routeEnterGuard,会先执行我们定义的钩子函数,然后把回调函数收集到cbs中,cbs就是postEnterCbs
  // 之后在执行next,这个next就是传入hook的第三个参数。
  return function routeEnterGuard (to, from, next) {
    return guard(to, from, cb => {
      if (typeof cb === 'function') {
        cbs.push(() => {
          // #750
          // if a router-view is wrapped with an out-in transition,
          // the instance may not have been registered at this time.
          // we will need to poll for registration until current route
          // is no longer valid.
          poll(cb, match.instances, key, isValid)
        })
      }
      next(cb)
    })
  }
}

在根路由组件重新渲染后,遍历postEnterCbs执行回调,每一个回调执行的时候,其实是执行 poll(cb, match.instances, key, isValid) 方法,因为考虑到一些了路由组件被套 transition 組件在一些缓动模式下不一定能拿到实例,所以用一个轮询方法不断去判断,直到能获取到组件实例,再去调用 cb,并把组件实例作为参数传入,这就是我们在回调函数中能拿到组件实例的原因。

最后来看看更新路由、触发渲染调用的updateRoute函数

updateRoute (route: Route) {
  this.current = route
  this.cb && this.cb(route)
}

可以看到这个函数很简单,就是改变当前路由对象的值以及调用cb函数。这个cb函数之前在VueRouter初始化的时候就已近赋值了,还记得不?

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

所以当调用cb函数时会执行app._route = route这个赋值操作,而_route属性又是响应式的,所以就会触发组件渲染了。

结语

VueRouter源码的阅读笔记就写到这里了,虽然RouterView和RouterLink组件的源码没完全看完,但不影响对VueRouter原理的理解,所以这里就不写出来了[狗头保命]。即使没有逐行的去理解,但整体上算明白了VueRouter的原理了。

总的来说VueRouter的原理就是通过beforeCreate钩子函数为Vue根实例注入路由,然后使用defineReactive方法将_route属性变为响应式字段。然后通过监听路由变化事件(popstate,hashchange)或手动改变路由(push,replace等)更新_route的值,从而触发RouterView重新渲染。

参考借鉴