阅读 1576

解密vue-router: 从源码开始

前几天笔者看到一个问题:你真的了解vue-router的吗?你知道vue-router的运行原理吗?抱着这样的问题,笔者开始了vue-router的源码探索之旅。本文并没有逐行去深究源码,而是跟着笔者画的流程图来简析每一步的运行流程。

剖析运行流程

笔者根据源码的结构和自己的理解事先画好了一张流程图,乍一看这张运行流程图可能会有点蒙圈,笔者接下来会现根据这张图分析下运行流程,然后再一步一步的剖析源码的核心部分。

运行流程
为了便于我们理解这张运行流程图,我们将挂载完vue-router的Vue实例打印出来看看都增加了什么东西:
1
2

  • $options下的router对象很好理解,这个就是我们在实例化Vue的时候挂载的那个vue-router实例;
  • _route是一个响应式的路由route对象,这个对象会存储我们路由信息,它是通过Vue提供的Vue.util.defineReactive来实现响应式的,下面的get和set便是对它进行的数据劫持;
  • _router存储的就是我们从$options中拿到的vue-router对象;
  • _routerRoot指向我们的Vue根节点;
  • _routerViewCache是我们对View的缓存;
  • $route$router是定义在Vue.prototype上的两个getter。前者指向_routerRoot下的_route,后者指向_routerRoot下的_router

接下来让我们顺顺这个“眼花缭乱的图”,以便于我们后面更好的理解之后的源码分析。

首先我们根据Vue的插件机制安装了vue-router,这里其实做的很简单,总结起来就是封装了一个mixin,定义了两个'原型',注册了两个组件。在这个mixin中,beforeCreate钩子被调用然后判断vue-router是否实例话了并初始化路由相关逻辑,前文提到的_routerRoot、_router、_route便是在此时被定义的。定义了两个“原型”是指在Vue.prototype上定一个两个getter,也就$route和$router。注册了两个组件是指在这里注册了我们后续会用到的RouterView和RouterLink这两个组件。

然后我们创建了一个VueRouter的实例,并将它挂载在Vue的实例上,这时候VueRouter的实例中的constructor初始化了各种钩子队列;初始化了matcher用于做我们的路由匹配逻辑并创建路由对象;初始化了history来执行过渡逻辑并执行钩子队列。

接下里mixin中beforeCreate做的另一件事就是执行了我们VueRouter实例的init()方法执行初始化,这一套流程和我们点击RouteLink或者函数式控制路由的流程类似,这里我就一起说了。在init方法中调用了history对象的transitionTo方法,然后去通过match获取当前路由匹配的数据并创建了一个新的路由对象route,接下来拿着这个route对象去执行confirmTransition方法去执行钩子队列中的事件,最后通过updateRoute更新存储当前路由数据的对象current,指向我们刚才创建的路由对象route。

最开始的时候我们说过_route被定义成了响应式的 那么一个路由更新之后,_route对象会接收到响应并通知RouteView去更新视图。

到此,流程就结束了,接下来我们将深入vue-router的源码去深度学习其原理。

剖析源码

说在前面

vue-router的源码都采用了flow作为类型检验,没有配置flow的话可能会满屏报错,本文不对flow做过多的介绍了。为了便于大家的理解,下面的源码部分我会将flow相关的语法去掉。顺便附上一些flow相关:

flow官方文档(需要科学上网):https://flow.org/ flow入门:https://zhuanlan.zhihu.com/p/26204569 flow配置:https://zhuanlan.zhihu.com/p/24649359

项目结构

在拿到一个项目的源码时候,我们首先要去看它的目录结构:

3
其中src是我们的项目源码部分,它包含如下结构:

  • componets是RouterLink和RouterView这两个组件;
  • create-matcher.js就是我们创建match的入口文件;
  • create-route-map.js用于创建path列表,path map,name map等;
  • history是创建hitory类的逻辑;
  • index.js就是我们的入口文件,其中创建了VueRouter这个类;
  • install.js是我们挂载vue-router插件的逻辑;
  • util定义了很多工具函数;

