vue-router 源碼分析-history | 掘金技術徵文

做者:滴滴公共前端團隊 - dolymoodjavascript

上篇中介紹了 vue-router 的總體流程,可是具體的 history 部分沒有具體分析,本文就具體分析下和 history 相關的細節。html

初始化 Router

經過總體流程能夠知道在路由實例化的時候會根據當前 mode 模式來選擇實例化對應的History類,這裏再來回顧下,在 src/index.js 中:前端

// ...
import { HashHistory, getHash } from './history/hash'
import { HTML5History, getLocation } from './history/html5'
import { AbstractHistory } from './history/abstract'
// ...
export default class VueRouter {
// ...
  constructor (options: RouterOptions = {}) {
// ...
    // 默認模式是 hash
    let mode = options.mode || 'hash'
    // 若是設置的是 history 可是若是瀏覽器不支持的話 
    // 強制退回到 hash
    this.fallback = mode === 'history' && !supportsHistory
    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':
        // 細節 傳入了 fallback
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this)
        break
      default:
        assert(false, `invalid mode: ${mode}`)
    }
  }
// ...複製代碼

能夠看到 vue-router 提供了三種模式:hash(默認)、history 以及 abstract 模式,還不瞭解具體區別的能夠在文檔 中查看,有很詳細的解釋。下面就這三種模式初始化一一來進行分析。vue

HashHistory

首先就看默認的 hash 模式,也應該是用的最多的模式,對應的源碼在 src/history/hash.js 中:html5

// ...
import { History } from './base'
import { getLocation } from './html5'
import { cleanPath } from '../util/path'

// 繼承 History 基類
export class HashHistory extends History {
  constructor (router: VueRouter, base: ?string, fallback: boolean) {
    // 調用基類構造器
    super(router, base)

    // 若是說是從 history 模式降級來的
    // 須要作降級檢查
    if (fallback && this.checkFallback()) {
      // 若是降級 且 作了降級處理 則什麼也不須要作
      return
    }
    // 保證 hash 是以 / 開頭
    ensureSlash()
  }

  checkFallback () {
    // 獲得除去 base 的真正的 location 值
    const location = getLocation(this.base)
    if (!/^\/#/.test(location)) {
      // 若是說此時的地址不是以 /# 開頭的
      // 須要作一次降級處理 降級爲 hash 模式下應有的 /# 開頭
      window.location.replace(
        cleanPath(this.base + '/#' + location)
      )
      return true
    }
  }
// ...
}

// 保證 hash 以 / 開頭
function ensureSlash (): boolean {
  // 獲得 hash 值
  const path = getHash()
  // 若是說是以 / 開頭的 直接返回便可
  if (path.charAt(0) === '/') {
    return true
  }
  // 不是的話 須要手工保證一次 替換 hash 值
  replaceHash('/' + path)
  return false
}

export function getHash (): string {
  // 由於兼容性問題 這裏沒有直接使用 window.location.hash
  // 由於 Firefox decode hash 值
  const href = window.location.href
  const index = href.indexOf('#')
  // 若是此時沒有 # 則返回 ''
  // 不然 取得 # 後的全部內容
  return index === -1 ? '' : href.slice(index + 1)
}複製代碼

能夠看到在實例化過程當中主要作兩件事情:針對於不支持 history api 的降級處理,以及保證默認進入的時候對應的 hash 值是以 / 開頭的,若是不是則替換。值得注意的是這裏並無監聽 hashchange 事件來響應對應的邏輯,這部分邏輯在上篇router.init 中包含的,主要是爲了解決 github.com/vuejs/vue-r…,在對應的回調中則調用了 onHashChange 方法,後邊具體分析。java

友善高級的 HTML5History

HTML5History 則是利用 history.pushState/repaceState API 來完成 URL 跳轉而無須從新加載頁面,頁面地址和正常地址無異;源碼在 src/history/html5.js 中:node

// ...
import { cleanPath } from '../util/path'
import { History } from './base'
// 記錄滾動位置工具函數
import {
  saveScrollPosition,
  getScrollPosition,
  isValidPosition,
  normalizePosition,
  getElementPosition
} from '../util/scroll-position'

