vue-router 源碼分析-history

作者:滴滴公共前端團隊——苗典

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

初始化 Router

通過整體流程可以知道在路由實例化的時候會根據當前 mode 模式來選擇實例化對應的History類,這裡再來回顧下,在 src/index.js 中:

// ...nimport { HashHistory, getHash } from ./history/hashnimport { HTML5History, getLocation } from ./history/html5nimport { AbstractHistory } from ./history/abstractn// ...nexport default class VueRouter {n// ...n constructor (options: RouterOptions = {}) {n// ...n // 默認模式是 hashn let mode = options.mode || hashn // 如果設置的是 history 但是如果瀏覽器不支持的話 n // 強制退回到 hashn this.fallback = mode === history && !supportsHistoryn if (this.fallback) {n mode = hashn }n // 不在瀏覽器中 強制 abstract 模式n if (!inBrowser) {n mode = abstractn }n this.mode = moden // 根據不同模式選擇實例化對應的 History 類n switch (mode) {n case history:n this.history = new HTML5History(this, options.base)n breakn case hash:n // 細節 傳入了 fallbackn this.history = new HashHistory(this, options.base, this.fallback)n breakn case abstract:n this.history = new AbstractHistory(this)n breakn default:n assert(false, `invalid mode: ${mode}`)n }n }n// ...n

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

HashHistory

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

// ...nimport { History } from ./basenimport { getLocation } from ./html5nimport { cleanPath } from ../util/pathnn// 繼承 History 基類nexport class HashHistory extends History {n constructor (router: VueRouter, base: ?string, fallback: boolean) {n // 調用基類構造器n super(router, base)nn // 如果說是從 history 模式降級來的n // 需要做降級檢查n if (fallback && this.checkFallback()) {n // 如果降級 且 做了降級處理 則什麼也不需要做n returnn }n // 保證 hash 是以 / 開頭n ensureSlash()n }nn checkFallback () {n // 得到除去 base 的真正的 location 值n const location = getLocation(this.base)n if (!/^/#/.test(location)) {n // 如果說此時的地址不是以 /# 開頭的n // 需要做一次降級處理 降級為 hash 模式下應有的 /# 開頭n window.location.replace(n cleanPath(this.base + /# + location)n )n return truen }n }n// ...n}nn// 保證 hash 以 / 開頭nfunction ensureSlash (): boolean {n // 得到 hash 值n const path = getHash()n // 如果說是以 / 開頭的 直接返回即可n if (path.charAt(0) === /) {n return truen }n // 不是的話 需要手工保證一次 替換 hash 值n replaceHash(/ + path)n return falsen}nnexport function getHash (): string {n // 因為兼容性問題 這裡沒有直接使用 window.location.hashn // 因為 Firefox decode hash 值n const href = window.location.hrefn const index = href.indexOf(#)n // 如果此時沒有 # 則返回 n // 否則 取得 # 後的所有內容n return index === -1 ? : href.slice(index + 1)n}n

可以看到在實例化過程中主要做兩件事情:針對於不支持 history api 的降級處理,以及保證默認進入的時候對應的 hash 值是以 / 開頭的,如果不是則替換。值得注意的是這裡並沒有監聽 hashchange 事件來響應對應的邏輯,這部分邏輯在上篇的 router.init 中包含的,主要是為了解決 beforeEnter fire twice on root path (『/『) after async next call · Issue #725 · vuejs/vue-router after async next call · Issue #725 · vuejs/vue-router),在對應的回調中則調用了 onHashChange 方法,後邊具體分析。

友善高級的 HTML5History

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

// ...nimport { cleanPath } from ../util/pathnimport { History } from ./basen// 記錄滾動位置工具函數nimport {n saveScrollPosition,n getScrollPosition,n isValidPosition,n normalizePosition,n getElementPositionn} from ../util/scroll-positionnn// 生成唯一 key 作為位置相關緩存 keynconst genKey = () => String(Date.now())nlet _key: string = genKey()nnexport class HTML5History extends History {n constructor (router: VueRouter, base: ?string) {n // 基類構造函數n super(router, base)nn // 定義滾動行為 optionn const expectScroll = router.options.scrollBehaviorn // 監聽 popstate 事件 也就是n // 瀏覽器歷史記錄發生改變的時候(點擊瀏覽器前進後退 或者調用 history api )n window.addEventListener(popstate, e => {n// ...n })nn if (expectScroll) {n // 需要記錄滾動行為 監聽滾動事件 記錄位置n window.addEventListener(scroll, () => {n saveScrollPosition(_key)n })n }n }n// ...n}n// ...n

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

AbstractHistory

理論上來說這種模式是用於 Node.js 環境的,一般場景也就是在做測試的時候。但是在實際項目中其實還可以使用的,利用這種特性還是可以很方便的做很多事情的。由於它和瀏覽器無關,所以代碼上來說也是最簡單的,在 src/history/abstract.js 中:

// ...nimport { History } from ./basennexport class AbstractHistory extends History {n index: number;n stack: Array<Route>;nn constructor (router: VueRouter) {n super(router)n // 初始化模擬記錄棧n this.stack = []n // 當前活動的棧的位置n this.index = -1n }n// ...n}n

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

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

history 改變

history 改變可以有兩種,一種是用戶點擊鏈接元素,一種是更新瀏覽器本身的前進後退導航來更新。

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

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

onHashChange () {n // 不是 / 開頭n if (!ensureSlash()) {n returnn }n // 調用 transitionTon this.transitionTo(getHash(), route => {n // 替換 hashn replaceHash(route.fullPath)n })n }n

對於 history 模式則是:

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

上邊的 transitionTo 以及 replaceHash、getLocation、handleScroll 後邊統一分析。

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

// ...n render (h: Function) {n// ...nn // 事件綁定n const on = {n click: (e) => {n // 忽略帶有功能鍵的點擊n if (e.metaKey || e.ctrlKey || e.shiftKey) returnn // 已阻止的返回n if (e.defaultPrevented) returnn // 右擊n if (e.button !== 0) returnn // `target="_blank"` 忽略n const target = e.target.getAttribute(target)n if (/b_blankb/i.test(target)) returnn // 阻止默認行為 防止跳轉n e.preventDefault()n if (this.replace) {n // replace 邏輯n router.replace(to)n } else {n // push 邏輯n router.push(to)n }n }n }n // 創建元素需要附加的數據們n const data: any = {n class: classesn }nn if (this.tag === a) {n data.on = onn data.attrs = { href }n } else {n // 找到第一個 <a> 給予這個元素事件綁定和href屬性n const a = findAnchor(this.$slots.default)n if (a) {n // in case the <a> is a static noden a.isStatic = falsen const extend = _Vue.util.extendn const aData = a.data = extend({}, a.data)n aData.on = onn const aAttrs = a.data.attrs = extend({}, a.data.attrs)n aAttrs.href = hrefn } else {n // 沒有 <a> 的話就給當前元素自身綁定時間n data.on = onn }n }n // 創建元素n return h(this.tag, data, this.$slots.default)n }n// ...n

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

push (location: RawLocation) {n this.history.push(location)n }nn replace (location: RawLocation) {n this.history.replace(location)n }n

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

HashHistory

直接看代碼:

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

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

transitionTo (location: RawLocation, cb?: Function) {n // 調用 match 得到匹配的 route 對象n const route = this.router.match(location, this.current)n // 確認過渡n this.confirmTransition(route, () => {n // 更新當前 route 對象n this.updateRoute(route)n cb && cb(route)n // 子類實現的更新url地址n // 對於 hash 模式的話 就是更新 hash 的值n // 對於 history 模式的話 就是利用 pushstate / replacestate 來更新n // 瀏覽器地址n this.ensureURL()n })n }n // 確認過渡n confirmTransition (route: Route, cb: Function) {n const current = this.currentn // 如果是相同 直接返回n if (isSameRoute(route, current)) {n this.ensureURL()n returnn }n const {n deactivated,n activatedn } = resolveQueue(this.current.matched, route.matched)nn // 整個切換周期的隊列n const queue: Array<?NavigationGuard> = [].concat(n // leave 的鉤子n extractLeaveGuards(deactivated),n // 全局 router before hooksn this.router.beforeHooks,n // 將要更新的路由的 beforeEnter 鉤子n activated.map(m => m.beforeEnter),n // 非同步組件n resolveAsyncComponents(activated)n )nn this.pending = routen // 每一個隊列執行的 iterator 函數n const iterator = (hook: NavigationGuard, next) => {n// ...n }n // 執行隊列 leave 和 beforeEnter 相關鉤子n runQueue(queue, iterator, () => {n//...n })n }n

這裡有一個很關鍵的路由對象的 matched 實例,從上次的分析中可以知道它就是匹配到的路由記錄的合集;這裡從執行順序上來看有這些 resolveQueue、extractLeaveGuards、resolveAsyncComponents、runQueue 關鍵方法。

首先來看 resolveQueue:

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

可以看出 resolveQueue 就是交叉比對當前路由的路由記錄和現在的這個路由的路由記錄來決定調用哪些路由記錄的鉤子函數。

繼續來看 extractLeaveGuards:

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

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

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

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

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

先來看 extractGuard 的定義:

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

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

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

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

其實這個函數還可以這樣寫:

function wrapLeaveGuard (n guard: NavigationGuard,n instance: _Vuen): NavigationGuard {n return _Vue.util.bind(guard, instance)n}n

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

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

function resolveAsyncComponents (matched: Array<RouteRecord>): Array<?Function> {n // 依舊調用 flatMapComponents 只是此時傳入的 fn 是這樣的:n return flatMapComponents(matched, (def, _, match, key) => {n // 這裡假定說路由上定義的組件 是函數 但是沒有 optionsn // 就認為他是一個非同步組件。n // 這裡並沒有使用 Vue 默認的非同步機制的原因是我們希望在得到真正的非同步組件之前n // 整個的路由導航是一直處於掛起狀態n if (typeof def === function && !def.options) {n // 返回「非同步」鉤子函數n return (to, from, next) => {n// ...n }n }n })n}n

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

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

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

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

// 每一個隊列執行的 iterator 函數nconst iterator = (hook: NavigationGuard, next) => {n // 確保期間還是當前路由n if (this.pending !== route) returnn // 調用鉤子n hook(route, current, (to: any) => {n // 如果說鉤子函數在調用第三個參數(函數)` 時傳入了 falsen // 則意味著要終止本次的路由切換n if (to === false) {n // next(false) -> abort navigation, ensure current URLn // 重新保證當前 url 是正確的n this.ensureURL(true)n } else if (typeof to === string || typeof to === object) {n // next(/) or next({ path: / }) -> redirectn // 如果傳入的是字元串 或者對象的話 認為是一個重定向操作n // 直接調用 push 走你n this.push(to)n } else {n // confirm transition and pass on the valuen // 其他情況 意味著此次路由切換沒有問題 繼續隊列下一個n // 且把值傳入了n // 傳入的這個值 在此時的 leave 的情況下是沒用的n // 注意:這是為了後邊 enter 的時候在處理 beforeRouteEnter 鉤子的時候n // 可以傳入一個函數 用於獲得組件實例n next(to)n }n })n}n// 執行隊列 leave 和 beforeEnter 相關鉤子nrunQueue(queue, iterator, () => {n// ...n})n

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

runQueue(queue, iterator, () => {n // enter 後的回調函數們 用於組件實例化後需要執行的一些回調n const postEnterCbs = []n // leave 完了後 就要進入 enter 階段了n const enterGuards = extractEnterGuards(activated, postEnterCbs, () => {n return this.current === routen })n // enter 的回調鉤子們依舊有可能是非同步的 不僅僅是非同步組件場景n runQueue(enterGuards, iterator, () => {n// ...n })n})n

仔細看看這個 extractEnterGuards,從調用參數上來看還是和之前的 extractLeaveGuards 是不同的:

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

可以看出此時整體的思路還是和 extractLeaveGuards 的差不多的,只是多了 cbs 回調數組 和 isValid 校驗函數,截止到現在還不知道他們的具體作用,繼續往下看此時調用的 runQueue:

// enter 的鉤子們nrunQueue(enterGuards, iterator, () => {n// ...n})n

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

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

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

function poll (n cb: any, // somehow flow cannot infer this is a functionn instances: Object,n key: string,n isValid: () => booleann) {n // 如果實例上有 keyn // 也就意味著有 key 為名的命名視圖實例了n if (instances[key]) {n // 執行回調n cb(instances[key])n } else if (isValid()) {n // 輪訓的前提是當前 cuurent route 是有效的n setTimeout(() => {n poll(cb, instances, key, isValid)n }, 16)n }n}n

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

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

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

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

通過上篇分析可以知道 confirmTransition 的回調做的事情:

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

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

// ...n push (location: RawLocation) {n // 調用 transitionTon this.transitionTo(location, route => {n // 完成後 pushHashn pushHash(route.fullPath)n })n }nn replace (location: RawLocation) {n // 調用 transitionTon this.transitionTo(location, route => {n // 完成後 replaceHashn replaceHash(route.fullPath)n })n }n// ...nfunction pushHash (path) {n window.location.hash = pathn}nnfunction replaceHash (path) {n const i = window.location.href.indexOf(#)n // 直接調用 replace 強制替換 以避免產生「多餘」的歷史記錄n // 主要是用戶初次跳入 且hash值不是以 / 開頭的時候直接替換n // 其餘時候和push沒啥區別 瀏覽器總是記錄hash記錄n window.location.replace(n window.location.href.slice(0, i >= 0 ? i : 0) + # + pathn )n}n

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

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

ensureURL (push?: boolean) {n const current = this.current.fullPathn if (getHash() !== current) {n push ? pushHash(current) : replaceHash(current)n }n}n

此時 push 為 undefined,所以調用 replaceHash 更新瀏覽器 hash 值。

HTML5History

整個的流程和 HashHistory 是類似的,不同的只是一些具體的邏輯處理以及特性,所以這裡呢就直接來看整個的 HTML5History:

export class HTML5History extends History {n// ...n go (n: number) {n window.history.go(n)n }nn push (location: RawLocation) {n const current = this.currentn // 依舊調用基類 transitionTon this.transitionTo(location, route => {n // 調用 pushState 但是 url 是 base 值加上當前 fullPathn // 因為 fullPath 是不帶 base 部分得n pushState(cleanPath(this.base + route.fullPath))n // 處理滾動n this.handleScroll(route, current, false)n })n }nn replace (location: RawLocation) {n const current = this.currentn // 依舊調用基類 transitionTon this.transitionTo(location, route => {n // 調用 replaceStaten replaceState(cleanPath(this.base + route.fullPath))n // 滾動n this.handleScroll(route, current, false)n })n }n // 保證 location 地址是同步的n ensureURL (push?: boolean) {n if (getLocation(this.base) !== this.current.fullPath) {n const current = cleanPath(this.base + this.current.fullPath)n push ? pushState(current) : replaceState(current)n }n }n // 處理滾動n handleScroll (to: Route, from: Route, isPop: boolean) {n const router = this.routern if (!router.app) {n returnn }n // 自定義滾動行為n const behavior = router.options.scrollBehaviorn if (!behavior) {n // 不存在直接返回了n returnn }n assert(typeof behavior === function, `scrollBehavior must be a function`)nn // 等待下重新渲染邏輯n router.app.$nextTick(() => {n // 得到key對應位置n let position = getScrollPosition(_key)n // 根據自定義滾動行為函數來判斷是否應該滾動n const shouldScroll = behavior(to, from, isPop ? position : null)n if (!shouldScroll) {n returnn }n // 應該滾動n const isObject = typeof shouldScroll === objectn if (isObject && typeof shouldScroll.selector === string) {n // 帶有 selector 得到該元素n const el = document.querySelector(shouldScroll.selector)n if (el) {n // 得到該元素位置n position = getElementPosition(el)n } else if (isValidPosition(shouldScroll)) {n // 元素不存在 降級下n position = normalizePosition(shouldScroll)n }n } else if (isObject && isValidPosition(shouldScroll)) {n // 對象 且是合法位置 統一格式n position = normalizePosition(shouldScroll)n }nn if (position) {n // 滾動到指定位置n window.scrollTo(position.x, position.y)n }n })n }n}nn// 得到 不帶 base 值的 locationnexport function getLocation (base: string): string {n let path = window.location.pathnamen if (base && path.indexOf(base) === 0) {n path = path.slice(base.length)n }n // 是包含 search 和 hash 的n return (path || /) + window.location.search + window.location.hashn}nnfunction pushState (url: string, replace?: boolean) {n // 加了 try...catch 是因為 Safari 有調用 pushState 100 次限制n // 一旦達到就會拋出 DOM Exception 18 錯誤n const history = window.historyn try {n // 如果是 replace 則調用 history 的 replaceState 操作n // 否則則調用 pushStaten if (replace) {n // replace 的話 key 還是當前的 key 沒必要生成新的n // 因為被替換的頁面是進入不了的n history.replaceState({ key: _key }, , url)n } else {n // 重新生成 keyn _key = genKey()n // 帶入新的 key 值n history.pushState({ key: _key }, , url)n }n // 保存 key 對應的位置n saveScrollPosition(_key)n } catch (e) {n // 達到限制了 則重新指定新的地址n window.location[replace ? assign : replace](url)n }n}n// 直接調用 pushState 傳入 replace 為 truenfunction replaceState (url: string) {n pushState(url, true)n}n

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

AbstractHistory

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

// ...nimport { History } from ./basennexport class AbstractHistory extends History {n index: number;n stack: Array<Route>;n// ...nn push (location: RawLocation) {n this.transitionTo(location, route => {n // 更新歷史堆棧信息n this.stack = this.stack.slice(0, this.index + 1).concat(route)n // 更新當前所處位置n this.index++n })n }nn replace (location: RawLocation) {n this.transitionTo(location, route => {n // 更新歷史堆棧信息 位置則不用更新 因為是 replace 操作n // 在堆棧中也是直接 replace 掉的n this.stack = this.stack.slice(0, this.index).concat(route)n })n }n // 對於 go 的模擬n go (n: number) {n // 新的歷史記錄位置n const targetIndex = this.index + nn // 超出返回了n if (targetIndex < 0 || targetIndex >= this.stack.length) {n returnn }n // 取得新的 route 對象n // 因為是和瀏覽器無關的 這裡得到的一定是已經訪問過的n const route = this.stack[targetIndex]n // 所以這裡直接調用 confirmTransition 了n // 而不是調用 transitionTo 還要走一遍 match 邏輯n this.confirmTransition(route, () => {n // 更新n this.index = targetIndexn this.updateRoute(route)n })n }nn ensureURL () {n // noopn }n}n

小結

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

歡迎拍磚。


推薦閱讀:

TAG:前端开发 | Vuejs |