稍微學(xué)一下 VueRouter 原理

vue.jpg

博客原文

兩種路由模式的基本原理

用過 vue-router 就知道它提供了兩種模式鞋吉,hashhistory 奄毡,通過 Router 構(gòu)建選項 mode 可進行配置折欠。
簡單理解 SPA 中的前端路由就是:

  1. 利用一些現(xiàn)有的 API 實現(xiàn) url 的改變,但不觸發(fā)瀏覽器主動加載新的 url吼过,新的頁面展示邏輯全部交給 js 控制锐秦;
  2. 給 history 中添加記錄,以實現(xiàn)頁面的后退前進盗忱;

前端路由下面通過兩個例子了解一下這兩種路由最基本的原理酱床。

hash 模式

hash 模式是通過修改 URL.hash (即 url 中 # 標(biāo)識符后的內(nèi)容)來實現(xiàn)的。
URL.hash 的改變不會觸發(fā)瀏覽器加載頁面趟佃,但會主動修改 history 記錄扇谣。

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

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

<body>
  // 頁面跳轉(zhuǎn)修改 hash
  <a href="#/home">Home</a>
  <a href="#/about">About</a>
  <div id="app"></div>
</body>
<script>
  // 頁面加載完后根據(jù) hash 顯示頁面內(nèi)容
  window.addEventListener('load', () => {
    app.innerHTML = location.hash.slice(1)
  })
  // 監(jiān)聽 hash 改變后修改頁面顯示內(nèi)容
  window.addEventListener('hashchange', () => {
    app.innerHTML = location.hash.slice(1)
  })
</script>

</html>

history 模式

history 模式 主要原理是使用了瀏覽器的 history API昧捷,主要是 history.pushState()history.replaceState() 兩個方法。

通過這兩種方法修改 history 的 url 記錄時罐寨,瀏覽器不會檢查并加載新的 url 靡挥。

這兩個方法都是接受三個參數(shù):

  1. 狀態(tài)對象 -- 可以用來暫存一些數(shù)據(jù)
  2. 標(biāo)題 -- 暫無效 一般寫空字符串
  3. url -- 新的歷史 url 記錄

兩個方法的區(qū)別是 replaceState() 僅修改當(dāng)前記錄而非新建。

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

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

<body>
  // 點擊后調(diào)用 go 函數(shù)跳轉(zhuǎn)路由
  <a onclick="go('/home')">Home</a>
  <a onclick="go('/about')">About</a>
  <div id="app"></div>
</body>
<script>
  // 修改 history 記錄及頁面內(nèi)容
  function go(pathname) {
    history.pushState(null, '', pathname)
    app.innerHTML = pathname
  }
  // 監(jiān)聽瀏覽器的前進后退并修改頁面內(nèi)容
  window.addEventListener('popstate', () => {
    app.innerHTML = location.pathname
  })
</script>

</html>

手寫一個超簡易的 VueRouter

看源碼之前鸯绿,先通過一個簡易的 VueRouter 了解一下整體的結(jié)構(gòu)和邏輯跋破。

以下代碼的 git 地址:simple-vue-router

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

class VueRouter {
  constructor(opts) {
    this.mode = opts.mode || 'hash';
    this.routes = opts.routes || [];
    // 創(chuàng)建路由映射表
    this.routesMap = this.creatMap(this.routes);
    // 記錄當(dāng)前展示的路由
    this.history = new HistoryRoute();
    this.init();
  }
  // 初始化 動態(tài)修改 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;
      })
    }
  }
  // 創(chuàng)建路由映射表
  // {
  //   '/': HomeComponent,
  //   '/about': AboutCompontent
  // }
  creatMap(routes) {
    return routes.reduce((memo, current) => {
      memo[current.path] = current.component;
      return memo;
    }, {})
  }
}
// Vue.use(Router) 時觸發(fā)
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 鉤子函數(shù)
  Vue.mixin({
    beforeCreate() {
      // 通過 this.$options.router 判斷為根實例
      if (this.$options && this.$options.router) {
        this._router = this.$options.router;
        // 給 this 對象定義一個響應(yīng)式 屬性
        // https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js
        Vue.util.defineReactive(this, '_route', this._router.history);
      }
    },
  });
  // 渲染函數(shù) & JSX  https://cn.vuejs.org/v2/guide/render-function.html
  // 注冊全局組件 router-link
  // 默認渲染為 a 標(biāo)簽
  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
  // 根據(jù) 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 行代碼實現(xiàn)了最最基本的 VueRouter,梳理一下整體的結(jié)構(gòu):

  1. 首先是一個 VueRouter 類瓶蝴,并有一個 install 方法毒返,install 方法會在使用 Vue.use(VueRouter) 時被調(diào)用;
  2. install 方法中添加了 Vue 原型對象上的兩個屬性 $router $routerouter-view router-link 兩個全局組件囊蓝;
  3. VueRouter 類中通過構(gòu)造函數(shù)處理傳入的參數(shù)饿悬,生成路由映射表并調(diào)用 init 方法;
  4. init 方法中監(jiān)聽路由變化并改變 history.current聚霜;
  5. history.current 表示當(dāng)前路由狡恬,在 install 中被定義為了一個響應(yīng)式屬性 _route ,在該屬性被改變后會觸發(fā)依賴中的響應(yīng)已達到渲染 router-view 中的組件蝎宇;