// 生成惟一 key 做爲位置相關緩存 key
const genKey = () => String(Date.now())
let _key: string = genKey()

export class HTML5History extends History {
  constructor (router: VueRouter, base: ?string) {
    // 基類構造函數
    super(router, base)

    // 定義滾動行爲 option
    const expectScroll = router.options.scrollBehavior
    // 監聽 popstate 事件 也就是
    // 瀏覽器歷史記錄發生改變的時候(點擊瀏覽器前進後退 或者調用 history api )
    window.addEventListener('popstate', e => {
// ...
    })

    if (expectScroll) {
      // 須要記錄滾動行爲 監聽滾動事件 記錄位置
      window.addEventListener('scroll', () => {
        saveScrollPosition(_key)
      })
    }
  }
// ...
}
// ...複製代碼

能夠看到在這種模式下,初始化做的工做相比 hash 模式少了不少,只是調用基類構造函數以及初始化監聽事件,不須要再作額外的工做。git

AbstractHistory

理論上來講這種模式是用於 Node.js 環境的,通常場景也就是在作測試的時候。可是在實際項目中其實還可使用的,利用這種特性仍是能夠很方便的作不少事情的。因爲它和瀏覽器無關,因此代碼上來講也是最簡單的,在 src/history/abstract.js 中:github

// ...
import { History } from './base'

export class AbstractHistory extends History {
  index: number;
  stack: Array<Route>;

  constructor (router: VueRouter) {
    super(router)
    // 初始化模擬記錄棧
    this.stack = []
    // 當前活動的棧的位置
    this.index = -1
  }
// ...
}複製代碼

能夠看出在抽象模式下,所作的僅僅是用一個數組當作棧來模擬瀏覽器歷史記錄,拿一個變量來標示當前處於哪一個位置。vue-router

三種模式的初始化的部分已經完成了,可是這只是剛剛開始,繼續日後看。

history 改變

history 改變能夠有兩種,一種是用戶點擊連接元素,一種是更新瀏覽器自己的前進後退導航來更新。

先來講瀏覽器導航發生變化的時候會觸發對應的事件:對於 hash 模式而言觸發 windowhashchange 事件,對於 history 模式而言則觸發 windowpopstate 事件。

先說 hash 模式,當觸發改變的時候會調用 HashHistory 實例的 onHashChange

onHashChange () {
    // 不是 / 開頭
    if (!ensureSlash()) {
      return
    }
    // 調用 transitionTo
    this.transitionTo(getHash(), route => {
      // 替換 hash
      replaceHash(route.fullPath)
    })
  }複製代碼

對於 history 模式則是:

window.addEventListener('popstate', e => {
  // 取得 state 中保存的 key
  _key = e.state && e.state.key
  // 保存當前的先
  const current = this.current
  // 調用 transitionTo
  this.transitionTo(getLocation(this.base), next => {
    if (expectScroll) {
      // 處理滾動
      this.handleScroll(next, current, true)
    }
  })
})複製代碼

上邊的 transitionTo 以及 replaceHashgetLocationhandleScroll 後邊統一分析。

再看用戶點擊連接交互,即點擊了 <router-link>,回顧下這個組件在渲染的時候作的事情:

// ...
  render (h: Function) {
// ...

    // 事件綁定
    const on = {
      click: (e) => {
        // 忽略帶有功能鍵的點擊
        if (e.metaKey || e.ctrlKey || e.shiftKey) return
        // 已阻止的返回
        if (e.defaultPrevented) return
        // 右擊
        if (e.button !== 0) return
        // `target="_blank"` 忽略
        const target = e.target.getAttribute('target')
        if (/\b_blank\b/i.test(target)) return
        // 阻止默認行爲 防止跳轉
        e.preventDefault()
        if (this.replace) {
          // replace 邏輯
          router.replace(to)
        } else {
          // push 邏輯
          router.push(to)
        }
      }
    }
    // 建立元素須要附加的數據們
    const data: any = {
      class: classes
    }

    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // 找到第一個 <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 {
        // 沒有 <a> 的話就給當前元素自身綁定時間
        data.on = on
      }
    }
    // 建立元素
    return h(this.tag, data, this.$slots.default)
  }