应用入口

通常我们去构建一个Vue应用程序的时候入口文件通常会这么写:

// app.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Main from '../components/main';

Vue.use(VueRouter);

const router = new VueRouter({
  routes: [{
    path: '/',
    component: Main,
  }],
});

// app.js
new Vue({
  router,
  template,
}).$mount('#app')
复制代码

我们可以看到vue-router是以插件的形式安装的,并且vue-router的实例也会挂载在Vue的实例上面。

插件安装

此时我们将目光移入源码的入口文件,发现index.js中引入了install模块,并在VueRouter类上挂载了一个静态的install方法。而且还判断了环境中如果已经挂载了Vue则自动去使用这个插件。

源码位置:/src/index.js

import { install } from './install'
import { inBrowser } from './util/dom'
// ...
export default class VueRouter {}
// ...
// 挂载install;
VueRouter.install = install
// 判断如果window上挂载了Vue则自动使用插件;
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}
复制代码

接下来看install.js这个文件,这个文件导出了export方法以供Vue.use去安装:

源码位置:/src/install.js

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

// export一个Vue的原因是可以不讲Vue打包进插件中而使用Vue一些方法;
// 只能在install之后才会存在这个Vue的实例;
export let _Vue

export function install (Vue) {
  // 如果插件已经安装就return
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    beforeCreate () {
      // this.$options.router为VueRouter实例;
      // 这里判断实例是否已经挂载;
      if (isDef(this.$options.router)) {
        // 将router的根组件指向Vue实例
        this._routerRoot = this
        this._router = this.$options.router
        // router初始化,调用VueRouter的init方法;
        this._router.init(this)
        // 使用Vue的defineReactive增加_route的响应式对象
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 将每一个组件的_routerRoot都指向根Vue实例;
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      // 注册VueComponent 进行Observer处理;
      registerInstance(this, this)
    },
    destroyed () {
      // 注销VueComponent
      registerInstance(this)
    }
  })
  // 为$router和4route定义 << getter >> 分别指向_routerRoot的 _router 和 _route
  // _router 为VueRouter的实例;
  // _route 为一个存储了路由数据的对象;
  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)

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

这里需要注意的几点:

  • 导出一个Vue引用:这是为了不用将整个Vue打包进去就可以使用Vue提供的一些API,当然,这些的前提就是vue-router必须被安装挂载;
  • 在Vue.prototype上定义两个getter:Vue的组件都是Vue实例的一个扩展,他们都可以访问prototype上的方法和属性;
  • 定义响应式_route对象:有了这个响应式的路由对象,就可以在路由更新的时候及时的通知RouterView去更新组件了;

实例化VueRouter

接下来我们来看VueRouter类的实例化,在constructor中主要做的就两件事,创建matcher和创建history:

源码位置:/src/index.js

