3

博客原文

两种路由模式的基本原理

用过 vue-router 就知道它提供了两种模式,hashhistory ,通过 Router 构建选项 mode
) 可进行配置。
简单理解 SPA 中的前端路由就是:

  1. 利用一些现有的 API 实现 url 的改变,但不触发浏览器主动加载新的 url,新的页面展示逻辑全部交给 js 控制;
  2. 给 history 中添加记录,以实现页面的后退前进;

前端路由下面通过两个例子了解一下这两种路由最基本的原理。

hash 模式

hash 模式是通过修改 URL.hash (即 url 中 # 标识符后的内容)来实现的。
URL.hash 的改变不会触发浏览器加载页面,但会主动修改 history 记录。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>router-hash</title>
</head>

<body>
  // 页面跳转修改 hash
  <a href="#/home">Home</a>
  <a href="#/about">About</a>
  <div id="app"></div>
</body>
<script>
  // 页面加载完后根据 hash 显示页面内容
  window.addEventListener('load', () => {
    app.innerHTML = location.hash.slice(1)
  })
  // 监听 hash 改变后修改页面显示内容
  window.addEventListener('hashchange', () => {
    app.innerHTML = location.hash.slice(1)
  })
</script>

</html>

history 模式

history 模式 主要原理是使用了浏览器的 history API,主要是 history.pushState()history.replaceState() 两个方法。

通过这两种方法修改 history 的 url 记录时,浏览器不会检查并加载新的 url 。

这两个方法都是接受三个参数:

  1. 状态对象 -- 可以用来暂存一些数据
  2. 标题 -- 暂无效 一般写空字符串
  3. url -- 新的历史 url 记录

两个方法的区别是 replaceState() 仅修改当前记录而非新建。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>router-history</title>
</head>

<body>
  // 点击后调用 go 函数跳转路由
  <a onclick="go('/home')">Home</a>
  <a onclick="go('/about')">About</a>
  <div id="app"></div>
</body>
<script>
  // 修改 history 记录及页面内容
  function go(pathname) {
    history.pushState(null, '', pathname)
    app.innerHTML = pathname
  }
  // 监听浏览器的前进后退并修改页面内容
  window.addEventListener('popstate', () => {
    app.innerHTML = location.pathname
  })
</script>

</html>

手写一个超简易的 VueRouter

看源码之前,先通过一个简易的 VueRouter 了解一下整体的结构和逻辑。

class HistoryRoute {
  constructor() {
    this.current = null
  }
}

class VueRouter {
  constructor(opts) {
    this.mode = opts.mode || 'hash';
    this.routes = opts.routes || [];
    // 创建路由映射表
    this.routesMap = this.creatMap(this.routes);
    // 记录当前展示的路由
    this.history = new HistoryRoute();
    this.init();
  }
  // 初始化 动态修改 history.current
  init() {
    if (this.mode === 'hash') {
      location.hash ? '' : location.hash = '/';
      window.addEventListener('load', () => {
        this.history.current = location.hash.slice(1);
      });
      window.addEventListener('hashchange', () => {
        this.history.current = location.hash.slice(1);
      });
    } else {
      location.pathname ? '' : location.pathname = '/';
      window.addEventListener('load', () => {
        this.history.current = location.pathname;
      });
      window.addEventListener('popstate', () => {
        this.history.current = location.pathname;
      })
    }
  }
  // 创建路由映射表
  // {
  //   '/': HomeComponent,
  //   '/about': AboutCompontent
  // }
  creatMap(routes) {
    return routes.reduce((memo, current) => {
      memo[current.path] = current.component;
      return memo;
    }, {})
  }
}
// Vue.use(Router) 时触发
VueRouter.install = function (Vue) {
  // 定义 $router $route 属性
  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this.$root._router;
    }
  });
  Object.defineProperty(Vue.prototype, '$route', {
    get() {
      return this.$root._route;
    }
  });
  // 全局混入 beforeCreate 钩子函数
  Vue.mixin({
    beforeCreate() {
      // 通过 this.$options.router 判断为根实例
      if (this.$options && this.$options.router) {
        this._router = this.$options.router;
        // 给 this 对象定义一个响应式 属性
        // https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js
        Vue.util.defineReactive(this, '_route', this._router.history);
      }
    },
  });
  // 渲染函数 & JSX  https://cn.vuejs.org/v2/guide/render-function.html
  // 注册全局组件 router-link
  // 默认渲染为 a 标签
  Vue.component('router-link', {
    props: {
      to: String,
      tag: String
    },
    methods: {
      handleClick() {
        const mode = this._self.$root._router.mode;
        location.href = mode === 'hash' ? `#${this.to}` : this.to;
      }
    },
    render: function (h) {
      const mode = this._self.$root._router.mode;
      const tag = this.tag || 'a';
      return (
        <tag
          on-click={ tag !== 'a' && this.handleClick }
          href={ mode === 'hash' ? `#${this.to}` : this.to }
        >
          { this.$slots.default }
        </tag>
      );
    }
  });
  // 注册全局组件 router-view
  // 根据 history.current 从 路由映射表中获取到对象组件并渲染
  Vue.component('router-view', {
    render: function (h) {
      const current = this._self.$root._route.current;
      const routeMap = this._self.$root._router.routesMap;
      return h(routeMap[current]);
    }
  });
}

