Анализ исходного кода VueRouter

Vue.js
Анализ исходного кода VueRouter

image

благодарный

funfish, Сыграй призрака в своем сердце, Демистификация технологии Vue.jsстатья которая мне помогла

предисловие

Исходного кода vue-router не много, но и контента не так уж и мало. В этой статье не рассказывается о построчном анализе, но постараемся подробно объяснить основной процесс и принципы. Обработка некоторых служебных функций и краевых условий будет пропущена, потому что я не понял их построчно, извините.

Предварительные знания

Прежде чем мы изучим исходный код VueRouter, давайте рассмотрим знания, связанные с хешем и историей. Для получения более подробной информации см.документация mdn, этот раздел взят из документации mdn.

hash

onhashchange

Событие hashchange запускается при изменении идентификатора фрагмента URL-адреса (часть URL-адреса, следующая за знаком #, включая знак #). Обратите внимание, что histroy.pushState() никогда не запускает событие hashchange, даже если новый URL отличается от старого URL только хэшированием.

histroy

pushState

pushState() принимает три параметра: объект состояния, заголовок (в настоящее время игнорируется) и URL-адрес.

  • состояние, состояние объекта состояния является объектом JavaScript. Когда срабатывает событие popstate, объект будет передан в функцию обратного вызова.
  • заголовок, в настоящее время игнорируется всеми браузерами
  • url, новая запись URL

replaceState

Использование history.replaceState() очень похоже на history.pushState(), разница в том, что replaceState() изменяет текущий элемент истории, а не создает новый.

onpopstate

Вызов history.pushState() или history.replaceState() не приведет к запуску события popstate.Событие popstate будет запускаться только при определенных действиях браузера, таких как нажатие кнопок «назад» и «вперед» (или вызов history.back(), history в JavaScript ) методы .forward(), history.go()).

Если текущая активная запись истории была создана методом history.pushState() или изменена методом history.replaceState(), свойство состояния объекта события popstate содержит копию объекта состояния записи истории.

Инициализация приложения

Обычно при создании приложения Vue мы используем Vue.use для установки VueRouter в качестве плагина. В то же время экземпляр маршрутизатора будет смонтирован на экземпляре Vue.

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

let a = new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/about',
      name: 'about',
      component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
    }
  ]
})

Установка плагина

В документации Vue указаноПлагины Vue.js должны иметь публичный метод install. Первый параметр этого метода — конструктор Vue, а второй параметр — необязательный объект параметров., мы сначала смотрим на файл install.js в исходном коде.

В установочном файле мы инициализируем некоторые частные свойства экземпляра Vue.

  • _routerRoot указывает на экземпляр Vue
  • _router указывает на экземпляр VueRouter

Некоторые геттеры инициализируются на прототипе Vue.

  • $ маршрутизатор, текущий экземпляр маршрутизатора
  • $ Route, текущая информация Router

И миксин микшируется глобально, и компоненты RouterView и RouterLink прописаны глобально.


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

export let _Vue

export function install (Vue) {
  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 () {
      // 判断是否实例是否挂载了router
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        // _router, 劫持的是当前的路由
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

  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
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

Vue.util.defineReactive, это метод, с помощью которого наблюдатель в Vue перехватывает данные, перехватывает _route, и когда _route запускает метод установки, он уведомляет зависимые компоненты.И RouterView должен получить доступ к parent.$route, чтобы он формировал зависимость(мы увидим позже)

👀 Давайте взглянем на исходный код defineReactive в Vue: в defineReactive Object.defineProperty будет использоваться для захвата метода set для _route. Наблюдатели уведомляются при установке.



Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    // ...
  },
  set: function reactiveSetter (newVal) {
    // ...
    childOb = !shallow && observe(newVal)
    dep.notify()
  }
})

Экземпляр VueRouter


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'
    // fallback会在不支持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}`)
        }
    }
  }
}

matcher

Объект matcher содержит два свойства: addRoutes, match.

pathList, pathMap, nameMap