// ...
import { createMatcher } from './create-matcher'
import { supportsPushState } from './util/push-state'
import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'
// ...
export default class VueRouter {
  constructor (options) {
    this.app = null
    this.apps = []
    // VueRouter 配置项;
    this.options = options
    // 三个钩子
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    // 创建路由匹配实例;传人我们定义的routes:包含path和component的对象;
    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'
    }
    // node运行环境 mode = 'abstract';
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = 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}`)
        }
    }
  }
  // ...
}
复制代码

创建matcher

顺着思路我们先看createMatcher这个函数:

源码位置:/src/create-matcher.js

import VueRouter from './index'
import { resolvePath } from './util/path'
import { assert, warn } from './util/warn'
import { createRoute } from './util/route'
import { fillParams } from './util/params'
import { createRouteMap } from './create-route-map'
import { normalizeLocation } from './util/location'

// routes为我们初始化VueRouter的路由配置;
// router就是我们的VueRouter实例;
export function createMatcher (routes, router) {
  // pathList是根据routes生成的path数组;
  // pathMap是根据path的名称生成的map;
  // 如果我们在路由配置上定义了name,那么就会有这么一个name的Map;
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  // 根据新的routes生成路由;
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  // 路由匹配函数;
  function match (raw, currentRoute, redirectedFrom) {
    // 简单讲就是拿出我们path params query等等;
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location

    if (name) {
      // 如果有name的话,就去name map中去找到这条路由记录;
      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]
          }
        }
      }

      if (record) {
        location.path = fillParams(record.path, location.params, `named route "${name}"`)
        return _createRoute(record, location, redirectedFrom)
      }
    } else if (location.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, location, redirectedFrom) {
    // 根据不同的条件去创建路由对象;
    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)
  }

  return {
    match,
    addRoutes
  }
}

function matchRoute (regex, path, params) {
  const m = path.match(regex)

  if (!m) {
    return false
  } else if (!params) {
    return true
  }

  for (let i = 1, len = m.length; i < len; ++i) {
    const key = regex.keys[i - 1]
    const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i]
    if (key) {
      params[key.name] = val
    }
  }

  return true
}

function resolveRecordPath (path, record) {
  return resolvePath(path, record.parent ? record.parent.path : '/', true)
}
复制代码

首先createMatcher会根据我们初始化VueRouter实例时候定义的routes配置,通过createRouteMap生成一份含有对应关系的map,具体逻辑下面我们会说到。然后返回一个包含match和addRoutes两个方法的对象match,就是我们实现路由匹配的详细逻辑,他会返回匹配的路由对象;addRoutes会就是添加路由的方法。

接下来我们顺着刚才的思路去看create-route-map.js

源码位置:/src/create-route-map.js

/* @flow */

import Regexp from 'path-to-regexp'
import { cleanPath } from './util/path'
import { assert, warn } from './util/warn'

export function createRouteMap (routes, oldPathList, oldPathMap, oldNameMap) {
  // the path list is used to control path matching priority
  const pathList = oldPathList || []
  // $flow-disable-line
  const pathMap = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap = oldNameMap || Object.create(null)
  // path列表
  // path的map映射
  // name的map映射
  // 为配置的路由项增加路由记录
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })

  // 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--
    }
  }
  // 返回包含path数组,path map和name map的对象;
  return {
    pathList,
    pathMap,
    nameMap
  }
}

function addRouteRecord (pathList, pathMap, nameMap, route, parent, matchAs) {
  const { path, name } = route
  if (process.env.NODE_ENV !== 'production') {
    assert(path != null, `"path" is required in a route configuration.`)
    assert(
      typeof route.component !== 'string',
      `route config "component" for path: ${String(path || name)} cannot be a ` +
      `string id. Use an actual component instead.`
    )
  }

  // 定义 path 到 Reg 的选项;
  const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
  // 序列化path,'/'将会被替换成'';
  const normalizedPath = normalizePath(
    path,
    parent,
    pathToRegexpOptions.strict
  )

  // 正则匹配是否区分大小写;
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }

  const record = {
    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) {
    // Warn if route is named, does not redirect and has a default child route.
    // If users navigate to this route by name, the default child will
    // not be rendered (GH Issue #629)
    if (process.env.NODE_ENV !== 'production') {
      if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
        warn(
          false,
          `Named Route '${route.name}' has a default child route. ` +
          `When navigating to this named route (:to="{name: '${route.name}'"), ` +
          `the default child route will not be rendered. Remove the name from ` +
          `this route and use the name of the default child route for named ` +
          `links instead.`
        )
      }
    }
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  // 如果路由含有别名,则为其添加别名路由记录
  // 关于alias
  // https://router.vuejs.org/zh-cn/essentials/redirect-and-alias.html
  if (route.alias !== undefined) {
    const aliases = Array.isArray(route.alias)
      ? route.alias
      : [route.alias]

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

  // 更新path map
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }
  // 为定义了name的路由更新 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}" }`
      )
    }
  }
}

