做者:滴滴公共前端團隊 - dolymoodjavascript
在上篇中介紹了 vue-router 的總體流程,可是具體的 history
部分沒有具體分析,本文就具體分析下和 history
相關的細節。html
經過總體流程能夠知道在路由實例化的時候會根據當前 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
首先就看默認的 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
則是利用 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
理論上來講這種模式是用於 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 改變能夠有兩種,一種是用戶點擊連接元素,一種是更新瀏覽器自己的前進後退導航來更新。
先來講瀏覽器導航發生變化的時候會觸發對應的事件:對於 hash 模式而言觸發 window
的 hashchange
事件,對於 history 模式而言則觸發 window
的 popstate
事件。
先說 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
以及 replaceHash
、getLocation
、handleScroll
後邊統一分析。
再看用戶點擊連接交互,即點擊了 <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
事件,當用戶觸發後,會調用 router
的 push
或 replace
方法來更新路由。下邊就來看看這兩個方法定義,在 src/index.js
中:
push (location: RawLocation) {
this.history.push(location)
}
replace (location: RawLocation) {
this.history.replace(location)
}複製代碼
能夠看到其實他們只是代理而已,真正作事情的仍是 history
來作,下面就分別把 history 的三種模式下的這兩個方法進行分析。
直接看代碼:
// ...
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
實例,從上次的分析中能夠知道它就是匹配到的路由記錄的合集;這裏從執行順序上來看有這些 resolveQueue
、extractLeaveGuards
、resolveAsyncComponents
、runQueue
關鍵方法。
首先來看 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 值,push
和 replace
的場景下都是一個效果。
回到 confirmTransition
的回調,最後還作了一件事情 ensureURL
:
ensureURL (push?: boolean) {
const current = this.current.fullPath
if (getHash() !== current) {
push ? pushHash(current) : replaceHash(current)
}
}複製代碼
此時 push
爲 undefined
,因此調用 replaceHash
更新瀏覽器 hash 值。
整個的流程和 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 來更新。
抽象模式是屬於最簡單的處理了,由於不涉及和瀏覽器地址相關記錄關聯在一塊兒;總體流程依舊和 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」或掃描下面的二維碼