pathList, pathMap, nameMap — это список путей, карта объектов пути и маршрута и карта имен маршрута и объектов маршрута соответственно. Цель vue-router поддерживает динамическую маршрутизацию, pathList, pathMap, nameMap можно динамически изменять после инициализации. Они создаются методом createRouteMap, давайте взглянем на исходный код createRouteMap.


export function createRouteMap (
  routes,
  oldPathList,
  oldPathMap,
  oldNameMap
) {
  // pathList,pathMap,nameMap支持后续的动态添加
  const pathList: Array<string> = oldPathList || []
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  // 遍历路由列表
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })

  // 将通配符的路径, push到pathList的末尾
  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,
  pathMap,
  nameMap,
  route,
  parent,
  matchAs
) {
  const { path, name } = route
 
  const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}

  // normalizePath, 会对path进行格式化
  // 会删除末尾的/,如果route是子级,会连接父级和子级的path,形成一个完整的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 }
  }

  // 如果route存在children, 我们会递归的创建路由对象
  // 递归的创建route对象
  if (route.children) {
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  // 这里是对路由别名的处理
  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
      )
    })
  }

  // 填充pathMap,nameMap,pathList
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

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

addRoutes

Динамически добавлять больше правил маршрутизации и динамически изменять pathList, pathMap, nameMap

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

match

Метод match ищет соответствующий Route в nameMap по параметрам raw (которые могут быть строкой или объектом Location) и currentRoute (текущий объект route возвращает объект Route) и возвращает его.

Если локация содержит name, я нахожу соответствующий Route через nameMap, но в это время путь может содержать параметры, поэтому мы заполним параметры в патч через функцию fillParams и вернем реальный путь path.


function match (
  raw,
  currentRoute,
  redirectedFrom
) {
  // 会对raw,currentRoute处理,返回格式化后path, hash, 以及params
  const location = normalizeLocation(raw, currentRoute, false, router)

  const { name } = location

  if (name) {
    const record = nameMap[name]
    if (!record) return _createRoute(null, location)
    
    // 获取所有必须的params。如果optional为true说明params不是必须的
    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) {
      // 使用params对path进行填充返回一个真实的路径
      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      // 创建Route对象
      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]
      // 使用pathList中的每一个regex,对path进行匹配
      if (matchRoute(record.regex, location.path, location.params)) {
        return _createRoute(record, location, redirectedFrom)
      }
    }
  }
  return _createRoute(null, location)
}

Давайте продолжим смотреть, что делается в _createRoute.


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

Среди них перенаправление и псевдоним в конечном итоге вызовут метод createRoute. Давайте снова обратимся к функции createRoute. Функция createRoute возвращает замороженный объект Router.

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


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

init

в этом. Будет смонтирован обратный вызов cb, связанный с рендерингом RouteView. В соответствии с текущим URL-адресом мы завершаем инициализацию маршрута в хуке жизненного цикла beforeCreate корневого экземпляра Vue и завершаем навигацию по первому маршруту.


init (app) {

  // app为Vue的实例
  this.apps.push(app)

  if (this.app) {
    return
  }

  // 在VueRouter上挂载app属性
  this.app = app

  const history = this.history

  // 初始化当前的路由,完成第一次导航,在hash模式下会在transitionTo的回调中调用setupListeners
  // setupListeners里会对hashchange事件进行监听
  // transitionTo是进行路由导航的函数,我们将会在下面介绍
  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation())
  } else if (history instanceof HashHistory) {
    const setupHashListener = () => {
      history.setupListeners()
    }
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener,
      setupHashListener
    )
  }

  // 挂载了回调的cb, 每次更新路由更好更新_route
  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}

history

Есть три режима истории: хэш, история, абстракция, и эти три класса наследуются от базового класса.

base

Давайте сначала посмотрим на конструктор base, где router — это экземпляр VueRouter, а base — это базовый путь маршрута. current — текущий маршрут, по умолчанию «/», ready — состояние маршрута, readyCbs — набор готовых обратных вызовов, а readyErrorCbs — обратный вызов при сбое raday. errorCbs Коллекция обратных вызовов для ошибок навигации.