現(xiàn)在已經(jīng)對 VueRouter 有了一個最最基本的認識了弟劲,再去看源碼時就容易了一些。

淺嘗源碼

下面是我自己看 VueRouter 源碼并結(jié)合一些文章的學(xué)習(xí)筆記姥芥。

閱讀源碼的過程中寫了一些方便理解的注釋兔乞,希望給大家閱讀源碼帶來幫助,github: vue-router 源碼

vue-router 的 src 目錄如下凉唐,下面依次來分析這里主要的幾個文件的作用庸追。

vue-router

index.js

VueRouter 的入口文件,主要作用是定義并導(dǎo)出了一個 VueRouter 類台囱。
下面是 index.js 源碼淡溯,刪除了 flow 相關(guān)的類型定義及函數(shù)的具體實現(xiàn),先來看一下整體的結(jié)構(gòu)和每部分的功能簿训。

// ...
// 導(dǎo)出 VueRouter 類
export default class VueRouter {
  // 定義類的靜態(tài)屬性及方法
  // install 用于 vue 的插件機制咱娶,Vue.use 時會自動調(diào)用 install 方法
  static install: () => void;
  static version: string;

  // 構(gòu)造函數(shù) 用于處理實例化時傳入的參數(shù)
  constructor (options) {}
  // 獲取到路由路徑對應(yīng)的組件實例
  match ( raw, current, redirectedFrom ) {}
  // 返回 history.current 當(dāng)前路由路徑
  get currentRoute () {}

  // 存入根組件實例,并監(jiān)聽路由的改變
  init (app) {}

  // 注冊一些全局鉤子函數(shù)
  beforeEach (fn) {} // 全局前置守衛(wèi)
  beforeResolve (fn) {} // 全局解析守衛(wèi)
  afterEach (fn) {} // 全局后置鉤子
  onReady (cb, errorCb) {} // 路由完成初始導(dǎo)航時調(diào)用
  onError (errorCb) {} // 路由導(dǎo)航過程中出錯時被調(diào)用

  // 注冊一些 history 導(dǎo)航函數(shù)
  push (location, onComplete, onAbort) {}
  replace (location, onComplete, onAbort) {}
  go (n) {}
  back () {}
  forward () {}

  // 獲取路由對應(yīng)的組件
  getMatchedComponents (to) {}
  // 解析路由表
  resolve (to, current, append) {}
  // 添加路由表  并自動跳轉(zhuǎn)到首頁
  addRoutes (routes) {}
}