export default VueRouter;

120 行代码实现了最最基本的 VueRouter,梳理一下整体的结构:

  1. 首先是一个 VueRouter 类,并有一个 install 方法,install 方法会在使用 Vue.use(VueRouter) 时被调用;
  2. install 方法中添加了 Vue 原型对象上的两个属性 $router $routerouter-view router-link 两个全局组件;
  3. VueRouter 类中通过构造函数处理传入的参数,生成路由映射表并调用 init 方法;
  4. init 方法中监听路由变化并改变 history.current;
  5. history.current 表示当前路由,在 install 中被定义为了一个响应式属性 _route ,在该属性被改变后会触发依赖中的响应已达到渲染 router-view 中的组件;

现在已经对 VueRouter 有了一个基本的认识了,再去看源码时就容易了一些。

浅尝源码

下面是我自己看 VueRouter 源码并结合一些文章的学习笔记。

阅读源码的过程中写了一些方便理解的注释,希望给大家阅读源码带来帮助,github: vue-router 源码

vue-router 的 src 目录如下,下面依次来分析这里主要的几个文件的作用。

vue-router

index.js

VueRouter 的入口文件,主要作用是定义并导出了一个 VueRouter 类。
下面是 index.js 源码,删除了 flow 相关的类型定义及函数的具体实现,先来看一下整体的结构和每部分的功能。

// ...
// 导出 VueRouter 类
export default class VueRouter {
  // 定义类的静态属性及方法
  // install 用于 vue 的插件机制,Vue.use 时会自动调用 install 方法
  static install: () => void;
  static version: string;

  // 构造函数 用于处理实例化时传入的参数
  constructor (options) {}
  // 获取到路由路径对应的组件实例
  match ( raw, current, redirectedFrom ) {}
  // 返回 history.current 当前路由路径
  get currentRoute () {}

  // 存入根组件实例,并监听路由的改变
  init (app) {}

  // 注册一些全局钩子函数
  beforeEach (fn) {} // 全局前置守卫
  beforeResolve (fn) {} // 全局解析守卫
  afterEach (fn) {} // 全局后置钩子
  onReady (cb, errorCb) {} // 路由完成初始导航时调用
  onError (errorCb) {} // 路由导航过程中出错时被调用

  // 注册一些 history 导航函数
  push (location, onComplete, onAbort) {}
  replace (location, onComplete, onAbort) {}
  go (n) {}
  back () {}
  forward () {}

  // 获取路由对应的组件
  getMatchedComponents (to) {}
  // 解析路由表
  resolve (to, current, append) {}
  // 添加路由表  并自动跳转到首页
  addRoutes (routes) {}
}

// 注册钩子函数,push 存入数组
function registerHook (list, fn) {}
// 根据模式(hash / history)拼接 location.href
function createHref (base, fullPath, mode) {}

// 挂载静态属性及方法
VueRouter.install = install
VueRouter.version = '__VERSION__'

// 浏览器环境下且 window.Vue 存在则自动调用 Vue.use 注册该路由插件
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

下面看一些主要方法的具体实现。

constructor