// ...複製代碼

這裏一個關鍵就是綁定了元素的 click 事件,當用戶觸發後,會調用 routerpushreplace 方法來更新路由。下邊就來看看這兩個方法定義,在 src/index.js 中:

push (location: RawLocation) {
    this.history.push(location)
  }

  replace (location: RawLocation) {
    this.history.replace(location)
  }複製代碼

能夠看到其實他們只是代理而已,真正作事情的仍是 history 來作,下面就分別把 history 的三種模式下的這兩個方法進行分析。

HashHistory

直接看代碼:

// ...
  push (location: RawLocation) {
    // 調用 transitionTo
    this.transitionTo(location, route => {
// ...
    })
  }

  replace (location: RawLocation) {
    // 調用 transitionTo
    this.transitionTo(location, route => {
// ...
    })
  }
// ...複製代碼

操做是相似的,主要就是調用基類的 transitionTo 方法來過渡此次歷史的變化,在完成後更新當前瀏覽器的 hash 值。上篇中大概分析了 transitionTo 方法,可是一些細節並沒細說,這裏來看下遺漏的細節:

transitionTo (location: RawLocation, cb?: Function) {
    // 調用 match 獲得匹配的 route 對象
    const route = this.router.match(location, this.current)
    // 確認過渡
    this.confirmTransition(route, () => {
      // 更新當前 route 對象
      this.updateRoute(route)
      cb && cb(route)
      // 子類實現的更新url地址
      // 對於 hash 模式的話 就是更新 hash 的值
      // 對於 history 模式的話 就是利用 pushstate / replacestate 來更新
      // 瀏覽器地址
      this.ensureURL()
    })
  }
  // 確認過渡
  confirmTransition (route: Route, cb: Function) {
    const current = this.current
    // 若是是相同 直接返回
    if (isSameRoute(route, current)) {
      this.ensureURL()
      return
    }
    const {
      deactivated,
      activated
    } = resolveQueue(this.current.matched, route.matched)

    // 整個切換週期的隊列
    const queue: Array<?NavigationGuard> = [].concat(
      // leave 的鉤子
      extractLeaveGuards(deactivated),
      // 全局 router before hooks
      this.router.beforeHooks,
      // 將要更新的路由的 beforeEnter 鉤子
      activated.map(m => m.beforeEnter),
      // 異步組件
      resolveAsyncComponents(activated)
    )

    this.pending = route
    // 每個隊列執行的 iterator 函數
    const iterator = (hook: NavigationGuard, next) => {
// ...
    }
    // 執行隊列 leave 和 beforeEnter 相關鉤子
    runQueue(queue, iterator, () => {
//...
    })
  }複製代碼

這裏有一個很關鍵的路由對象的 matched 實例,從上次的分析中能夠知道它就是匹配到的路由記錄的合集;這裏從執行順序上來看有這些 resolveQueueextractLeaveGuardsresolveAsyncComponentsrunQueue 關鍵方法。

首先來看 resolveQueue

function resolveQueue ( current: Array<RouteRecord>, next: Array<RouteRecord> ): {
  activated: Array<RouteRecord>,
  deactivated: Array<RouteRecord>
} {
  let i
  // 取得最大深度
  const max = Math.max(current.length, next.length)
  // 從根開始對比 一旦不同的話 就能夠中止了
  for (i = 0; i < max; i++) {
    if (current[i] !== next[i]) {
      break
    }
  }
  // 舍掉相同的部分 只保留不一樣的
  return {
    activated: next.slice(i),
    deactivated: current.slice(i)
  }
}複製代碼

能夠看出 resolveQueue 就是交叉比對當前路由的路由記錄和如今的這個路由的路由記錄來決定調用哪些路由記錄的鉤子函數。

繼續來看 extractLeaveGuards