export class History {
  constructor (router: Router, base: ?string) {
    this.router = router
    // normalizeBase会对base路径做出格式化的处理,会为base开头自动添加‘/’,删除结尾的‘/’,默认返回’/‘
    this.base = normalizeBase(base)
    // 初始化的当前路由对象
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
  }
}

export const START = createRoute(null, {
  path: '/'
})

function normalizeBase (base: ?string): string {
  if (!base) {
    // inBrowser判断是否为浏览器环境
    if (inBrowser) {
      const baseEl = document.querySelector('base')
      base = (baseEl && baseEl.getAttribute('href')) || '/'
      base = base.replace(/^https?:\/\/[^\/]+/, '')
    } else {
      base = '/'
    }
  }
  if (base.charAt(0) !== '/') {
    base = '/' + base
  }
  return base.replace(/\/$/, '')
}

Метод listen в базе будет использоваться в методе инициализации VueRouter, а listen добавит обратный вызов для каждого обновления маршрута.


listen (cb: Function) {
  this.cb = cb
}   

В базовом классе есть и другие методы, такие как transitionTo, confirmTransition, updateRoute, которые используются в базовом подклассе. Вскоре мы рассмотрим их конкретную реализацию в hashrouter.

HashRouter

Конструктор

в конструкторе HashHistory. Мы определим, верен ли текущий запасной вариант. Если это правда, используйте checkFallback, добавьте '#' и замените документ на window.location.replace.

Если резервное копирование ложно, мы вызовем sureSlash, sureSlash добавит «#» для URL-адресов без «#» и заменит документ API-интерфейсом histroy или заменит.

Поэтому, когда мы посещаем 127.0.0.1, он будет автоматически заменен на 127.0.0.1/#/


export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // 如果是回退hash的情况,并且判断当前路径是否有/#/。如果没有将会添加'/#/'
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }
}

checkFallback


// 检查url是否包含‘/#/’
function checkFallback (base) {
  // 获取hash值
  const location = getLocation(base)
  // 如果location不是以/#,开头。添加/#,使用window.location.replace替换文档
  if (!/^\/#/.test(location)) {
    window.location.replace(
      cleanPath(base + '/#' + location)
    )
    return true
  }
}
// 返回hash
export function getLocation (base) {
  let path = decodeURI(window.location.pathname)
  if (base && path.indexOf(base) === 0) {
    path = path.slice(base.length)
  }
  return (path || '/') + window.location.search + window.location.hash
}