function compileRouteRegex (path, pathToRegexpOptions) {
  const regex = Regexp(path, [], pathToRegexpOptions)
  if (process.env.NODE_ENV !== 'production') {
    const keys: any = Object.create(null)
    regex.keys.forEach(key => {
      warn(!keys[key.name], `Duplicate param keys in route with path: "${path}"`)
      keys[key.name] = true
    })
  }
  return regex
}

function normalizePath (path, parent, strict): string {
  if (!strict) path = path.replace(/\/$/, '')
  if (path[0] === '/') return path
  if (parent == null) return path
  return cleanPath(`${parent.path}/${path}`)
}
复制代码

从上述代码可以看出,create-route-map.js的就是根据用户的routes配置的path、alias以及name来生成对应的路由记录。

创建history

matcher这一部分算是讲完了,接下来该说History的实例化了,从源码来说history文件夹下是有4个文件的,base作为基类,另外三个继承这个基类来分别处理vue-router的各种mode情况,这里我们主要看base的逻辑就可以了。

// install 到处的Vue,避免Vue打包进项目增加体积;
import { START, isSameRoute } from '../util/route'

export class History {
  constructor (router, base) {
    this.router = router
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    // 生成一个基础的route对象;
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
  }
  // ...
}
// ...
function normalizeBase (base: ?string): string {
  if (!base) {
    if (inBrowser) {
      // respect <base> tag
      const baseEl = document.querySelector('base')
      base = (baseEl && baseEl.getAttribute('href')) || '/'
      // strip full URL origin
      base = base.replace(/^https?:\/\/[^\/]+/, '')
    } else {
      base = '/'
    }
  }
  // make sure there's the starting slash
  if (base.charAt(0) !== '/') {
    base = '/' + base
  }
  // remove trailing slash
  return base.replace(/\/$/, '')
}
复制代码

基础的挂载和各种实例化都说完了之后,我们可以从init入手去看之后的流程了。

5
之前在讲install的时候知道了在mixin中的beforeCreate钩子里执行了init,现在我们移步到VueRouter的init方法。

源码位置:/src/index.js

// ...
init (app) {
    process.env.NODE_ENV !== 'production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )
    // 从install中的调用我们知道,这个app就是我们实例化的vVue实例;
    this.apps.push(app)

    // main app already initialized.
    if (this.app) {
      return
    }
    // 将VueRouter内的app指向我们亘Vue实例;
    this.app = app

    const history = this.history
    // 针对于 HTML5History 和 HashHistory 特殊处理,
    // 因为在这两种模式下才有可能存在进入时候的不是默认页,
    // 需要根据当前浏览器地址栏里的 path 或者 hash 来激活对应的路由
    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }
    //...
  }
// ...
复制代码

可以看到初始化主要就是给app赋值,并且针对于HTML5History和HashHistory进行特殊的处理,因为在这两种模式下才有可能存在进入时候的不是默认页,需要根据当前浏览器地址栏里的path或者hash来激活对应的路由,此时就是通过调用transitionTo来达到目的;

接下来来看看这个具体的transitionTo:

源码位置:/src/history/base.js