// 取得 leave 的組件的 beforeRouteLeave 鉤子函數們
function extractLeaveGuards (matched: Array<RouteRecord>): Array<?Function> {
  // 打平組件的 beforeRouteLeave 鉤子函數們 按照順序獲得 而後再 reverse
  // 由於 leave 的過程是從內層組件到外層組件的過程
  return flatten(flatMapComponents(matched, (def, instance) => {
    const guard = extractGuard(def, 'beforeRouteLeave')
    if (guard) {
      return Array.isArray(guard)
        ? guard.map(guard => wrapLeaveGuard(guard, instance))
        : wrapLeaveGuard(guard, instance)
    }
  }).reverse())
}
// ...
// 將一個二維數組(僞)轉換成按順序轉換成一維數組
// [[1], [2, 3], 4] -> [1, 2, 3, 4]
function flatten (arr) {
  return Array.prototype.concat.apply([], arr)
}複製代碼

能夠看到在執行 extractLeaveGuards 的時候首先須要調用 flatMapComponents 函數,下面來看看這個函數具體定義:

// 將匹配到的組件們根據fn獲得的鉤子函數們打平
function flatMapComponents ( matched: Array<RouteRecord>, fn: Function ): Array<?Function> {
  // 遍歷匹配到的路由記錄
  return flatten(matched.map(m => {
    // 遍歷 components 配置的組件們
    //// 對於默認視圖模式下,會包含 default (也就是實例化路由的時候傳入的 component 的值)
    //// 若是說多個命名視圖的話 就是配置的對應的 components 的值
    // 調用 fn 獲得 guard 鉤子函數的值
    // 注意此時傳入的值分別是:視圖對應的組件類,對應的組件實例,路由記錄,當前 key 值 (命名視圖 name 值)
    return Object.keys(m.components).map(key => fn(
      m.components[key],
      m.instances[key],
      m, key
    ))
  }))
}複製代碼

此時須要仔細看下調用 flatMapComponents 時傳入的 fn

flatMapComponents(matched, (def, instance) => {
  // 組件配置的 beforeRouteLeave 鉤子
  const guard = extractGuard(def, 'beforeRouteLeave')
  // 存在的話 返回
  if (guard) {
    // 每個鉤子函數須要再包裹一次
    return Array.isArray(guard)
      ? guard.map(guard => wrapLeaveGuard(guard, instance))
      : wrapLeaveGuard(guard, instance)
  }
  // 這裏沒有返回值 默認調用的結果是 undefined
})複製代碼

先來看 extractGuard 的定義:

// 取得指定組件的 key 值
function extractGuard ( def: Object | Function, key: string ): NavigationGuard | Array<NavigationGuard> {
  if (typeof def !== 'function') {
    // 對象的話 爲了應用上全局的 mixins 這裏 extend 下
    // 賦值 def 爲 Vue 「子類」
    def = _Vue.extend(def)
  }
  // 取得 options 上的 key 值
  return def.options[key]
}複製代碼

很簡答就是取得組件定義時的 key 配置項的值。

再來看看具體的 wrapLeaveGuard 是幹啥用的:

function wrapLeaveGuard ( guard: NavigationGuard, instance: _Vue ): NavigationGuard {
  // 返回函數 執行的時候 用於保證上下文 是當前的組件實例 instance
  return function routeLeaveGuard () {
    return guard.apply(instance, arguments)
  }
}複製代碼

其實這個函數還能夠這樣寫:

function wrapLeaveGuard ( guard: NavigationGuard, instance: _Vue ): NavigationGuard {
  return _Vue.util.bind(guard, instance)
}複製代碼

這樣整個的 extractLeaveGuards 就分析完了,這部分仍是比較繞的,須要好好理解下。可是目的是明確的就是獲得將要離開的組件們按照由深到淺的順序組合的 beforeRouteLeave 鉤子函數們。

再來看一個關鍵的函數 resolveAsyncComponents,一看名字就知道這個是用來解決異步組件問題的:

function resolveAsyncComponents (matched: Array<RouteRecord>): Array<?Function> {
  // 依舊調用 flatMapComponents 只是此時傳入的 fn 是這樣的:
  return flatMapComponents(matched, (def, _, match, key) => {
    // 這裏假定說路由上定義的組件 是函數 可是沒有 options
    // 就認爲他是一個異步組件。
    // 這裏並無使用 Vue 默認的異步機制的緣由是咱們但願在獲得真正的異步組件以前
    // 整個的路由導航是一直處於掛起狀態
    if (typeof def === 'function' && !def.options) {
      // 返回「異步」鉤子函數
      return (to, from, next) => {
// ...
      }
    }
  })
}複製代碼

下面繼續看,最後一個關鍵的 runQueue 函數,它的定義在 src/util/async.js 中:

// 執行隊列
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  // 內部迭代函數
  const step = index => {
    // 若是說當前的 index 值和整個隊列的長度值齊平了 說明隊列已經執行完成
    if (index >= queue.length) {
      // 執行隊列執行完成的回調函數
      cb()
    } else {
      if (queue[index]) {
        // 若是存在的話 調用傳入的迭代函數執行
        fn(queue[index], () => {
          // 第二個參數是一個函數 當調用的時候才繼續處理隊列的下一個位置
          step(index + 1)
        })
      } else {
        // 當前隊列位置的值爲假 繼續隊列下一個位置
        step(index + 1)
      }
    }
  }
  // 從隊列起始位置開始迭代
  step(0)
}複製代碼

能夠看出就是一個執行一個函數隊列中的每一項,可是考慮了異步場景,只有上一個隊列中的項顯式調用回調的時候纔會繼續調用隊列的下一個函數。

在切換路由過程當中調用的邏輯是這樣的:

// 每個隊列執行的 iterator 函數
const iterator = (hook: NavigationGuard, next) => {
  // 確保期間仍是當前路由
  if (this.pending !== route) return
  // 調用鉤子
  hook(route, current, (to: any) => {
    // 若是說鉤子函數在調用第三個參數(函數)` 時傳入了 false
    // 則意味着要終止本次的路由切換
    if (to === false) {
      // next(false) -> abort navigation, ensure current URL
      // 從新保證當前 url 是正確的
      this.ensureURL(true)
    } else if (typeof to === 'string' || typeof to === 'object') {
      // next('/') or next({ path: '/' }) -> redirect
      // 若是傳入的是字符串 或者對象的話 認爲是一個重定向操做
      // 直接調用 push 走你
      this.push(to)
    } else {
      // confirm transition and pass on the value
      // 其餘狀況 意味着這次路由切換沒有問題 繼續隊列下一個
      // 且把值傳入了
      // 傳入的這個值 在此時的 leave 的狀況下是沒用的
      // 注意:這是爲了後邊 enter 的時候在處理 beforeRouteEnter 鉤子的時候
      // 能夠傳入一個函數 用於得到組件實例
      next(to)
    }
  })
}
// 執行隊列 leave 和 beforeEnter 相關鉤子
runQueue(queue, iterator, () => {
// ...
})複製代碼

queue 是上邊定義的一個切換週期的各類鉤子函數以及處理異步組件的「異步」鉤子函數所組成隊列,在執行完後就會調用隊列執行完成後毀掉函數,下面來看這個函數作的事情:

runQueue(queue, iterator, () => {
  // enter 後的回調函數們 用於組件實例化後須要執行的一些回調
  const postEnterCbs = []
  // leave 完了後 就要進入 enter 階段了
  const enterGuards = extractEnterGuards(activated, postEnterCbs, () => {
    return this.current === route
  })
  // enter 的回調鉤子們依舊有多是異步的 不只僅是異步組件場景
  runQueue(enterGuards, iterator, () => {
// ...
  })
})複製代碼

仔細看看這個 extractEnterGuards,從調用參數上來看仍是和以前的 extractLeaveGuards 是不一樣的:

function extractEnterGuards ( matched: Array<RouteRecord>, cbs: Array<Function>, isValid: () => boolean ): Array<?Function> {
  // 依舊是調用 flatMapComponents
  return flatten(flatMapComponents(matched, (def, _, match, key) => {
    // 調用 extractGuard 獲得組件上的 beforeRouteEnter 鉤子
    const guard = extractGuard(def, 'beforeRouteEnter')
    if (guard) {
      // 特殊處理 依舊進行包裝
      return Array.isArray(guard)
        ? guard.map(guard => wrapEnterGuard(guard, cbs, match, key, isValid))
        : wrapEnterGuard(guard, cbs, match, key, isValid)
    }
  }))
}
function wrapEnterGuard ( guard: NavigationGuard, cbs: Array<Function>, match: RouteRecord, key: string, isValid: () => boolean ): NavigationGuard {
  // 代理 路由 enter 的鉤子函數
  return function routeEnterGuard (to, from, next) {
// ...
  }
}複製代碼

能夠看出此時總體的思路仍是和 extractLeaveGuards 的差很少的,只是多了 cbs 回調數組 和 isValid 校驗函數,截止到如今還不知道他們的具體做用,繼續往下看此時調用的 runQueue

// enter 的鉤子們
runQueue(enterGuards, iterator, () => {
// ...
})複製代碼

能夠看到此時執行 enterGuards 隊列的迭代函數依舊是上邊定義的 iterator,在迭代過程當中就會調用 wrapEnterGuard 返回的 routeEnterGuard 函數:

function wrapEnterGuard ( guard: NavigationGuard, cbs: Array<Function>, match: RouteRecord, key: string, isValid: () => boolean ): NavigationGuard {
  // 代理 路由 enter 的鉤子函數
  return function routeEnterGuard (to, from, next) {
    // 調用用戶設置的鉤子函數
    return guard(to, from, cb => {
      // 此時若是說調用第三個參數的時候傳入了回調函數
      // 認爲是在組件 enter 後有了組件實例對象以後執行的回調函數
      // 依舊把參數傳遞過去 由於有可能傳入的是
      // false 或者 字符串 或者 對象
      // 繼續走原有邏輯
      next(cb)
      if (typeof cb === 'function') {
        // 加入到 cbs 數組中
        // 只是這裏沒有直接 push 進去 而是作了額外處理
        cbs.push(() => {
          // 主要是爲了修復 #750 的bug
          // 若是說 router-view 被一個 out-in transition 過渡包含的話
          // 此時的實例不必定是註冊了的(由於須要作完動畫) 因此須要輪訓判斷
          // 直至 current route 的值再也不有效
          poll(cb, match.instances, key, isValid)
        })
      }
    })
  }
}複製代碼

這個 poll 又是作什麼事情呢?

function poll ( cb: any, // somehow flow cannot infer this is a function instances: Object, key: string, isValid: () => boolean ) {
  // 若是實例上有 key
  // 也就意味着有 key 爲名的命名視圖實例了
  if (instances[key]) {
    // 執行回調
    cb(instances[key])
  } else if (isValid()) {
    // 輪訓的前提是當前 cuurent route 是有效的
    setTimeout(() => {
      poll(cb, instances, key, isValid)
    }, 16)
  }
}複製代碼

isValid 的定義就是很簡單了,經過在調用 extractEnterGuards 的時候傳入的:

const enterGuards = extractEnterGuards(activated, postEnterCbs, () => {
  // 判斷當前 route 是和 enter 的 route 是同一個
  return this.current === route
})複製代碼

回到執行 enter 進入時的鉤子函數隊列的地方,在執行完全部隊列中函數後會調用傳入 runQueue 的回調:

runQueue(enterGuards, iterator, () => {
  // 確保當前的 pending 中的路由是和要激活的是同一個路由對象
  // 以防在執行鉤子過程當中又一次的切換路由
  if (this.pending === route) {
    this.pending = null
    // 執行傳入 confirmTransition 的回調
    cb(route)
    // 在 nextTick 時執行 postEnterCbs 中保存的回調
    this.router.app.$nextTick(() => {
      postEnterCbs.forEach(cb => cb())
    })
  }
})複製代碼

經過上篇分析能夠知道 confirmTransition 的回調作的事情:

this.confirmTransition(route, () => {
  // 更新當前 route 對象
  this.updateRoute(route)
  // 執行回調 也就是 transitionTo 傳入的回調
  cb && cb(route)
  // 子類實現的更新url地址
  // 對於 hash 模式的話 就是更新 hash 的值
  // 對於 history 模式的話 就是利用 pushstate / replacestate 來更新
  // 瀏覽器地址
  this.ensureURL()
})複製代碼

針對於 HashHistory 來講,調用 transitionTo 的回調就是:

// ...
  push (location: RawLocation) {
    // 調用 transitionTo
    this.transitionTo(location, route => {
      // 完成後 pushHash
      pushHash(route.fullPath)
    })
  }

  replace (location: RawLocation) {
    // 調用 transitionTo
    this.transitionTo(location, route => {
      // 完成後 replaceHash
      replaceHash(route.fullPath)
    })
  }
// ...
function pushHash (path) {
  window.location.hash = path
}

function replaceHash (path) {
  const i = window.location.href.indexOf('#')
  // 直接調用 replace 強制替換 以免產生「多餘」的歷史記錄
  // 主要是用戶初次跳入 且hash值不是以 / 開頭的時候直接替換
  // 其他時候和push沒啥區別 瀏覽器老是記錄hash記錄
  window.location.replace(
    window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
  )
}複製代碼

其實就是更新瀏覽器的 hash 值,pushreplace 的場景下都是一個效果。

回到 confirmTransition 的回調,最後還作了一件事情 ensureURL

ensureURL (push?: boolean) {
  const current = this.current.fullPath
  if (getHash() !== current) {
    push ? pushHash(current) : replaceHash(current)
  }
}複製代碼

此時 pushundefined,因此調用 replaceHash 更新瀏覽器 hash 值。

HTML5History

整個的流程和 HashHistory 是相似的,不一樣的只是一些具體的邏輯處理以及特性,因此這裏呢就直接來看整個的 HTML5History

export class HTML5History extends History {
// ...
  go (n: number) {
    window.history.go(n)
  }

  push (location: RawLocation) {
    const current = this.current
    // 依舊調用基類 transitionTo
    this.transitionTo(location, route => {
      // 調用 pushState 可是 url 是 base 值加上當前 fullPath
      // 由於 fullPath 是不帶 base 部分得
      pushState(cleanPath(this.base + route.fullPath))
      // 處理滾動
      this.handleScroll(route, current, false)
    })
  }

  replace (location: RawLocation) {
    const current = this.current
    // 依舊調用基類 transitionTo
    this.transitionTo(location, route => {
      // 調用 replaceState
      replaceState(cleanPath(this.base + route.fullPath))
      // 滾動
      this.handleScroll(route, current, false)
    })
  }
  // 保證 location 地址是同步的
  ensureURL (push?: boolean) {
    if (getLocation(this.base) !== this.current.fullPath) {
      const current = cleanPath(this.base + this.current.fullPath)
      push ? pushState(current) : replaceState(current)
    }
  }
  // 處理滾動
  handleScroll (to: Route, from: Route, isPop: boolean) {
    const router = this.router
    if (!router.app) {
      return
    }
    // 自定義滾動行爲
    const behavior = router.options.scrollBehavior
    if (!behavior) {
      // 不存在直接返回了
      return
    }
    assert(typeof behavior === 'function', `scrollBehavior must be a function`)

    // 等待下從新渲染邏輯
    router.app.$nextTick(() => {
      // 獲得key對應位置
      let position = getScrollPosition(_key)
      // 根據自定義滾動行爲函數來判斷是否應該滾動
      const shouldScroll = behavior(to, from, isPop ? position : null)
      if (!shouldScroll) {
        return
      }
      // 應該滾動
      const isObject = typeof shouldScroll === 'object'
      if (isObject && typeof shouldScroll.selector === 'string') {
        // 帶有 selector 獲得該元素
        const el = document.querySelector(shouldScroll.selector)
        if (el) {
          // 獲得該元素位置
          position = getElementPosition(el)
        } else if (isValidPosition(shouldScroll)) {
          // 元素不存在 降級下
          position = normalizePosition(shouldScroll)
        }
      } else if (isObject && isValidPosition(shouldScroll)) {
        // 對象 且是合法位置 統一格式
        position = normalizePosition(shouldScroll)
      }

      if (position) {
        // 滾動到指定位置
        window.scrollTo(position.x, position.y)
      }
    })
  }
}