// 删除 //, 替换为 /
export function cleanPath (path) {
  return path.replace(/\/\//g, '/')
}

ensureSlash


function ensureSlash (): boolean {
  // 判断是否包含#,并获取hash值。如果url没有#,则返回‘’
  const path = getHash()
  // 判断path是否以/开头
  if (path.charAt(0) === '/') {
    return true
  }
  // 如果开头不是‘/’, 则添加/
  replaceHash('/' + path)
  return false
}
// 获取“#”后面的hash
export function getHash (): string {
  const href = window.location.href
  const index = href.indexOf('#')
  return index === -1 ? '' : decodeURI(href.slice(index + 1))
}
function replaceHash (path) {
  // supportsPushState判断是否存在history的API
  // 使用replaceState或者window.location.replace替换文档
  // getUrl获取完整的url
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
// getUrl返回了完整了路径,并且会添加#, 确保存在/#/
function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

В replaceHash мы вызываем метод replaceState, а в методе replaceState мы вызываем метод pushState. В pushState мы вызовем метод saveScrollPosition, который будет записывать информацию о текущей позиции прокрутки. Затем используйте histroyAPI или window.location.replace для обновления документа.


export function replaceState (url?: string) {
  pushState(url, true)
}

export function pushState (url?: string, replace?: boolean) {
  // 记录当前的x轴和y轴,以发生导航的时间为key,位置信息记录在positionStore中
  saveScrollPosition()
  const history = window.history
  try {
    if (replace) {
      history.replaceState({ key: _key }, '', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

push, replace,

Мы объединяем push и replace, потому что исходный код, который они реализуют, похож. В push and replace вызывается метод transitionTo.Метод transitionTo находится в базе базового класса.Теперь давайте развернёмся и посмотрим на исходный код transitionTo(👇 в следующих двух разделах,Код не сложный, но callback вложенный callback, как мёд, выглядит отвратительно)


push (location, onComplete, onAbort) {
  const { current: fromRoute } = this
  this.transitionTo(
    location,
    route => {
      pushHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    },
    onAbort
  )
}

replace (location, onComplete, onAbort) {
  const { current: fromRoute } = this
  this.transitionTo(
    location,
    route => {
      replaceHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    },
    onAbort
  )
}

transitionTo, confirmTransition, updateRoute

image

Параметр location для transitionTo — это наш целевой путь, который может быть строкой или объектом RawLocation. Мы передаем метод router.match (который мы представили в matcher), и router.match вернет наш целевой объект маршрута. Далее мы вызовем функцию подтверждения перехода.


transitionTo (location, onComplete, onAbort) {
  const route = this.router.match(location, this.current)
  this.confirmTransition(
    route,
    () => {
      // ...
    },
    err => {
      // ...
    }
  )
}

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


if (
  isSameRoute(route, current) &&
  route.matched.length === current.matched.length
) {
  this.ensureURL()
  return abort()
}

Затем мы вызываем метод resolveQueue.solveQueue принимает текущий маршрут и соответствующий атрибут целевого маршрута в качестве параметров.Как работает resolveQueue, можно показать на следующем рисунке. Мы сравним маршруты двух массивов один за другим, найдем маршруты, которые нужно уничтожить, которые нужно обновить, которые нужно активировать, и вернем их (потому что нам нужно реализовать их разные охранники маршрутов)

image

function resolveQueue (
  current
  next
) {
  let i
  // 依次比对当前的路由和目标的路由的matched属性中的每一个路由
  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)
  }
}

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

На первом этапе мы используем функцию extractLeaveGuards для извлечения охранников «beforeRouteLeave» во всех деактивированных компонентах, которые необходимо уничтожить. Функция extractLeaveGuards вызовет функцию extractGuards, функция extractGuards вызовет функцию flatMapComponents, а функция flatMapComponents будет просматривать записи (resolveQueue возвращает деактивированный), в процессе обхода мы передаем компонент, экземпляр компонента и объект маршрута в fn(Обратный вызов передается в flatMapComponents в extractGuards.), в fn мы получим охрану beforeRouteLeave в компоненте.


// 返回每一个组件中导航的集合
function extractLeaveGuards (deactivated) {
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

function extractGuards (
  records,
  name,
  bind,
  reverse?
) {
  const guards = flatMapComponents(
    records,
    // def为组件
    // instance为组件的实例
    (def, instance, match, key) => {
      // 返回每一个组件中定义的路由守卫
      const guard = extractGuard(def, name)
      if (guard) {
        // bindGuard函数确保了guard(路由守卫)的this指向的是Component中的实例
        return Array.isArray(guard)
          ? guard.map(guard => bind(guard, instance, match, key))
          : bind(guard, instance, match, key)
      }
    }
  )
  // 返回导航的集合
  return flatten(reverse ? guards.reverse() : guards)
}

export function flatMapComponents (
  matched,
  fn
) {
  // 遍历matched,并返回matched中每一个route中的每一个Component
  return flatten(matched.map(m => {
    // 如果没有设置components则默认是components{ default: YouComponent },可以从addRouteRecord函数中看到
    // 将每一个matched中所有的component传入fn中
    // m.components[key]为components中的key键对应的组件
    // m.instances[key]为组件的实例,这个属性是在routerview组件中beforecreated中被赋值的
    return Object.keys(m.components).map(key => fn(
      m.components[key],
      m.instances[key],
      m,
      key
    ))
  }))
}

// 返回一个新数组
export function flatten (arr) {
  return Array.prototype.concat.apply([], arr)
}

// 获取组件中的属性
function extractGuard (def, key) {
  if (typeof def !== 'function') {
    def = _Vue.extend(def)
  }
  return def.options[key]
}

// 修正函数的this指向
function bindGuard (guard, instance) {
  if (instance) {
    return function boundRouteGuard () {
      return guard.apply(instance, arguments)
    }
  }
}

Второй шаг — получить защиту глобального объекта VueRouter перед каждым

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

Четвертый шаг, получите охранник beforeEach в конфигурации активированных опций.

Часть 5, получить все асинхронные компоненты


После получения всех охранников маршрута мы определяем итератор iterator. Затем мы используем runQueue для обхода очереди. Передайте каждый элемент в очереди в fn(итератор итератор), защита маршрутизации будет выполняться в итераторе, а метод next должен быть явно вызван в защите маршрутизации, чтобы войти в следующий конвейер и войти в следующую итерацию. После завершения итерации будет выполнен обратный вызов runQueue.

В обратном вызове runQueue мы получаем защиту, которая активирует beforeRouteEnter в компоненте, и сохраняем следующий обратный вызов в защите beforeRouteEnter в postEnterCbs, и проходим через postEnterCbs для выполнения следующего обратного вызова после подтверждения навигации.

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

// queue为路由守卫的队列
// fn为定义的迭代器
export function runQueue (queue, fn, cb) {
  const step = index => {
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        // 使用迭代器处理每一个钩子
        // fn是迭代器
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}

// 迭代器
const iterator = (hook, next) => {
  if (this.pending !== route) {
    return abort()
  }
  try {
    // 传入路由守卫三个参数,分别分别对应to,from,next
    hook(route, current, (to: any) => {
      if (to === false || isError(to)) {
        // 如果next的参数为false
        this.ensureURL(true)
        abort(to)
      } else if (
        // 如果next需要重定向到其他路由
        typeof to === 'string' ||
        (typeof to === 'object' && (
          typeof to.path === 'string' ||
          typeof to.name === 'string'
        ))
      ) {
        abort()
        if (typeof to === 'object' && to.replace) {
          this.replace(to)
        } else {
          this.push(to)
        }
      } else {
        // 进入下个管道
        next(to)
      }
    })
  } catch (e) {
    abort(e)
  }
}

runQueue(
  queue,
  iterator,
  () => {
    const postEnterCbs = []
    const isValid = () => this.current === route
    // 获取所有激活组件内部的路由守卫beforeRouteEnter,组件内的beforeRouteEnter守卫,是无法获取this实例的
    // 因为这时激活的组件还没有创建,但是我们可以通过传一个回调给next来访问组件实例。
    // beforeRouteEnter (to, from, next) {
    //   next(vm => {
    //     // 通过 `vm` 访问组件实例
    //   })
    // }
    const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
    // 获取全局的beforeResolve的路由守卫
    const queue = enterGuards.concat(this.router.resolveHooks)
    // 再一次遍历queue
    runQueue(queue, iterator, () => {
      // 完成过渡
      if (this.pending !== route) {
        return abort()
      }
      // 正在过渡的路由设置为null
      this.pending = null
      // 
      onComplete(route)
      // 导航被确认后,我们执行beforeRouteEnter守卫中,next的回调
      if (this.router.app) {
        this.router.app.$nextTick(() => {
          postEnterCbs.forEach(cb => { cb() })
        })
      }
    }
  )
})

// 获取组件中的beforeRouteEnter守卫
function extractEnterGuards (
  activated,
  cbs,
  isValid
) {
  return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
    // 这里没有修改guard(守卫)中this的指向
    return bindEnterGuard(guard, match, key, cbs, isValid)
  })
}

// 将beforeRouteEnter守卫中next的回调push到postEnterCbs中
function bindEnterGuard (
  guard,
  match,
  key,
  cbs,
  isValid
) {
  // 这里的next参数是迭代器中传入的参数
  return function routeEnterGuard (to, from, next) {
    return guard(to, from, cb => {
      // 执行迭代器中传入的next,进入下一个管道
      next(cb)
      if (typeof cb === 'function') {
        // 我们将next的回调包装后保存到cbs中,next的回调会在导航被确认的时候执行回调
        cbs.push(() => {
          poll(cb, match.instances, key, isValid)
        })
      }
    })
  }
}

В обратном вызове onComplete для confirmTransition мы вызываем метод updateRoute, параметр — это маршрут навигации. В updateRoute мы будем обновлять текущий маршрут (history.current) и выполнить cb(Обновите свойство _route в экземпляре Vue, это вызовет повторную визуализацию RouterView.)


updateRoute (route: Route) {
  const prev = this.current
  this.current = route
  this.cb && this.cb(route)
  // 执行after的钩子
  this.router.afterHooks.forEach(hook => {
    hook && hook(route, prev)
  })
}

Затем мы выполняем функцию обратного вызова onComplete для transitionTo. В обратном вызове вызывается метод replaceHash или pushHash. Они обновляют хэш местоположения. При совместимости с historyAPI будет использоваться history.replaceState или history.pushState. Если он не совместим с historyAPI, он будет использовать window.location.replace или window.location.hash. Метод handleScroll обновит положение нашей полосы прокрутки, здесь мы не будем вдаваться в подробности.


// replaceHash方法
(route) => {
  replaceHash(route.fullPath)
  handleScroll(this.router, route, fromRoute, false)
  onComplete && onComplete(route)
}

// push方法
route => {
  pushHash(route.fullPath)
  handleScroll(this.router, route, fromRoute, false)
  onComplete && onComplete(route)
}

Что ж, теперь мы закончили процесс метода replace или push.

🎉🎉🎉🎉🎉🎉 Ниже приведен полный код в transitionTo, confirmTransition. 🎉🎉🎉🎉🎉🎉


// onComplete 导航成功的回调
// onAbort 导航终止的回调
transitionTo (location, onComplete, onAbort) {
  const route = this.router.match(location, this.current)
  this.confirmTransition(route,
    () => {
      this.updateRoute(route)
      onComplete && onComplete(route)
      this.ensureURL()
      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) })
      }
    }
  )
}