transitionTo (location, onComplete, onAbort) {
    // localtion为我们当前页面的路由;
    // 调用VueRouter的match方法获取匹配的路由对象,创建下一个状态的路由对象;
    // this.current是我们保存的当前状态的路由对象;
    const route = this.router.match(location, this.current)
    this.confirmTransition(route, () => {
      // 更新当前的route对象;
      this.updateRoute(route)
      onComplete && onComplete(route)
      // 调用子类的方法更新url
      this.ensureURL()
      // fire ready cbs once
      // 调用成功后的ready的回调函数;
      if (!this.ready) {
        this.ready = true
        this.readyCbs.forEach(cb => { cb(route) })
      }
    }, err => {
      if (onAbort) {
        onAbort(err)
      }
      // 调用失败的err回调函数;
      if (err && !this.ready) {
        this.ready = true
        this.readyErrorCbs.forEach(cb => { cb(err) })
      }
    })
  }
  confirmTransition (route, onComplete, onAbort) {
    const current = this.current
    const abort = err => {
      if (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)
    }
    // 如果是同一个路由就不去跳转;
    if (
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      route.matched.length === current.matched.length
    ) {
      // 调用子类的方法更新url
      this.ensureURL()
      return abort()
    }
    // 交叉比对当前路由的路由记录和现在的这个路由的路由记录
    // 以便能准确得到父子路由更新的情况下可以确切的知道
    // 哪些组件需要更新 哪些不需要更新
    const {
      updated,
      deactivated,
      activated
    } = resolveQueue(this.current.matched, route.matched)
    // 注意,matched里头存储的是路由记录的数组;

    // // 整个切换周期的队列,待执行的各种钩子更新队列
    const queue: Array<?NavigationGuard> = [].concat(
      // in-component leave guards
      // 提取组件的 beforeRouteLeave 钩子
      extractLeaveGuards(deactivated),
      // global before hooks
      this.router.beforeHooks,
      // in-component update hooks
      // 提取组件的 beforeRouteUpdate 钩子
      extractUpdateHooks(updated),
      // in-config enter guards
      activated.map(m => m.beforeEnter),
      // async components
      // 异步处理组件
      resolveAsyncComponents(activated)
    )
    // 保存下一个状态的路由
    this.pending = route
    // 每一个队列执行的 iterator 函数
    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
            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
            abort()
            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, () => {
      const postEnterCbs = []
      const isValid = () => this.current === route
      // wait until async components are resolved before
      // extracting in-component enter guards
      // 等待异步组件 OK 时,执行组件内的钩子
      const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
      const queue = enterGuards.concat(this.router.resolveHooks)
      // 在上次的队列执行完成后再执行组件内的钩子
      // 因为需要等异步组件以及是OK的情况下才能执行
      runQueue(queue, iterator, () => {
        if (this.pending !== route) {
          return abort()
        }
        // 路由过渡完成
        this.pending = null
        onComplete(route)
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            postEnterCbs.forEach(cb => { cb() })
          })
        }
      })
    })
  }
  updateRoute (route) {
    const prev = this.current
    // 将current指向我们更新后的route对象;
    this.current = route
    this.cb && this.cb(route)
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }
复制代码

逻辑看似复杂,实际上就是各种钩子函数的来回处理,但是这里要注意下,每一个路由route对象都会有一个matchd属性,这个属性包含一个路由记录,这个记录的生成在create-matcher.js中已经提到了。

等一下,我们好像漏了点东西,init后面还有一点没说:

源码位置:/src/index.js

// 设置路由改变时候的监听;
history.listen(route => {
    this.apps.forEach((app) => {
        app._route = route
    })
})
复制代码

在这里设置了route改变之后的回调函数, 会在confirmTransition中的onComplete回调中调用, 并更新当前的_route的值,前面我们提到,_route是响应式的,那么当其更新的时候就会去通知组件重新render渲染。

两个组件

大体流程都看完了,接下来可以看看两个组件了,我们先看RouterView组件: 源码位置:/src/components/view.js

import { warn } from '../util/warn'

export default {
  name: 'RouterView',
  functional: true,
  props: {
    // 试图名称,默认是default
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    data.routerView = true
    // directly use parent context's createElement() function
    // so that components rendered by router-view can resolve named slots
    // 渲染函数
    const h = parent.$createElement
    const name = props.name
    // 拿到_route对象和缓存对象;
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    // determine current view depth, also check to see if the tree
    // has been toggled inactive but kept-alive.
    // 组件层级
    // 当 _routerRoot 指向 Vue 实例时就终止循环
    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      // 处理 keep-alive 组件
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth

    // render previous view if the tree is inactive and kept-alive
    // 渲染缓存的 keep-alive 组件
    if (inactive) {
      return h(cache[name], data, children)
    }
    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]
    // attach instance registration hook
    // this will be called in the instance's injected lifecycle hooks
    // 添加注册钩子, 钩子会被注入到组件的生命周期钩子中
    // 在 src/install.js, 会在 beforeCreate 钩子中调用
    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
      }
    }
    // also register instance in prepatch hook
    // in case the same component instance is reused across different routes
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }
    // resolve props
    let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
    if (propsToPass) {
      // clone to prevent mutation
      propsToPass = data.props = extend({}, propsToPass)
      // pass non-declared props as attrs
      const attrs = data.attrs = data.attrs || {}
      for (const key in propsToPass) {
        if (!component.props || !(key in component.props)) {
          attrs[key] = propsToPass[key]
          delete propsToPass[key]
        }
      }
    }

    return h(component, data, children)
  }
}

