關于vue-router的使用及實現(xiàn)原理
vue-router使用及實現(xiàn)原理
前端路由是直接找到與地址匹配的一個組件或對象并將其渲染出來。
改變?yōu)g覽器地址而不向服務器發(fā)出請求有兩種方式:
- 1. 在地址中加入#以欺騙瀏覽器,地址的改變是由于正在進行頁內(nèi)導航
- 2. 使用H5的window.history功能,使用URL的Hash來模擬一個完整的URL。
當打包構建應用時,Javascript 包會變得非常大,影響頁面加載。
如果我們能把不同路由對應的組件分割成不同的代碼塊,然后當路由被訪問的時候才加載對應組件,這樣就更加高效了。

目錄結構
先來看看整體的目錄結構

和流程相關的主要需要關注點的就是 components、history 目錄以及 create-matcher.js、create-route-map.js、index.js、install.js。
下面就從 basic 應用入口開始來分析 vue-router 的整個流程。
import Vue from 'vue'
import VueRouter from 'vue-router'
// 1. 插件
// 安裝 <router-view> and <router-link> 組件
// 且給當前應用下所有的組件都注入 $router and $route 對象
Vue.use(VueRouter)
// 2. 定義各個路由下使用的組件,簡稱路由組件
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
// 3. 創(chuàng)建 VueRouter 實例 router
const router = new VueRouter({
mode: 'history',
base: __dirname,
routes: [
{ path: '/', component: Home },
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
})
// 4. 創(chuàng)建 啟動應用
// 一定要確認注入了 router
// 在 <router-view> 中將會渲染路由組件
new Vue({
router,
template: `
<div id="app">
<h1>Basic</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/foo">/foo</router-link></li>
<li><router-link to="/bar">/bar</router-link></li>
<router-link tag="li" to="/bar">/bar</router-link>
</ul>
<router-view class="view"></router-view>
</div>
`
}).$mount('#app')作為插件
上邊代碼中關鍵的第 1 步,利用 Vue.js 提供的插件機制 .use(plugin) 來安裝 VueRouter,而這個插件機制則會調(diào)用該 plugin 對象的 install 方法(當然如果該 plugin 沒有該方法的話會把 plugin 自身作為函數(shù)來調(diào)用);下邊來看下 vue-router 這個插件具體的實現(xiàn)部分。
VueRouter 對象是在 src/index.js 中暴露出來的,這個對象有一個靜態(tài)的 install 方法:
/* @flow */
// 導入 install 模塊
import { install } from './install'
// ...
import { inBrowser, supportsHistory } from './util/dom'
// ...
export default class VueRouter {
// ...
}
// 賦值 install
VueRouter.install = install
// 自動使用插件
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}可以看到這是一個 Vue.js 插件的經(jīng)典寫法,給插件對象增加 install 方法用來安裝插件具體邏輯,同時在最后判斷下如果是在瀏覽器環(huán)境且存在 window.Vue 的話就會自動使用插件。
install 在這里是一個單獨的模塊,繼續(xù)來看同級下的 src/install.js 的主要邏輯:
// router-view router-link 組件
import View from './components/view'
import Link from './components/link'
// export 一個 Vue 引用
export let _Vue
// 安裝函數(shù)
export function install (Vue) {
if (install.installed) return
install.installed = true
// 賦值私有 Vue 引用
_Vue = Vue
// 注入 $router $route
Object.defineProperty(Vue.prototype, '$router', {
get () { return this.$root._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this.$root._route }
})
// beforeCreate mixin
Vue.mixin({
beforeCreate () {
// 判斷是否有 router
if (this.$options.router) {
// 賦值 _router
this._router = this.$options.router
// 初始化 init
this._router.init(this)
// 定義響應式的 _route 對象
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
}
})
// 注冊組件
Vue.component('router-view', View)
Vue.component('router-link', Link)
// ...
}這里就會有一些疑問了?
為啥要 export 一個 Vue 引用?
插件在打包的時候是肯定不希望把 vue 作為一個依賴包打進去的,但是呢又希望使用 Vue 對象本身的一些方法,此時就可以采用上邊類似的做法,在 install 的時候把這個變量賦值 Vue ,這樣就可以在其他地方使用 Vue 的一些方法而不必引入 vue 依賴包(前提是保證 install 后才會使用)。
通過給 Vue.prototype 定義 $router、$route 屬性就可以把他們注入到所有組件中嗎?
在 Vue.js 中所有的組件都是被擴展的 Vue 實例,也就意味著所有的組件都可以訪問到這個實例原型上定義的屬性。
beforeCreate mixin 這個在后邊創(chuàng)建 Vue 實例的時候再細說。
實例化 VueRouter
在入口文件中,首先要實例化一個 VueRouter ,然后將其傳入 Vue 實例的 options 中。
現(xiàn)在繼續(xù)來看在 src/index.js 中暴露出來的 VueRouter 類:
// ...
import { createMatcher } from './create-matcher'
// ...
export default class VueRouter {
// ...
constructor (options: RouterOptions = {}) {
this.app = null
this.options = options
this.beforeHooks = []
this.afterHooks = []
// 創(chuàng)建 match 匹配函數(shù)
this.match = createMatcher(options.routes || [])
// 根據(jù) mode 實例化具體的 History
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsHistory
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
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)
break
default:
assert(false, `invalid mode: ${mode}`)
}
}
// ...
}里邊包含了重要的一步:創(chuàng)建 match 匹配函數(shù)。
match 匹配函數(shù)
匹配函數(shù)是由 src/create-matcher.js 中的 createMatcher 創(chuàng)建的:
/* @flow */
import Regexp from 'path-to-regexp'
// ...
import { createRouteMap } from './create-route-map'
// ...
export function createMatcher (routes: Array<RouteConfig>): Matcher {
// 創(chuàng)建路由 map
const { pathMap, nameMap } = createRouteMap(routes)
// 匹配函數(shù)
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// ...
}
function redirect (
record: RouteRecord,
location: Location
): Route {
// ...
}
function alias (
record: RouteRecord,
location: Location,
matchAs: string
): Route {
// ...
}
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom)
}
// 返回
return match
}
// ...具體邏輯后續(xù)再具體分析,現(xiàn)在只需要理解為根據(jù)傳入的 routes 配置生成對應的路由 map,然后直接返回了 match 匹配函數(shù)。
繼續(xù)來看 src/create-route-map.js 中的 createRouteMap 函數(shù):
/* @flow */
import { assert, warn } from './util/warn'
import { cleanPath } from './util/path'
// 創(chuàng)建路由 map
export function createRouteMap (routes: Array<RouteConfig>): {
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// path 路由 map
const pathMap: Dictionary<RouteRecord> = Object.create(null)
// name 路由 map
const nameMap: Dictionary<RouteRecord> = Object.create(null)
// 遍歷路由配置對象 增加 路由記錄
routes.forEach(route => {
addRouteRecord(pathMap, nameMap, route)
})
return {
pathMap,
nameMap
}
}
// 增加 路由記錄 函數(shù)
function addRouteRecord (
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
// 獲取 path 、name
const { path, name } = route
assert(path != null, `"path" is required in a route configuration.`)
// 路由記錄 對象
const record: RouteRecord = {
path: normalizePath(path, parent),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {}
}
// 嵌套子路由 則遞歸增加 記錄
if (route.children) {
// ...
route.children.forEach(child => {
addRouteRecord(pathMap, nameMap, child, record)
})
}
// 處理別名 alias 邏輯 增加對應的 記錄
if (route.alias !== undefined) {
if (Array.isArray(route.alias)) {
route.alias.forEach(alias => {
addRouteRecord(pathMap, nameMap, { path: alias }, parent, record.path)
})
} else {
addRouteRecord(pathMap, nameMap, { path: route.alias }, parent, record.path)
}
}
// 更新 path map
pathMap[record.path] = record
// 更新 name map
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
} else {
warn(false, `Duplicate named routes definition: { name: "${name}", path: "${record.path}" }`)
}
}
}
function normalizePath (path: string, parent?: RouteRecord): string {
path = path.replace(/\/$/, '')
if (path[0] === '/') return path
if (parent == null) return path
return cleanPath(`${parent.path}/${path}`)
}可以看出主要做的事情就是根據(jù)用戶路由配置對象生成普通的根據(jù) path 來對應的路由記錄以及根據(jù) name 來對應的路由記錄的 map,方便后續(xù)匹配對應。
實例化 History
這也是很重要的一步,所有的 History 類都是在 src/history/ 目錄下,現(xiàn)在呢不需要關心具體的每種 History 的具體實現(xiàn)上差異,只需要知道他們都是繼承自 src/history/base.js 中的 History 類的:
/* @flow */
// ...
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
import { START, isSameRoute } from '../util/route'
// 這里從之前分析過的 install.js 中 export _Vue
import { _Vue } from '../install'
export class History {
// ...
constructor (router: VueRouter, base: ?string) {
this.router = router
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
}
// ...
}
// 得到 base 值
function normalizeBase (base: ?string): string {
if (!base) {
if (inBrowser) {
// respect <base> tag
const baseEl = document.querySelector('base')
base = baseEl ? baseEl.getAttribute('href') : '/'
} else {
base = '/'
}
}
// make sure there's the starting slash
if (base.charAt(0) !== '/') {
base = '/' + base
}
// remove trailing slash
return base.replace(/\/$/, '')
}
// ...實例化完了 VueRouter,下邊就該看看 Vue 實例了。
實例化 Vue
new Vue({
router,
template: `
<div id="app">
<h1>Basic</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/foo">/foo</router-link></li>
<li><router-link to="/bar">/bar</router-link></li>
<router-link tag="li" to="/bar">/bar</router-link>
</ul>
<router-view class="view"></router-view>
</div>
`
}).$mount('#app')options 中傳入了 router,以及模板;還記得上邊沒具體分析的 beforeCreate mixin 嗎,此時創(chuàng)建一個 Vue 實例,對應的 beforeCreate 鉤子就會被調(diào)用:
// ...
Vue.mixin({
beforeCreate () {
// 判斷是否有 router
if (this.$options.router) {
// 賦值 _router
this._router = this.$options.router
// 初始化 init
this._router.init(this)
// 定義響應式的 _route 對象
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
}
})具體來說,首先判斷實例化時 options 是否包含 router,如果包含也就意味著是一個帶有路由配置的實例被創(chuàng)建了,此時才有必要繼續(xù)初始化路由相關邏輯。
然后給當前實例賦值_router,這樣在訪問原型上的 $router 的時候就可以得到 router 了。
下邊來看里邊兩個關鍵:router.init 和 定義響應式的 _route 對象。
router.init
然后來看 router 的 init 方法就干了哪些事情,依舊是在 src/index.js 中:
/* @flow */
import { install } from './install'
import { createMatcher } from './create-matcher'
import { HashHistory, getHash } from './history/hash'
import { HTML5History, getLocation } from './history/html5'
import { AbstractHistory } from './history/abstract'
import { inBrowser, supportsHistory } from './util/dom'
import { assert } from './util/warn'
export default class VueRouter {
// ...
init (app: any /* Vue component instance */) {
// ...
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(getLocation(history.base))
} else if (history instanceof HashHistory) {
history.transitionTo(getHash(), () => {
window.addEventListener('hashchange', () => {
history.onHashChange()
})
})
}
history.listen(route => {
this.app._route = route
})
}
// ...
}
// ...可以看到初始化主要就是給 app 賦值,針對于 HTML5History 和 HashHistory 特殊處理,因為在這兩種模式下才有可能存在進入時候的不是默認頁,需要根據(jù)當前瀏覽器地址欄里的 path 或者 hash 來激活對應的路由,此時就是通過調(diào)用 transitionTo 來達到目的;而且此時還有個注意點是針對于 HashHistory 有特殊處理,為什么不直接在初始化 HashHistory 的時候監(jiān)聽 hashchange 事件呢?
這個是為了修復vuejs/vue-router#725這個 bug 而這樣做的,簡要來說就是說如果在 beforeEnter 這樣的鉤子函數(shù)中是異步的話,beforeEnter 鉤子就會被觸發(fā)兩次,原因是因為在初始化的時候如果此時的 hash 值不是以 / 開頭的話就會補上 #/,這個過程會觸發(fā) hashchange 事件,所以會再走一次生命周期鉤子,也就意味著會再次調(diào)用 beforeEnter 鉤子函數(shù)。
來看看這個具體的 transitionTo 方法的大概邏輯,在 src/history/base.js 中:
/* @flow */
import type VueRouter from '../index'
import { warn } from '../util/warn'
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
import { START, isSameRoute } from '../util/route'
import { _Vue } from '../install'
export class History {
// ...
transitionTo (location: RawLocation, cb?: Function) {
// 調(diào)用 match 得到匹配的 route 對象
const route = this.router.match(location, this.current)
// 確認過渡
this.confirmTransition(route, () => {
// 更新當前 route 對象
this.updateRoute(route)
cb && cb(route)
// 子類實現(xiàn)的更新url地址
// 對于 hash 模式的話 就是更新 hash 的值
// 對于 history 模式的話 就是利用 pushstate / replacestate 來更新
// 瀏覽器地址
this.ensureURL()
})
}
// 確認過渡
confirmTransition (route: Route, cb: Function) {
const current = this.current
// 如果是相同 直接返回
if (isSameRoute(route, current)) {
this.ensureURL()
return
}
// 交叉比對當前路由的路由記錄和現(xiàn)在的這個路由的路由記錄
// 以便能準確得到父子路由更新的情況下可以確切的知道
// 哪些組件需要更新 哪些不需要更新
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
每一個隊列執(zhí)行的 iterator 函數(shù)
const iterator = (hook: NavigationGuard, next) => {
// 確保期間還是當前路由
if (this.pending !== route) return
hook(route, current, (to: any) => {
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
} else if (typeof to === 'string' || typeof to === 'object') {
// next('/') or next({ path: '/' }) -> redirect
this.push(to)
} else {
// confirm transition and pass on the value
next(to)
}
})
}
// 執(zhí)行隊列
runQueue(queue, iterator, () => {
const postEnterCbs = []
// 組件內(nèi)的鉤子
const enterGuards = extractEnterGuards(activated, postEnterCbs, () => {
return this.current === route
})
// 在上次的隊列執(zhí)行完成后再執(zhí)行組件內(nèi)的鉤子
// 因為需要等異步組件以及是OK的情況下才能執(zhí)行
runQueue(enterGuards, iterator, () => {
// 確保期間還是當前路由
if (this.pending === route) {
this.pending = null
cb(route)
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => cb())
})
}
})
})
}
// 更新當前 route 對象
updateRoute (route: Route) {
const prev = this.current
this.current = route
// 注意 cb 的值
// 每次更新都會調(diào)用 下邊需要用到!
this.cb && this.cb(route)
// 執(zhí)行 after hooks 回調(diào)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
}
// ...可以看到整個過程就是執(zhí)行約定的各種鉤子以及處理異步組件問題,這里有一些具體函數(shù)具體細節(jié)被忽略掉了(后續(xù)會具體分析)但是不影響具體理解這個流程。
但是需要注意一個概念:路由記錄,每一個路由 route 對象都對應有一個 matched 屬性,它對應的就是路由記錄,他的具體含義在調(diào)用 match() 中有處理;通過之前的分析可以知道這個 match 是在 src/create-matcher.js 中的:
// ...
import { createRoute } from './util/route'
import { createRouteMap } from './create-route-map'
// ...
export function createMatcher (routes: Array<RouteConfig>): Matcher {
const { pathMap, nameMap } = createRouteMap(routes)
// 關鍵的 match
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute)
const { name } = location
// 命名路由處理
if (name) {
// nameMap[name] = 路由記錄
const record = nameMap[name]
const paramNames = getParams(record.path)
// ...
if (record) {
location.path = fillParams(record.path, location.params, `named route "${name}"`)
// 創(chuàng)建 route
return _createRoute(record, location, redirectedFrom)
}
} else if (location.path) {
// 普通路由處理
location.params = {}
for (const path in pathMap) {
if (matchRoute(path, location.params, location.path)) {
// 匹配成功 創(chuàng)建route
// pathMap[path] = 路由記錄
return _createRoute(pathMap[path], location, redirectedFrom)
}
}
}
// no match
return _createRoute(null, location)
}
// ...
// 創(chuàng)建路由
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
// 重定向和別名邏輯
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
// 創(chuàng)建路由對象
return createRoute(record, location, redirectedFrom)
}
return match
}
// ...路由記錄在分析 match 匹配函數(shù)那里以及分析過了,這里還需要了解下創(chuàng)建路由對象的 createRoute,存在于 src/util/route.js 中:
// ...
export function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
// 可以看到就是一個被凍結的普通對象
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query: location.query || {},
params: location.params || {},
fullPath: getFullPath(location),
// 根據(jù)記錄層級的得到所有匹配的 路由記錄
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom)
}
return Object.freeze(route)
}
// ...
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
// ...回到之前看的 init,最后調(diào)用了 history.listen 方法:
history.listen(route => {
this.app._route = route
})listen 方法很簡單就是設置下當前歷史對象的 cb 的值, 在之前分析 transitionTo 的時候已經(jīng)知道在 history 更新完畢的時候調(diào)用下這個 cb。
然后看這里設置的這個函數(shù)的作用就是更新下當前應用實例的 _route 的值,更新這個有什么用呢?請看下段落的分析。
defineReactive 定義 _route
繼續(xù)回到 beforeCreate 鉤子函數(shù)中,在最后通過 Vue 的工具方法給當前應用實例定義了一個響應式的 _route 屬性,值就是獲取的 this._router.history.current,也就是當前 history 實例的當前活動路由對象。
給應用實例定義了這么一個響應式的屬性值也就意味著如果該屬性值發(fā)生了變化,就會觸發(fā)更新機制,繼而調(diào)用應用實例的 render 重新渲染。
還記得上一段結尾留下的疑問,也就是 history 每次更新成功后都會去更新應用實例的 _route 的值,也就意味著一旦 history 發(fā)生改變就會觸發(fā)更新機制調(diào)用應用實例的 render 方法進行重新渲染。
router-link 和 router-view 組件
new Vue({
router,
template: `
<div id="app">
<h1>Basic</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/foo">/foo</router-link></li>
<li><router-link to="/bar">/bar</router-link></li>
<router-link tag="li" to="/bar">/bar</router-link>
</ul>
<router-view class="view"></router-view>
</div>
`
}).$mount('#app')可以看到這個實例的 template 中包含了兩個自定義組件:router-link 和 router-view。
router-view 組件
router-view 組件比較簡單,所以這里就先來分析它,他是在源碼的 src/components/view.js 中定義的:
export default {
name: 'router-view',
functional: true, // 功能組件 純粹渲染
props: {
name: {
type: String,
default: 'default' // 默認default 默認命名視圖的name
}
},
render (h, { props, children, parent, data }) {
// 解決嵌套深度問題
data.routerView = true
// route 對象
const route = parent.$route
// 緩存
const cache = parent._routerViewCache || (parent._routerViewCache = {})
let depth = 0
let inactive = false
// 當前組件的深度
while (parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
處理 keepalive 邏輯
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
// 得到相匹配的當前組件層級的 路由記錄
const matched = route.matched[depth]
if (!matched) {
return h()
}
// 得到要渲染組件
const name = props.name
const component = inactive
? cache[name]
: (cache[name] = matched.components[name])
if (!inactive) {
// 非 keepalive 模式下 每次都需要設置鉤子
// 進而更新(賦值&銷毀)匹配了的實例元素
const hooks = data.hook || (data.hook = {})
hooks.init = vnode => {
matched.instances[name] = vnode.child
}
hooks.prepatch = (oldVnode, vnode) => {
matched.instances[name] = vnode.child
}
hooks.destroy = vnode => {
if (matched.instances[name] === vnode.child) {
matched.instances[name] = undefined
}
}
}
// 調(diào)用 createElement 函數(shù) 渲染匹配的組件
return h(component, data, children)
}
}可以看到邏輯還是比較簡單的,拿到匹配的組件進行渲染就可以了。
router-link 組件
再來看看導航鏈接組件,他在源碼的 src/components/link.js 中定義的:
// ...
import { createRoute, isSameRoute, isIncludedRoute } from '../util/route'
// ...
export default {
name: 'router-link',
props: {
// 傳入的組件屬性們
to: { // 目標路由的鏈接
type: toTypes,
required: true
},
// 創(chuàng)建的html標簽
tag: {
type: String,
default: 'a'
},
// 完整模式,如果為 true 那么也就意味著
// 絕對相等的路由才會增加 activeClass
// 否則是包含關系
exact: Boolean,
// 在當前(相對)路徑附加路徑
append: Boolean,
// 如果為 true 則調(diào)用 router.replace() 做替換歷史操作
replace: Boolean,
// 鏈接激活時使用的 CSS 類名
activeClass: String
},
render (h: Function) {
// 得到 router 實例以及當前激活的 route 對象
const router = this.$router
const current = this.$route
const to = normalizeLocation(this.to, current, this.append)
// 根據(jù)當前目標鏈接和當前激活的 route匹配結果
const resolved = router.match(to, current)
const fullPath = resolved.redirectedFrom || resolved.fullPath
const base = router.history.base
// 創(chuàng)建的 href
const href = createHref(base, fullPath, router.mode)
const classes = {}
// 激活class 優(yōu)先當前組件上獲取 要么就是 router 配置的 linkActiveClass
// 默認 router-link-active
const activeClass = this.activeClass || router.options.linkActiveClass || 'router-link-active'
// 相比較目標
// 因為有命名路由 所有不一定有path
const compareTarget = to.path ? createRoute(null, to) : resolved
// 如果嚴格模式的話 就判斷是否是相同路由(path query params hash)
// 否則就走包含邏輯(path包含,query包含 hash為空或者相同)
classes[activeClass] = this.exact
? isSameRoute(current, compareTarget)
: isIncludedRoute(current, compareTarget)
// 事件綁定
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)
}
}
}
// 創(chuàng)建元素需要附加的數(shù)據(jù)們
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
}
}
// 創(chuàng)建元素
return h(this.tag, data, this.$slots.default)
}
}
function findAnchor (children) {
if (children) {
let child
for (let i = 0; i < children.length; i++) {
child = children[i]
if (child.tag === 'a') {
return child
}
if (child.children && (child = findAnchor(child.children))) {
return child
}
}
}
}
function createHref (base, fullPath, mode) {
var path = mode === 'hash' ? '/#' + fullPath : fullPath
return base ? cleanPath(base + path) : path
}可以看出 router-link 組件就是在其點擊的時候根據(jù)設置的 to 的值去調(diào)用 router 的 push 或者 replace 來更新路由的,同時呢,會檢查自身是否和當前路由匹配(嚴格匹配和包含匹配)來決定自身的 activeClass 是否添加。

總結
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
vue2.x 通過后端接口代理,獲取qq音樂api的數(shù)據(jù)示例
今天小編就為大家分享一篇vue2.x 通過后端接口代理,獲取qq音樂api的數(shù)據(jù)示例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-10-10