VueRouter 构造函数,主要做了 3 件事:

  1. 初始化传入的参数 options ,即调用 new VueRouter() 时传入的参数;
  2. 创建 match 匹配函数,用于匹配当前 path 或 name 对应的路由组件;
  3. 根据不同模式生成 history 实例,history 实例提供一些跳转、监听等方法;

关于 history 实例 及 match 匹配函数后面会讲到。

constructor(options = {}) {
  this.app = null // 根组件实例,在 init 中获取并赋值
  this.apps = [] // 保存多个根组件实例,在 init 中被添加
  this.options = options // 传入配置项参数
  this.beforeHooks = [] // 初始化全局前置守卫
  this.resolveHooks = [] // 初始化全局解析守卫
  this.afterHooks = [] // 初始化全局后置钩子
  this.matcher = createMatcher(options.routes || [], this) // 创建 match 匹配函数

  let mode = options.mode || 'hash' // 默认 hash 模式
  // history 浏览器环境不支持时向下兼容使用 hash 模式
  this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
  if (this.fallback) {
    mode = 'hash'
  }
  // 非浏览器环境强制使用 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}`)
      }
  }
}

init

install.js 中,init 函数会在根组件实例的 beforeCreate 生命周期函数里调用,传入根组件实例。

// 传入根组件实例
init(app) {
  // 非生产环境进行未安装路由的断言报错提示
  process.env.NODE_ENV !== 'production' && assert(
    install.installed,
    `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
    `before creating root instance.`
  )
  // 保存该根组件实例
  this.apps.push(app)

  // 设置 app 销毁程序
  // https://github.com/vuejs/vue-router/issues/2639
  app.$once('hook:destroyed', () => {
    // 当销毁时,将 app 从 this.apps 数组中清除,防止内存溢出
    const index = this.apps.indexOf(app)
    if (index > -1) this.apps.splice(index, 1)
    // ensure we still have a main app or null if no apps
    // we do not release the router so it can be reused
    if (this.app === app) this.app = this.apps[0] || null
  })

  // app 已初始化则直接返回
  if (this.app) {
    return
  }

  this.app = app

  // 跳转到当前路由
  const history = this.history

  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation())
  } else if (history instanceof HashHistory) {
    const setupHashListener = () => {
      history.setupListeners()
    }
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener,
      setupHashListener
    )
  }
  // 设置路由监听,路由改变时改变 _route 属性,表示当前路由
  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}

install.js

该文件主要是定义并导出一个 install 方法,在 Vue.use(VueRouter) 时被调用。
install 方法主要做了这几件事:

  1. 通过全局混入 beforeCreate 钩子函数的方式,为每个 vue 组件实例添加了指向同一个路由实例的 _routerRoot 属性,使得每个组件中都可以获取到路由信息及方法。
  2. Vue.prototype 上挂载 $router $route 两个属性,分别表示路由实例及当前路由。
  3. 全局注册 router-link router-view 组件。
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
  // install 函数中将 Vue 赋值给 _Vue 
  // 可在其他模块中不用引入直接使用 Vue 对象
  _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)
    }
  }
  // 每个组件混入 beforeCreate 钩子函数的实现
  Vue.mixin({
    beforeCreate () {
      // 判断是否存在 router 对象,若存在则为根实例
      if (isDef(this.$options.router)) {
        // 设置根路由
        this._routerRoot = this
        this._router = this.$options.router
        // 路由初始化,将根实例传入 VueRouter 的 init 方法
        this._router.init(this)
        // _router 属性双向绑定
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 非根实例则通过 $parent 指向父级的 _routerRoot 属性,最终指向根实例
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
  // 注入 $router $route
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
  // 全局注册 router-link router-view组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

create-route-map.js

create-route-map.js 文件导出一个 createRouteMap 方法,用于创建路由根据 path 和 name 的映射表。

// ...
// 创建路由 map
export function createRouteMap(routes, oldPathList, oldPathMap, oldNameMap) {
  // pathList 用于控制路径匹配优先级
  const pathList = oldPathList || []
  // 根据 path 的路由映射表
  const pathMap = oldPathMap || Object.create(null)
  // 根据 name 的路由映射表
  const nameMap = oldNameMap || Object.create(null)
  // 遍历路由配置添加到 pathList pathMap nameMap 中
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })
  // 将通配符路由 * 取出插到末尾,确保通配符路由始终在尾部
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }
  return {
    pathList,
    pathMap,
    nameMap
  }
}

function addRouteRecord(pathList, pathMap, nameMap, route, parent, matchAs) {}

function compileRouteRegex(path, pathToRegexpOptions) {}

function normalizePath(path, parent, strict) {}

addRouteRecord

createRouteMap 函数中最重要的一步就是 遍历路由配置并添加到映射表中 的 addRouteRecord 函数。
addRouteRecord 函数作用是生成两个映射表,PathMap 和 NameMap,分别可以通过 path 和 name 查询到对应的路由记录对象,路由记录对象包含 meta、props、及最重要的 components 视图组件实例用于渲染在 router-view 组件中。

// 增加 路由记录 函数
function addRouteRecord(pathList, pathMap, nameMap, route, parent, matchAs) {
  // 获取 path, name
  const { path, name } = route
  // 编译正则的选项
  const pathToRegexpOptions = route.pathToRegexpOptions || {}
  // 格式化 path
  // 根据 pathToRegexpOptions.strict 判断是否删除末尾斜杠 /
  // 根据是否以斜杠 / 开头判断是否需要拼接父级路由的路径
  const normalizedPath = normalizePath(
    path,
    parent,
    pathToRegexpOptions.strict // 末尾斜杠是否精确匹配 (default: false)
  )
  // 匹配规则是否大小写敏感?(默认值:false)
  // 路由配置中 caseSensitive 和 pathToRegexpOptions.sensitive 作用相同
  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, // alias 匹配的路由记录 path 为别名,需根据 matchAs 匹配
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props: route.props == null ? {} : route.components ?
      route.props : {
        default: route.props
      }
  }
  if (route.children) {
    // 如果是命名路由,没有重定向,并且有默认子路由,则发出警告。
    // 如果用户通过 name 导航路由跳转则默认子路由将不会渲染
    // https://github.com/vuejs/vue-router/issues/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.`
        )
      }
    }
    // 递归路由配置的 children 属性,添加路由记录
    route.children.forEach(child => {
      // 别名匹配时真正的 path 为 matchAs
      const childMatchAs = matchAs ?
        cleanPath(`${matchAs}/${child.path}`) :
        undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
  // 处理别名 alias 逻辑 增加对应的 记录
  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
  }
  // 命名路由添加记录
  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}" }`
      )
    }
  }
}

