Расшифровать vue-router: начать с источника

исходный код JavaScript Vue.js vue-router

Несколько дней назад автор увидел вопрос: Вы действительно понимаете vue-router? Вы знаете, как работает vue-router? Помня об этой проблеме, автор начал исследование исходного кода vue-router. Эта статья не углубляется в исходный код построчно, а следует блок-схеме, нарисованной автором, для краткого анализа текущего процесса каждого шага.

Анализ запущенного процесса

Автор заранее нарисовал блок-схему в соответствии со структурой исходного кода и моим собственным пониманием.На первый взгляд эта блок-схема может быть немного запутанной.Далее автор будет анализировать работающий поток по этой картинке, и затем шаг за шагом Основная часть парсинга исходного кода.

运行流程
Чтобы помочь нам понять этот запуск блок-схемы, мы выполним распечатку vue-router примера монтирования Vue, чтобы увидеть, что все добавляет:
1
2

  • под $optionsrouterОбъект легко понять, это экземпляр vue-router, который мы смонтировали при создании экземпляра Vue;
  • _routeЭто отзывчивый объект маршрутизации. Этот объект будет хранить нашу информацию о маршрутизации. Он реагирует через Vue.util.defineReactive, предоставленный Vue. Следующие get и set являются перехватом данных для него;
  • _routerСохраняется объект vue-router, который мы получили из $options;
  • _routerRootУкажите на наш корневой узел Vue;
  • _routerViewCacheнаш кеш представления;
  • $routeа также$routerдва геттера, определенные в Vue.prototype. Первый указывает на _route под _routerRoot, последний указывает на _router под _routerRoot

Далее, давайте сгладим эту «ослепительную картинку», чтобы позже мы могли лучше понять анализ исходного кода.

Прежде всего, мы установили vue-router в соответствии с механизмом плагинов Vue, который на самом деле здесь очень прост.Инкапсулирует миксин, определяет два «прототипа» и регистрирует два компонента.. В этом миксине хук beforeCreate вызывается, чтобы определить, говорил ли экземпляр vue-router, и инициализировать логику, связанную с маршрутизацией, как упоминалось выше._routerRoot、_router、_routeопределяется в это время. Определение двух «прототипов» означает определение одного или двух геттеров в Vue.prototype, то есть$route和$router. Регистрация двух компонентов означает регистрацию двух компонентов RouterView и RouterLink, которые мы будем использовать позже.

Затем мы создали экземпляр VueRouter и смонтировали его на экземпляре Vue.В это время конструктор в экземпляре VueRouter инициализировал различные очереди ловушек, инициализировал сопоставитель для выполнения нашей логики сопоставления маршрутов и создания объектов маршрутов, инициализировал историю для выполнения логика перехода и выполнение очереди ловушек.

Еще одна вещь, которую делает beforeCreate в миксине, — это выполнение метода init() нашего экземпляра VueRouter для выполнения инициализации.Этот набор процессов аналогичен процессу нажатия на RouteLink или маршрутизации функционального управления.Я расскажу об этом здесь. В методе init вызывается метод transitionTo объекта истории, а затем с помощью match получаются соответствующие данные текущего маршрута и создается новый маршрут объекта маршрута, а затем объект маршрута используется для выполнения метода confirmTransition для выполнить события в очереди ловушек. и, наконец, обновить текущий объект, который хранит текущие данные маршрутизации, через updateRoute, указывая на маршрут объекта маршрутизации, который мы только что создали.

В начале мы сказали_routeопределяется как отзывчивый, то после обновления маршрута_routeОбъект получит ответ и уведомит RouteView об обновлении представления.

На этом процесс завершен, далее мы углубимся в исходный код vue-router, чтобы подробно изучить его принципы.

Проанализируйте исходный код

скажи это прямо

Исходный код vue-router использует поток в качестве проверки типа. Если поток не настроен, экран может быть заполнен ошибками. Эта статья не представляет слишком много потока. Чтобы облегчить ваше понимание, я удалю синтаксис, связанный с потоком, в разделе исходного кода ниже. Кстати, прикрепите немного потока, связанного с:

Официальная документация Flow (требуется научный доступ в Интернет): https://flow.org/ Начало работы с потоком: https://zhuanlan.zhihu.com/p/26204569 Конфигурация потока: https://zhuanlan.zhihu.com/p/24649359

Структура проекта

Получая исходный код проекта, мы сначала должны посмотреть на его структуру каталогов:

3
Где src — часть исходного кода нашего проекта, содержащая следующую структуру:

  • компонент представляет собой два компонента RouterLink и Router View;
  • create-matcher.js — это входной файл для создания соответствия;
  • create-route-map.js используется для создания списков путей, карт путей, карт имен и т. д.;
  • история — это логика создания класса истории;
  • 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, а метод статической установки был смонтирован в классе VueRouter. Также считается, что если 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, который экспортирует метод экспорта для 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: это позволяет использовать некоторые API-интерфейсы, предоставляемые Vue, без упаковки всего Vue.Конечно, предпосылкой этого является то, что vue-router должен быть установлен и смонтирован;
  • Определите два геттера в Vue.prototype: все компоненты Vue являются расширениями экземпляра Vue, и все они могут обращаться к методам и свойствам прототипа;
  • Определите отзывчивый объект _route: с помощью этого отзывчивого объекта маршрутизации вы можете уведомить RouterView о своевременном обновлении компонентов при обновлении маршрута;

Создание экземпляра VueRouter

Далее давайте посмотрим на создание экземпляра класса VueRouter.В конструкторе есть две основные вещи: создание сопоставления и создание истории:

Местоположение источника:/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}`)
        }
    }
  }
  // ...
}

Создать сопоставление

Следуя задумке, давайте сначала посмотрим на функцию 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 создаст карту с соответствующими отношениями через createRouteMap в соответствии с конфигурацией маршрутов, определенной при инициализации экземпляра VueRouter.Конкретная логика будет обсуждаться ниже. Затем верните сопоставление объекта, содержащее два метода: match и addRoutes, которые представляют собой подробную логику реализации сопоставления маршрутов. Он вернет соответствующий объект маршрута, а 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 создает соответствующие записи маршрутизации в соответствии с путем, псевдонимом и именем, настроенными маршрутами пользователя.

создать историю

Эта часть матчера закончена. Далее следует поговорить об инстанцировании History. Из исходного кода в папке history находятся 4 файла. Base — это базовый класс, а остальные три наследуют этот базовый класс для обработки vue- маршрутизатор соответственно.В различных ситуациях режима, здесь мы в основном смотрим на логику базы.

// 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(/\/$/, '')
}

После того, как основное монтирование и различные инстанцирования завершены, мы можем начать с инициализации, чтобы увидеть последующий процесс.

5
Когда я говорил об установке раньше, я знал, что init выполняется в хуке beforeCreate в миксине Теперь мы переходим к методу init VueRouter.

Местоположение источника:/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
      )
    }
    //...
  }
// ...

Видно, что инициализация в основном предназначена для присвоения значения приложению, а для HTML5History и HashHistory выполняется специальная обработка, потому что в этих двух режимах возможно, что запись не является страницей по умолчанию, и ее нужно основывать в адресной строке текущего браузера.Путь или хэш для активации соответствующего маршрута, что достигается вызовом 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)
    })
  }

Логика кажется сложной, но на самом деле это обратная обработка различных функций ловушек, но здесь следует отметить, что каждый объект маршрута будет иметь атрибут matchd, который содержит запись маршрутизации, которая генерируется в create-matcher. .js уже упоминалось в .

Подождите, кажется, мы что-то упустили, после инициализации не хватает еще одной вещи:

Местоположение источника:/src/index.js

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

Функция обратного вызова после изменения маршрута устанавливается здесь, она будет вызываться в обратном вызове onComplete в подтверждении перехода, и текущее значение _route будет обновлено.Как мы упоминали ранее, _route является отзывчивым, поэтому при его обновлении он будет to Уведомляет компонент о повторном рендеринге.

два компонента

После прочтения общего процесса мы можем рассмотреть два компонента.Давайте сначала посмотрим на компонент 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 подошел к концу.Хотя нет построчного понимания мыслей автора, его можно считать общим принципом работы проекта.Понимание принципа будет сделать его более удобным для наших повседневных потребностей развития. . Наконец, спасибо, что понравилось.