// onComplete 导航成功的回调
// onAbort 导航终止的回调
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {

  // 当前的路由
  const current = this.current

  const abort = err => {
    if (isError(err)) {
      if (this.errorCbs.length) {
        this.errorCbs.forEach(cb => { cb(err) })
      }
    }
    onAbort && onAbort(err)
  }
  
  // 判断是否导航到相同的路由,如果是我们终止导航
  if (
    isSameRoute(route, current) &&
    route.matched.length === current.matched.length
  ) {
    this.ensureURL()
    return abort()
  }

  // 获取所有需要激活,更新,销毁的路由
  const {
    updated,
    deactivated,
    activated
  } = resolveQueue(this.current.matched, route.matched)

  // 获取所有需要执行的路由守卫
  const queue = [].concat(
    extractLeaveGuards(deactivated),
    this.router.beforeHooks,
    extractUpdateHooks(updated), 
    activated.map(m => m.beforeEnter),
    resolveAsyncComponents(activated)
  )

  this.pending = route

  // 定义迭代器
  const iterator = (hook: NavigationGuard, next) => {
    if (this.pending !== route) {
      return abort()
    }
    try {
      hook(route, current, (to: any) => {
        if (to === false || isError(to)) {
          this.ensureURL(true)
          abort(to)
        } else if (
          typeof to === 'string' ||
          (typeof to === 'object' && (
            typeof to.path === 'string' ||
            typeof to.name === 'string'
          ))
        ) {
          abort()
          if (typeof to === 'object' && to.replace) {
            this.replace(to)
          } else {
            this.push(to)
          }
        } else {
          next(to)
        }
      })
    } catch (e) {
      abort(e)
    }
  }

  // 迭代所有的路由守卫
  runQueue(
    queue,
    iterator, 
    () => {
      const postEnterCbs = []
      const isValid = () => this.current === route
      const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
      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) {
          this.router.app.$nextTick(() => {
            postEnterCbs.forEach(cb => { cb() })
          })
        }
      }
    )
  })
}