递归子路由中还有一个警告,对于命名路由且有默认子路由时在开发环境给出提示。
这个提示用于避免一个 bug,具体可以看一下对应的 issue
简单的说就是当命名路由有默认子路由时

routes: [{ 
  path: '/home', 
  name: 'home',
  component: Home,
  children: [{
    path: '',
    name: 'home.index',
    component: HomeIndex
  }]
}]

使用 to="/home" 会跳转到 HomeIndex 默认子路由,而使用 :to="{ name: 'home' }" 则只会跳转到 Home 并不会显示HomeIndex 默认子路由。
通过上面 addRouteRecord 函数源码就能知道这两种跳转方式 path 和 name 表现不同的原因了:
因为通过 path 和 name 是分别从两个映射表查找对应路由记录的,
pathMap 生成过程中是先递归子路由,如上例,当添加该子路由的路由记录时,key 就是 /home ,子路由添加完后父路由添加时判断 /home 已存在则不会添加进 pathMap。
而 nameMap 的 key 是 name,home 对应的就是 Home 组件,home.index 对应 HomeIndex。

create-matcher.js

createMatcher 函数根据路由配置调用 createRouteMap 方法建立映射表,并提供了 匹配路由记录 match 及 添加路由记录 addRoutes 两个方法。

addRoutes 用于动态添加路由配置;
match 用于根据传入的 location 和 路由对象 返回一个新的路由对象;