function resolveProps (route, config) {
  switch (typeof config) {
    case 'undefined':
      return
    case 'object':
      return config
    case 'function':
      return config(route)
    case 'boolean':
      return config ? route.params : undefined
    default:
      if (process.env.NODE_ENV !== 'production') {
        warn(
          false,
          `props in "${route.path}" is a ${typeof config}, ` +
          `expecting an object, function or boolean.`
        )
      }
  }
}

function extend (to, from) {
  for (const key in from) {
    to[key] = from[key]
  }
  return to
}
复制代码

然后是RouterLink组件:

源码位置:/src/components/link.js

/* @flow */

import { createRoute, isSameRoute, isIncludedRoute } from '../util/route'
import { _Vue } from '../install'

// work around weird flow bug
const toTypes: Array<Function> = [String, Object]
const eventTypes: Array<Function> = [String, Array]

export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
    // 获取挂载的VueRouter实例
    const router = this.$router
    // 获取当前的路由对象
    const current = this.$route
    // 获取当前匹配的路由信息
    const { location, route, href } = router.resolve(this.to, current, this.append)

    const classes = {}
    const globalActiveClass = router.options.linkActiveClass
    const globalExactActiveClass = router.options.linkExactActiveClass
    // Support global empty active class
    const activeClassFallback = globalActiveClass == null
      ? 'router-link-active'
      : globalActiveClass
    const exactActiveClassFallback = globalExactActiveClass == null
      ? 'router-link-exact-active'
      : globalExactActiveClass
    const activeClass = this.activeClass == null
      ? activeClassFallback
      : this.activeClass
    const exactActiveClass = this.exactActiveClass == null
      ? exactActiveClassFallback
      : this.exactActiveClass
    const compareTarget = location.path
      ? createRoute(null, location, null, router)
      : route

    classes[exactActiveClass] = isSameRoute(current, compareTarget)
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget)

    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location)
        } else {
          router.push(location)
        }
      }
    }
    
    // 事件绑定
    const on = { click: guardEvent }
    if (Array.isArray(this.event)) {
      this.event.forEach(e => { on[e] = handler })
    } else {
      on[this.event] = handler
    }

    const data: any = {
      class: classes
    }

    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // find the first <a> child and apply listener and href
      // 找到第一个 <a> 给予这个元素事件绑定和href属性
      const a = findAnchor(this.$slots.default)
      if (a) {
        // in case the <a> is a static node
        a.isStatic = false
        const extend = _Vue.util.extend
        const aData = a.data = extend({}, a.data)
        aData.on = on
        const aAttrs = a.data.attrs = extend({}, a.data.attrs)
        aAttrs.href = href
      } else {
        // doesn't have <a> child, apply listener to self
        // 没有 <a> 的话就给当前元素自身绑定事件
        data.on = on
      }
    }

    return h(this.tag, data, this.$slots.default)
  }
}

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
}

function findAnchor (children) {
  if (children) {
    let child
    for (let i = 0; i < children.length; i++) {
      child = children[i]
      if (child.tag === 'a') {
        return child
      }
      if (child.children && (child = findAnchor(child.children))) {
        return child
      }
    }
  }
}
复制代码

结语

到这里,vue-router的源码剖析就告一段落了,虽然没有逐行去理解作者的思想,但也算是整体上捋顺了项目的运行原理,理解了原理也就更方便我们日常的需求开发了。最后,谢谢大家喜欢。

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