go, forward, back

Методы go, forward и back, определенные в VueRouter, — это все методы go, которые вызывают атрибут истории.

// index.js

go (n) {
  this.history.go(n)
}

back () {
  this.go(-1)
}

forward () {
  this.go(1)
}

Метод go в хэше вызывает history.go. Как он обновляет RouteView? Ответ заключается в том, что хеш-объект добавляет прослушиватель для событий popstate или hashchange в методе setupListeners. Обновление RoterView будет запущено при обратном вызове события.

// go方法调用history.go
go (n) {
  window.history.go(n)
}

setupListeners

Когда мы нажимаем кнопку «назад, вперед» или вызываем методы «назад, вперед, вперед». Мы не обновляем активно _app.route и current. Как мы запускаем обновление RouterView? Прослушивая события popstate или hashchange в окне. В обратном вызове события вызовите метод transitionTo, чтобы завершить обновление _route и current.

Или можно сказать, что при использовании методов push и replace обновление хэша происходит после обновления _route. При использовании go, back обновление хэша находится перед обновлением _route.


setupListeners () {
  const router = this.router

  const expectScroll = router.options.scrollBehavior
  const supportsScroll = supportsPushState && expectScroll

  if (supportsScroll) {
    setupScroll()
  }

  window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
    const current = this.current
    if (!ensureSlash()) {
      return
    }
    this.transitionTo(getHash(), route => {
      if (supportsScroll) {
        handleScroll(this.router, route, current, true)
      }
      if (!supportsPushState) {
        replaceHash(route.fullPath)
      }
    })
  })
}