// 注冊鉤子函數(shù)强品,push 存入數(shù)組
function registerHook (list, fn) {}
// 根據(jù)模式(hash / history)拼接 location.href
function createHref (base, fullPath, mode) {}

// 掛載靜態(tài)屬性及方法
VueRouter.install = install
VueRouter.version = '__VERSION__'

// 瀏覽器環(huán)境下且 window.Vue 存在則自動調(diào)用 Vue.use 注冊該路由插件
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

下面看一些主要方法的具體實現(xiàn)膘侮。

constructor

VueRouter 構(gòu)造函數(shù),主要做了 3 件事:

  1. 初始化傳入的參數(shù) options 的榛,即調(diào)用 new VueRouter() 時傳入的參數(shù)琼了;
  2. 創(chuàng)建 match 匹配函數(shù),用于匹配當(dāng)前 path 或 name 對應(yīng)的路由組件困曙;
  3. 根據(jù)不同模式生成 history 實例表伦,history 實例提供一些跳轉(zhuǎn)谦去、監(jiān)聽等方法慷丽;

關(guān)于 history 實例 及 match 匹配函數(shù)后面會講到蹦哼。

constructor(options = {}) {
  this.app = null // 根組件實例,在 init 中獲取并賦值
  this.apps = [] // 保存多個根組件實例要糊,在 init 中被添加
  this.options = options // 傳入配置項參數(shù)
  this.beforeHooks = [] // 初始化全局前置守衛(wèi)
  this.resolveHooks = [] // 初始化全局解析守衛(wèi)
  this.afterHooks = [] // 初始化全局后置鉤子
  this.matcher = createMatcher(options.routes || [], this) // 創(chuàng)建 match 匹配函數(shù)

  let mode = options.mode || 'hash' // 默認 hash 模式
  // history 瀏覽器環(huán)境不支持時向下兼容使用 hash 模式
  this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
  if (this.fallback) {
    mode = 'hash'
  }
  // 非瀏覽器環(huán)境強制使用 abstract 模式
  if (!inBrowser) {
    mode = 'abstract'
  }
  this.mode = mode

  // 根據(jù)不同模式生成 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 函數(shù)會在根組件實例的 beforeCreate 生命周期函數(shù)里調(diào)用,傳入根組件實例锄俄。

// 傳入根組件實例
init(app) {
  // 非生產(chǎn)環(huán)境進行未安裝路由的斷言報錯提示
  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)

  // 設(shè)置 app 銷毀程序
  // https://github.com/vuejs/vue-router/issues/2639
  app.$once('hook:destroyed', () => {
    // 當(dāng)銷毀時局劲,將 app 從 this.apps 數(shù)組中清除,防止內(nèi)存溢出
    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

  // 跳轉(zhuǎn)到當(dāng)前路由
  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
    )
  }
  // 設(shè)置路由監(jiān)聽奶赠,路由改變時改變 _route 屬性鱼填,表示當(dāng)前路由
  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}

install.js

該文件主要是定義并導(dǎo)出一個 install 方法,在 Vue.use(VueRouter) 時被調(diào)用毅戈。
install 方法主要做了這幾件事:

  1. 通過全局混入 beforeCreate 鉤子函數(shù)的方式苹丸,為每個 vue 組件實例添加了指向同一個路由實例的 _routerRoot 屬性,使得每個組件中都可以獲取到路由信息及方法苇经。
  2. Vue.prototype 上掛載 $router $route 兩個屬性赘理,分別表示路由實例及當(dāng)前路由晃酒。
  3. 全局注冊 router-link router-view 組件啤咽。
import View from './components/view'
import Link from './components/link'

export let _Vue