// 獲得 不帶 base 值的 location
export function getLocation (base: string): string {
  let path = window.location.pathname
  if (base && path.indexOf(base) === 0) {
    path = path.slice(base.length)
  }
  // 是包含 search 和 hash 的
  return (path || '/') + window.location.search + window.location.hash
}

function pushState (url: string, replace?: boolean) {
  // 加了 try...catch 是由於 Safari 有調用 pushState 100 次限制
  // 一旦達到就會拋出 DOM Exception 18 錯誤
  const history = window.history
  try {
    // 若是是 replace 則調用 history 的 replaceState 操做
    // 不然則調用 pushState
    if (replace) {
      // replace 的話 key 仍是當前的 key 不必生成新的
      // 由於被替換的頁面是進入不了的
      history.replaceState({ key: _key }, '', url)
    } else {
      // 從新生成 key
      _key = genKey()
      // 帶入新的 key 值
      history.pushState({ key: _key }, '', url)
    }
    // 保存 key 對應的位置
    saveScrollPosition(_key)
  } catch (e) {
    // 達到限制了 則從新指定新的地址
    window.location[replace ? 'assign' : 'replace'](url)
  }
}
// 直接調用 pushState 傳入 replace 爲 true
function replaceState (url: string) {
  pushState(url, true)
}複製代碼

這樣能夠看出和 HashHistory 中不一樣的是這裏增長了滾動位置特性以及當歷史發生變化時改變瀏覽器地址的行爲是不同的,這裏使用了新的 history api 來更新。

AbstractHistory

抽象模式是屬於最簡單的處理了,由於不涉及和瀏覽器地址相關記錄關聯在一塊兒;總體流程依舊和 HashHistory 是同樣的,只是這裏經過數組來模擬瀏覽器歷史記錄堆棧信息。

// ...
import { History } from './base'

export class AbstractHistory extends History {
  index: number;
  stack: Array<Route>;
// ...

  push (location: RawLocation) {
    this.transitionTo(location, route => {
      // 更新歷史堆棧信息
      this.stack = this.stack.slice(0, this.index + 1).concat(route)
      // 更新當前所處位置
      this.index++
    })
  }

  replace (location: RawLocation) {
    this.transitionTo(location, route => {
      // 更新歷史堆棧信息 位置則不用更新 由於是 replace 操做
      // 在堆棧中也是直接 replace 掉的
      this.stack = this.stack.slice(0, this.index).concat(route)
    })
  }
  // 對於 go 的模擬
  go (n: number) {
    // 新的歷史記錄位置
    const targetIndex = this.index + n
    // 超出返回了
    if (targetIndex < 0 || targetIndex >= this.stack.length) {
      return
    }
    // 取得新的 route 對象
    // 由於是和瀏覽器無關的 這裏獲得的必定是已經訪問過的
    const route = this.stack[targetIndex]
    // 因此這裏直接調用 confirmTransition 了
    // 而不是調用 transitionTo 還要走一遍 match 邏輯
    this.confirmTransition(route, () => {
      // 更新
      this.index = targetIndex
      this.updateRoute(route)
    })
  }

  ensureURL () {
    // noop
  }
}複製代碼

小結

整個的和 history 相關的代碼到這裏已經分析完畢了,雖然有三種模式,可是總體執行過程仍是同樣的,惟一差別的就是在處理location更新時的具體邏輯不一樣。

歡迎拍磚。

Vuex 2.0 源碼分析知乎地址:zhuanlan.zhihu.com/p/23921964


歡迎關注DDFE
GITHUB:github.com/DDFE
微信公衆號:微信搜索公衆號「DDFE」或掃描下面的二維碼

相關文章
相關標籤/搜索