HistoryRouter

Реализация HistoryRouter в основном такая же, как у HashRouter. Разница в том, что HistoryRouter не будет выполнять какую-либо обработку отказоустойчивости и не будет оценивать, поддерживает ли текущая среда historyAPI. Событие popstate прослушивается по умолчанию, а histroyAPI используется по умолчанию. Заинтересованные студенты могут увидеть определение HistoryRouter в файле /history/html5.js.

компоненты

RouterView

RouterView могут быть вложены друг в друга, RouterView зависит от родителя.route属性,parent.маршрут это._routerRoot._route. Мы используем Vue.util.defineReactive, чтобы сделать _router реактивным. _route обновляется в обратном вызове transitionTo, который запускает отрисовку RouteView. (Механизм рендеринга в настоящее время не очень хорошо понят, и я еще не видел исходный код Vue, и мачо плачет).

export default {
  name: 'RouterView',
  functional: true,
  // RouterView的name, 默认是default
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    data.routerView = true

    // h为渲染函数
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

    let depth = 0
    let inactive = false
    // 使用while循环找到Vue的根节点, _routerRoot是Vue的根实例
    // depth为当前的RouteView的深度,因为RouteView可以互相嵌套,depth可以帮组我们找到每一级RouteView需要渲染的组件
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth

    if (inactive) {
      return h(cache[name], data, children)
    }

    const matched = route.matched[depth]
    if (!matched) {
      cache[name] = null
      return h()
    }

    // 获取到渲染的组件
    const component = cache[name] = matched.components[name]

    // registerRouteInstance会在beforeCreated中调用,又全局的Vue.mixin实现
    // 在matched.instances上注册组件的实例, 这会帮助我们修正confirmTransition中执行路由守卫中内部的this的指向
    data.registerRouteInstance = (vm, val) => {
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }

    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }

    let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
    if (propsToPass) {
      propsToPass = data.props = extend({}, propsToPass)
      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)
  }
}

Эпилог

Мы прочитали исходный код VueRouter. В целом не очень сложно.Как правило, используйте Vue.util.defineReactive, чтобы установить для свойства _route экземпляра значение реактивного. Методы push и replace будут активно обновлять атрибут _route. И переход, назад или нажатие кнопок «вперед» и «назад» обновит _route в обратном вызове onhashchange или onpopstate, а обновление _route вызовет повторную визуализацию RoterView.

Но он также пропускает обработку поведения прокрутки, такого как keep-live. Я планирую реализовать упрощенную версию, основанную на основных принципах VueRouter.VueRouter, конечно, это еще не началось.

разное

Я изучал исходный код некоторых библиотек с середины до конца марта, и изучение самого исходного кода не очень помогает в моей работе. Потому что, как и VueRouter, Preact хорошо документирован. Просмотр исходного кода представляет собой чисто личный интерес, но изучение исходного кода этих библиотек и реализация простой версии самостоятельно — это большое чувство выполненного долга.

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

Простая реализация React