export function install (Vue) {
  // 若已調(diào)用過則直接返回
  if (install.installed && _Vue === Vue) return
  install.installed = true
  // install 函數(shù)中將 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 鉤子函數(shù)的實現(xiàn)
  Vue.mixin({
    beforeCreate () {
      // 判斷是否存在 router 對象席覆,若存在則為根實例
      if (isDef(this.$options.router)) {
        // 設(shè)置根路由
        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 文件導(dǎo)出一個 createRouteMap 方法宇立,用于創(chuàng)建路由根據(jù) path 和 name 的映射表黔帕。

// ...
// 創(chuàng)建路由 map
export function createRouteMap(routes, oldPathList, oldPathMap, oldNameMap) {
  // pathList 用于控制路徑匹配優(yōu)先級
  const pathList = oldPathList || []
  // 根據(jù) path 的路由映射表
  const pathMap = oldPathMap || Object.create(null)
  // 根據(jù) 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 函數(shù)中最重要的一步就是 遍歷路由配置并添加到映射表中 的 addRouteRecord 函數(shù)鄙信。
addRouteRecord 函數(shù)作用是生成兩個映射表瞪醋,PathMap 和 NameMap,分別可以通過 path 和 name 查詢到對應(yīng)的路由記錄對象扮碧,路由記錄對象包含 meta趟章、props、及最重要的 components 視圖組件實例用于渲染在 router-view 組件中慎王。

// 增加 路由記錄 函數(shù)
function addRouteRecord(pathList, pathMap, nameMap, route, parent, matchAs) {
  // 獲取 path, name
  const { path, name } = route
  // 編譯正則的選項
  const pathToRegexpOptions = route.pathToRegexpOptions || {}
  // 格式化 path
  // 根據(jù) pathToRegexpOptions.strict 判斷是否刪除末尾斜杠 /
  // 根據(jù)是否以斜杠 / 開頭判斷是否需要拼接父級路由的路徑
  const normalizedPath = normalizePath(
    path,
    parent,
    pathToRegexpOptions.strict // 末尾斜杠是否精確匹配 (default: false)
  )
  // 匹配規(guī)則是否大小寫敏感蚓土?(默認值:false)
  // 路由配置中 caseSensitive 和 pathToRegexpOptions.sensitive 作用相同
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }

  // 路由記錄 對象
  const record = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    // 若非命名視圖組件,則設(shè)為默認視圖組件
    components: route.components || {
      default: route.component
    },
    instances: {},
    name,
    parent,
    matchAs, // alias 匹配的路由記錄 path 為別名赖淤,需根據(jù) matchAs 匹配
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props: route.props == null ? {} : route.components ?
      route.props : {
        default: route.props
      }
  }
  if (route.children) {
    // 如果是命名路由蜀漆,沒有重定向,并且有默認子路由咱旱,則發(fā)出警告确丢。
    // 如果用戶通過 name 導(dǎo)航路由跳轉(zhuǎn)則默認子路由將不會渲染
    // 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 邏輯 增加對應(yīng)的 記錄
  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}" }`
      )
    }
  }
}

遞歸子路由中還有一個警告,對于命名路由且有默認子路由時在開發(fā)環(huán)境給出提示鲜侥。
這個提示用于避免一個 bug褂始,具體可以看一下對應(yīng)的 issue
簡單的說就是當(dāng)命名路由有默認子路由時

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

使用 to="/home" 會跳轉(zhuǎn)到 HomeIndex 默認子路由,而使用 :to="{ name: 'home' }" 則只會跳轉(zhuǎn)到 Home 并不會顯示HomeIndex 默認子路由描函。
通過上面 addRouteRecord 函數(shù)源碼就能知道這兩種跳轉(zhuǎn)方式 path 和 name 表現(xiàn)不同的原因了:
因為通過 path 和 name 是分別從兩個映射表查找對應(yīng)路由記錄的崎苗,
pathMap 生成過程中是先遞歸子路由,如上例舀寓,當(dāng)添加該子路由的路由記錄時胆数,key 就是 /home ,子路由添加完后父路由添加時判斷 /home 已存在則不會添加進 pathMap互墓。
而 nameMap 的 key 是 name必尼,home 對應(yīng)的就是 Home 組件,home.index 對應(yīng) HomeIndex篡撵。

create-matcher.js

createMatcher 函數(shù)根據(jù)路由配置調(diào)用 createRouteMap 方法建立映射表判莉,并提供了 匹配路由記錄 match 及 添加路由記錄 addRoutes 兩個方法。

addRoutes 用于動態(tài)添加路由配置酸休;
match 用于根據(jù)傳入的 location 和 路由對象 返回一個新的路由對象骂租;

// 參數(shù) routes 表示創(chuàng)建 VueRouter 傳入的 routes 配置信息
// router 表示 VueRouter 實例
export function createMatcher(routes, router) {
  // 創(chuàng)建路由映射表
  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 的數(shù)據(jù)并返回一個新的路由對象
    } else if (location.path) {
      // 普通路由處理
      // 合并 location 及 record 的數(shù)據(jù)并返回一個新的路由對象
    }
    // 沒有匹配到路由記錄則返回一個空的路由對象
    return _createRoute(null, location)
  }

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

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

  // 根據(jù)條件創(chuàng)建不同的路由
  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 類,主要的作用是:
路由變化時通過調(diào)用 transitionTo 方法以獲取到對應(yīng)的路由記錄并依次執(zhí)行一系列守衛(wèi)鉤子函數(shù)斑司;

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 初始化及監(jiān)聽路由改變時會觸發(fā)
  transitionTo (location, onComplete, onAbort) {
    // 獲取匹配的路由信息
    const route = this.router.match(location, this.current)
    // 確認切換路由
    this.confirmTransition(route, () => {
      // 以下為切換路由成功或失敗的回調(diào)
      // 更新路由信息,對組件的 _route 屬性進行賦值宿刮,觸發(fā)組件渲染
      // 調(diào)用 afterHooks 中的鉤子函數(shù)
      this.updateRoute(route)
      // 添加 hashchange 監(jiān)聽
      onComplete && onComplete(route)
      // 更新 URL
      this.ensureURL()
      // 只執(zhí)行一次 ready 回調(diào)
      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
    // 中斷跳轉(zhuǎn)路由函數(shù)
    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)
    }
    // 如果是相同的路由就不跳轉(zhuǎn)
    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()
    }

    // 通過對比路由解析出可復(fù)用的組件互站,需要渲染的組件,失活的組件
    const { updated, deactivated, activated } = resolveQueue(this.current.matched, route.matched)
    // 導(dǎo)航守衛(wèi)數(shù)組
    const queue = [].concat(
      // 失活的組件鉤子
      extractLeaveGuards(deactivated),
      // 全局 beforeEach 鉤子
      this.router.beforeHooks,
      // 在當(dāng)前路由改變僵缺,但是該組件被復(fù)用時調(diào)用
      extractUpdateHooks(updated),
      // 需要渲染組件 enter 守衛(wèi)鉤子
      activated.map(m => m.beforeEnter),
      // 解析異步路由組件
      resolveAsyncComponents(activated)
    )
    // 保存路由
    this.pending = route
    // 迭代器胡桃,用于執(zhí)行 queue 中的導(dǎo)航守衛(wèi)鉤子
    const iterator = (hook, next) => {
      // 路由不相等就不跳轉(zhuǎn)路由
      if (this.pending !== route) {
        return abort()
      }
      try {
        // 執(zhí)行鉤子
        hook(route, current, (to) => {
          // 只有執(zhí)行了鉤子函數(shù)中的 next,才會繼續(xù)執(zhí)行下一個鉤子函數(shù)
          // 否則會暫停跳轉(zhuǎn)
          // 以下邏輯是在判斷 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 {
            // 這里執(zhí)行 next
            // 也就是執(zhí)行下面函數(shù) runQueue 中的 step(index + 1)
            // confirm transition and pass on the value
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }
    // 同步執(zhí)行異步函數(shù)
    runQueue(queue, iterator, () => {
      const postEnterCbs = []
      const isValid = () => this.current === route
      // 當(dāng)所有異步組件加載完成后磕潮,會執(zhí)行這里的回調(diào)翠胰,也就是 runQueue 中的 cb()
      // 接下來執(zhí)行 需要渲染組件的導(dǎo)航守衛(wèi)鉤子
      const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
      const queue = enterGuards.concat(this.router.resolveHooks)
      // 隊列中的函數(shù)都執(zhí)行完畢,就執(zhí)行回調(diào)函數(shù)
      runQueue(queue, iterator, () => {
        // 跳轉(zhuǎn)完成
        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. 一張思維導(dǎo)圖輔助你深入了解 Vue | Vue-Router | Vuex 源碼架構(gòu)
  5. vue-router源碼閱讀學(xué)習(xí)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末自脯,一起剝皮案震驚了整個濱河市之景,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌膏潮,老刑警劉巖锻狗,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡轻纪,警方通過查閱死者的電腦和手機油额,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刻帚,“玉大人潦嘶,你說我怎么就攤上這事∥依蓿” “怎么了衬以?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵缓艳,是天一觀的道長校摩。 經(jīng)常有香客問我,道長阶淘,這世上最難降的妖魔是什么衙吩? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮溪窒,結(jié)果婚禮上坤塞,老公的妹妹穿的比我還像新娘。我一直安慰自己澈蚌,他們只是感情好摹芙,可當(dāng)我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宛瞄,像睡著了一般浮禾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上份汗,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天盈电,我揣著相機與錄音,去河邊找鬼杯活。 笑死匆帚,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的旁钧。 我是一名探鬼主播吸重,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼歪今!你這毒婦竟也來了嚎幸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤彤委,失蹤者是張志新(化名)和其女友劉穎鞭铆,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡车遂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年封断,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舶担。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡坡疼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出衣陶,到底是詐尸還是另有隱情柄瑰,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布剪况,位于F島的核電站教沾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏译断。R本人自食惡果不足惜授翻,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一孙咪、第九天 我趴在偏房一處隱蔽的房頂上張望堪唐。 院中可真熱鬧,春花似錦翎蹈、人聲如沸淮菠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽合陵。三九已至,卻和暖如春逞力,著一層夾襖步出監(jiān)牢的瞬間曙寡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工寇荧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留举庶,地道東北人。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓揩抡,卻偏偏與公主長得像户侥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子峦嗤,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 本文由淺入深觀摩vue-router源碼是如何通過hash與History interface兩種方式實現(xiàn)前端路由...
    world_7735閱讀 1,331評論 0 10
  • 隨著前端應(yīng)用的業(yè)務(wù)功能起來越復(fù)雜烁设,用戶對于使用體驗的要求越來越高替梨,單面(SPA)成為前端應(yīng)用的主流形式钓试。大型單頁應(yīng)...
    指尖跳動閱讀 1,401評論 0 0
  • 隨著前端應(yīng)用的業(yè)務(wù)功能起來越復(fù)雜,用戶對于使用體驗的要求越來越高副瀑,單面(SPA)成為前端應(yīng)用的主流形式弓熏。大型單頁應(yīng)...
    bayi_lzp閱讀 5,766評論 0 2
  • SPA單頁應(yīng)用 傳統(tǒng)的項目大多使用多頁面結(jié)構(gòu)狈孔,需要切換內(nèi)容的時候我們往往會進行單個html文件的跳轉(zhuǎn)信认,這個時候受網(wǎng)...
    視覺派Pie閱讀 11,843評論 1 55
  • 早上起來,打開手機寫簡書均抽,意外的發(fā)現(xiàn)嫁赏,我的文章被收錄進了專題名叫工作生活 馬上整個人從床上彈起...
    杏聯(lián)何博匯閱讀 339評論 3 1