// 参数 routes 表示创建 VueRouter 传入的 routes 配置信息
// router 表示 VueRouter 实例
export function createMatcher(routes, router) {
  // 创建路由映射表
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  function addRoutes(routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  // 路由匹配
  function match(raw, currentRoute, redirectedFrom) {
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location

    if (name) {
      // 命名路由处理
      // 合并 location 及 record 的数据并返回一个新的路由对象
    } else if (location.path) {
      // 普通路由处理
      // 合并 location 及 record 的数据并返回一个新的路由对象
    }
    // 没有匹配到路由记录则返回一个空的路由对象
    return _createRoute(null, location)
  }

  function redirect(record, location) {
    // ...
  }

  function alias(record, location, matchAs) {
    // ...
  }

  // 根据条件创建不同的路由
  function _createRoute(record, location, redirectedFrom) {
    // 处理 重定向 redirect
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    // 处理 别名 alias
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    return createRoute(record, location, redirectedFrom, router)
  }

  return {
    match,
    addRoutes
  }
}

history/base.js

history/base.js 中定义了一个 History 类,主要的作用是:
路由变化时通过调用 transitionTo 方法以获取到对应的路由记录并依次执行一系列守卫钩子函数;

export class History {
  constructor (router, base) {
    this.router = router
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
  }
  listen (cb) {
    this.cb = cb
  }
  onReady (cb, errorCb) {
    if (this.ready) {
      cb()
    } else {
      this.readyCbs.push(cb)
      if (errorCb) {
        this.readyErrorCbs.push(errorCb)
      }
    }
  }
  onError (errorCb) {
    this.errorCbs.push(errorCb)
  }
  // 切换路由,在 VueRouter 初始化及监听路由改变时会触发
  transitionTo (location, onComplete, onAbort) {
    // 获取匹配的路由信息
    const route = this.router.match(location, this.current)
    // 确认切换路由
    this.confirmTransition(route, () => {
      // 以下为切换路由成功或失败的回调
      // 更新路由信息,对组件的 _route 属性进行赋值,触发组件渲染
      // 调用 afterHooks 中的钩子函数
      this.updateRoute(route)
      // 添加 hashchange 监听
      onComplete && onComplete(route)
      // 更新 URL
      this.ensureURL()
      // 只执行一次 ready 回调
      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) })
      }
    })
  }
  // 确认切换路由
  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
    ) {
      this.ensureURL()
      return abort()
    }

    // 通过对比路由解析出可复用的组件,需要渲染的组件,失活的组件
    const { updated, deactivated, activated } = resolveQueue(this.current.matched, route.matched)
    // 导航守卫数组
    const queue = [].concat(
      // 失活的组件钩子
      extractLeaveGuards(deactivated),
      // 全局 beforeEach 钩子
      this.router.beforeHooks,
      // 在当前路由改变,但是该组件被复用时调用
      extractUpdateHooks(updated),
      // 需要渲染组件 enter 守卫钩子
      activated.map(m => m.beforeEnter),
      // 解析异步路由组件
      resolveAsyncComponents(activated)
    )
    // 保存路由
    this.pending = route
    // 迭代器,用于执行 queue 中的导航守卫钩子
    const iterator = (hook, next) => {
      // 路由不相等就不跳转路由
      if (this.pending !== route) {
        return abort()
      }
      try {
        // 执行钩子
        hook(route, current, (to) => {
          // 只有执行了钩子函数中的 next,才会继续执行下一个钩子函数
          // 否则会暂停跳转
          // 以下逻辑是在判断 next() 中的传参
          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('/') 或者 next({ path: '/' }) -> 重定向
            abort()
            if (typeof to === 'object' && to.replace) {
              this.replace(to)
            } else {
              this.push(to)
            }
          } else {
            // 这里执行 next
            // 也就是执行下面函数 runQueue 中的 step(index + 1)
            // confirm transition and pass on the value
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }
    // 同步执行异步函数
    runQueue(queue, iterator, () => {
      const postEnterCbs = []
      const isValid = () => this.current === route
      // 当所有异步组件加载完成后,会执行这里的回调,也就是 runQueue 中的 cb()
      // 接下来执行 需要渲染组件的导航守卫钩子
      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() })
          })
        }
      })
    })
  }
  updateRoute (route) {
    const prev = this.current
    this.current = route
    this.cb && this.cb(route)
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }
}

参考

  1. vue-router 源码分析 - 整体流程
  2. 前端进阶之道 - VueRouter 源码解析
  3. MDN History_API
  4. 一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构
  5. vue-router源码阅读学习

ST_Pace
565 声望21 粉丝

Set The Pace