vue-router源碼分析-整體流程
本文作者: 苗典,來自滴滴出行公共前端團隊。
在現在單頁應用這麼火爆的年代,路由已經成為了我們開發應用必不可少的利器;而縱觀各大框架,都會有對應的強大路由支持。Vue.js 因其性能、通用、易用、體積、學習成本低等特點已經成為了廣大前端們的新寵,而其對應的路由 vue-router 也是設計的簡單好用,功能強大。本文就從源碼來分析下 Vue.js 官方路由 vue-router 的整體流程。
本文主要以 vue-router 的 2.0.3 版本來進行分析。
首先來張整體的圖:
先對整體有個大概的印象,下邊就以官方倉庫下 examples/basic 基礎例子來一點點具體分析整個流程。
目錄結構
先來看看整體的目錄結構:
和流程相關的主要需要關注點的就是 components、history 目錄以及 create-matcher.js、create-route-map.js、index.js、install.js。下面就從 basic 應用入口開始來分析 vue-router 的整個流程。
入口
首先看應用入口的代碼部分:
import Vue from vuenimport VueRouter from vue-routernn// 1. 插件n// 安裝 <router-view> and <router-link> 組件n// 且給當前應用下所有的組件都注入 $router and $route 對象nVue.use(VueRouter)nn// 2. 定義各個路由下使用的組件,簡稱路由組件nconst Home = { template: <div>home</div> }nconst Foo = { template: <div>foo</div> }nconst Bar = { template: <div>bar</div> }nn// 3. 創建 VueRouter 實例 routernconst router = new VueRouter({n mode: history,n base: __dirname,n routes: [n { path: /, component: Home },n { path: /foo, component: Foo },n { path: /bar, component: Bar }n ]n})nn// 4. 創建 啟動應用n// 一定要確認注入了 router n// 在 <router-view> 中將會渲染路由組件nnew Vue({n router,n template: `n <div id="app">n <h1>Basic</h1>n <ul>n <li><router-link to="/">/</router-link></li>n <li><router-link to="/foo">/foo</router-link></li>n <li><router-link to="/bar">/bar</router-link></li>n <router-link tag="li" to="/bar">/bar</router-link>n </ul>n <router-view class="view"></router-view>n </div>n `n}).$mount(#app)n
作為插件
上邊代碼中關鍵的第 1 步,利用 Vue.js 提供的插件機制 .use(plugin) 來安裝 VueRouter,而這個插件機制則會調用該 plugin 對象的 install 方法(當然如果該 plugin 沒有該方法的話會把 plugin 自身作為函數來調用);下邊來看下 vue-router 這個插件具體的實現部分。
VueRouter 對象是在 src/index.js 中暴露出來的,這個對象有一個靜態的 install 方法:
/* @flow */n// 導入 install 模塊nimport { install } from ./installn// ...nimport { inBrowser, supportsHistory } from ./util/domn// ...nnexport default class VueRouter {n// ...n}nn// 賦值 installnVueRouter.install = installnn// 自動使用插件nif (inBrowser && window.Vue) {n window.Vue.use(VueRouter)n}n
可以看到這是一個 Vue.js 插件的經典寫法,給插件對象增加 install 方法用來安裝插件具體邏輯,同時在最後判斷下如果是在瀏覽器環境且存在 window.Vue 的話就會自動使用插件。
install 在這裡是一個單獨的模塊,繼續來看同級下的 src/install.js 的主要邏輯:
// router-view router-link 組件nimport View from ./components/viewnimport Link from ./components/linknn// export 一個 Vue 引用nexport let _Vuenn// 安裝函數nexport function install (Vue) {n if (install.installed) returnn install.installed = truen n // 賦值私有 Vue 引用n _Vue = Vuenn // 注入 $router $routen Object.defineProperty(Vue.prototype, $router, {n get () { return this.$root._router }n })nn Object.defineProperty(Vue.prototype, $route, {n get () { return this.$root._route }n })n // beforeCreate mixinn Vue.mixin({n beforeCreate () {n // 判斷是否有 routern if (this.$options.router) {n // 賦值 _routern this._router = this.$options.routern // 初始化 initn this._router.init(this)n // 定義響應式的 _route 對象n Vue.util.defineReactive(this, _route, this._router.history.current)n }n }n })nn // 註冊組件n Vue.component(router-view, View)n Vue.component(router-link, Link)n// ...n}n
這裡就會有一些疑問了?
- 為啥要 export 一個 Vue 引用?
插件在打包的時候是肯定不希望把 vue 作為一個依賴包打進去的,但是呢又希望使用 Vue 對象本身的一些方法,此時就可以採用上邊類似的做法,在 install 的時候把這個變數賦值 Vue ,這樣就可以在其他地方使用 Vue 的一些方法而不必引入 vue 依賴包(前提是保證 install 後才會使用)。
- 通過給 Vue.prototype 定義 $router、$route 屬性就可以把他們注入到所有組件中嗎?
在 Vue.js 中所有的組件都是被擴展的 Vue 實例,也就意味著所有的組件都可以訪問到這個實例原型上定義的屬性。
beforeCreate mixin 這個在後邊創建 Vue 實例的時候再細說。
實例化 VueRouter
在入口文件中,首先要實例化一個 VueRouter ,然後將其傳入 Vue 實例的 options 中。現在繼續來看在 src/index.js 中暴露出來的 VueRouter類:
// ...nimport { createMatcher } from ./create-matchern// ...nexport default class VueRouter {n// ...n constructor (options: RouterOptions = {}) {n this.app = nulln this.options = optionsn this.beforeHooks = []n this.afterHooks = []n // 創建 match 匹配函數n this.match = createMatcher(options.routes || [])n // 根據 mode 實例化具體的 Historyn let mode = options.mode || hashn this.fallback = mode === history && !supportsHistoryn if (this.fallback) {n mode = hashn }n if (!inBrowser) {n mode = abstractn }n this.mode = modenn switch (mode) {n case history:n this.history = new HTML5History(this, options.base)n breakn case hash:n 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}n
裡邊包含了重要的一步:創建 match 匹配函數。
match 匹配函數
匹配函數是由 src/create-matcher.js 中的 createMatcher 創建的:
/* @flow */nnimport Regexp from path-to-regexpn// ...nimport { createRouteMap } from ./create-route-mapn// ...nnexport function createMatcher (routes: Array<RouteConfig>): Matcher {n // 創建路由 mapn const { pathMap, nameMap } = createRouteMap(routes)n // 匹配函數n function match (n raw: RawLocation,n currentRoute?: Route,n redirectedFrom?: Locationn ): Route {n// ...n }nn function redirect (n record: RouteRecord,n location: Locationn ): Route {n// ...n }nn function alias (n record: RouteRecord,n location: Location,n matchAs: stringn ): Route {n// ...n }nn function _createRoute (n record: ?RouteRecord,n location: Location,n redirectedFrom?: Locationn ): Route {n if (record && record.redirect) {n return redirect(record, redirectedFrom || location)n }n if (record && record.matchAs) {n return alias(record, location, record.matchAs)n }n return createRoute(record, location, redirectedFrom)n }n // 返回n return matchn}n// ...n
具體邏輯後續再具體分析,現在只需要理解為根據傳入的 routes 配置生成對應的路由 map,然後直接返回了 match 匹配函數。
繼續來看 src/create-route-map.js 中的 createRouteMap 函數:
/* @flow */nnimport { assert, warn } from ./util/warnnimport { cleanPath } from ./util/pathnn// 創建路由 mapnexport function createRouteMap (routes: Array<RouteConfig>): {n pathMap: Dictionary<RouteRecord>,n nameMap: Dictionary<RouteRecord>n} {n // path 路由 mapn const pathMap: Dictionary<RouteRecord> = Object.create(null)n // name 路由 mapn const nameMap: Dictionary<RouteRecord> = Object.create(null)n // 遍歷路由配置對象 增加 路由記錄n routes.forEach(route => {n addRouteRecord(pathMap, nameMap, route)n })nn return {n pathMap,n nameMapn }n}nn// 增加 路由記錄 函數nfunction addRouteRecord (n pathMap: Dictionary<RouteRecord>,n nameMap: Dictionary<RouteRecord>,n route: RouteConfig,n parent?: RouteRecord,n matchAs?: stringn) {n // 獲取 path 、namen const { path, name } = routen assert(path != null, `"path" is required in a route configuration.`)n // 路由記錄 對象n const record: RouteRecord = {n path: normalizePath(path, parent),n components: route.components || { default: route.component },n instances: {},n name,n parent,n matchAs,n redirect: route.redirect,n beforeEnter: route.beforeEnter,n meta: route.meta || {}n }n // 嵌套子路由 則遞歸增加 記錄n if (route.children) {n// ...n route.children.forEach(child => {n addRouteRecord(pathMap, nameMap, child, record)n })n }n // 處理別名 alias 邏輯 增加對應的 記錄n if (route.alias !== undefined) {n if (Array.isArray(route.alias)) {n route.alias.forEach(alias => {n addRouteRecord(pathMap, nameMap, { path: alias }, parent, record.path)n })n } else {n addRouteRecord(pathMap, nameMap, { path: route.alias }, parent, record.path)n }n }n // 更新 path mapn pathMap[record.path] = recordn // 更新 name mapn if (name) {n if (!nameMap[name]) {n nameMap[name] = recordn } else {n warn(false, `Duplicate named routes definition: { name: "${name}", path: "${record.path}" }`)n }n }n}nnfunction normalizePath (path: string, parent?: RouteRecord): string {n path = path.replace(//$/, )n if (path[0] === /) return pathn if (parent == null) return pathn return cleanPath(`${parent.path}/${path}`)n}n
可以看出主要做的事情就是根據用戶路由配置對象生成普通的根據 path 來對應的路由記錄以及根據 name 來對應的路由記錄的 map,方便後續匹配對應。
實例化 History
這也是很重要的一步,所有的 History 類都是在 src/history/ 目錄下,現在呢不需要關心具體的每種 History 的具體實現上差異,只需要知道他們都是繼承自 src/history/base.js 中的 History 類的:
/* @flow */nn// ...nimport { inBrowser } from ../util/domnimport { runQueue } from ../util/asyncnimport { START, isSameRoute } from ../util/routen// 這裡從之前分析過的 install.js 中 export _Vuenimport { _Vue } from ../installnnexport class History {n// ...n constructor (router: VueRouter, base: ?string) {n this.router = routern this.base = normalizeBase(base)n // start with a route object that stands for "nowhere"n this.current = STARTn this.pending = nulln }n// ...n}nn// 得到 base 值nfunction normalizeBase (base: ?string): string {n if (!base) {n if (inBrowser) {n // respect <base> tagn const baseEl = document.querySelector(base)n base = baseEl ? baseEl.getAttribute(href) : /n } else {n base = /n }n }n // make sure theres the starting slashn if (base.charAt(0) !== /) {n base = / + basen }n // remove trailing slashn return base.replace(//$/, )n}n// ...n
實例化完了 VueRouter,下邊就該看看 Vue 實例了。
實例化 Vue
實例化很簡單:
new Vue({n router,n template: `n <div id="app">n <h1>Basic</h1>n <ul>n <li><router-link to="/">/</router-link></li>n <li><router-link to="/foo">/foo</router-link></li>n <li><router-link to="/bar">/bar</router-link></li>n <router-link tag="li" to="/bar">/bar</router-link>n </ul>n <router-view class="view"></router-view>n </div>n `n}).$mount(#app)n
options 中傳入了 router,以及模板;還記得上邊沒具體分析的 beforeCreate mixin 嗎,此時創建一個 Vue 實例,對應的 beforeCreate鉤子就會被調用:
// ...n Vue.mixin({n beforeCreate () {n // 判斷是否有 routern if (this.$options.router) {n // 賦值 _routern this._router = this.$options.routern // 初始化 initn this._router.init(this)n // 定義響應式的 _route 對象n Vue.util.defineReactive(this, _route, this._router.history.current)n }n }n })n
具體來說,首先判斷實例化時 options 是否包含 router,如果包含也就意味著是一個帶有路由配置的實例被創建了,此時才有必要繼續初始化路由相關邏輯。然後給當前實例賦值 _router,這樣在訪問原型上的 $router 的時候就可以得到 router 了。
下邊來看裡邊兩個關鍵:router.init 和 定義響應式的 _route 對象。
router.init
然後來看 router 的 init 方法就幹了哪些事情,依舊是在 src/index.js中:
/* @flow */nnimport { install } from ./installnimport { createMatcher } from ./create-matchernimport { HashHistory, getHash } from ./history/hashnimport { HTML5History, getLocation } from ./history/html5nimport { AbstractHistory } from ./history/abstractnimport { inBrowser, supportsHistory } from ./util/domnimport { assert } from ./util/warnnnexport default class VueRouter {n// ...n init (app: any /* Vue component instance */) {n// ...n this.app = appnn const history = this.historynn if (history instanceof HTML5History) {n history.transitionTo(getLocation(history.base))n } else if (history instanceof HashHistory) {n history.transitionTo(getHash(), () => {n window.addEventListener(hashchange, () => {n history.onHashChange()n })n })n }nn history.listen(route => {n this.app._route = routen })n }n// ...n}n// ...n
可以看到初始化主要就是給 app 賦值,針對於 HTML5History 和 HashHistory 特殊處理,因為在這兩種模式下才有可能存在進入時候的不是默認頁,需要根據當前瀏覽器地址欄里的 path 或者 hash 來激活對應的路由,此時就是通過調用 transitionTo 來達到目的;而且此時還有個注意點是針對於 HashHistory 有特殊處理,為什麼不直接在初始化 HashHistory 的時候監聽 hashchange 事件呢?這個是為了修復https://github.com/vuejs/vue-router/issues/725這個 bug 而這樣做的,簡要來說就是說如果在 beforeEnter 這樣的鉤子函數中是非同步的話,beforeEnter 鉤子就會被觸發兩次,原因是因為在初始化的時候如果此時的 hash 值不是以 / 開頭的話就會補上 #/,這個過程會觸發 hashchange 事件,所以會再走一次生命周期鉤子,也就意味著會再次調用 beforeEnter 鉤子函數。
來看看這個具體的 transitionTo 方法的大概邏輯,在 src/history/base.js 中:
/* @flow */nnimport type VueRouter from ../indexnimport { warn } from ../util/warnnimport { inBrowser } from ../util/domnimport { runQueue } from ../util/asyncnimport { START, isSameRoute } from ../util/routenimport { _Vue } from ../installnnexport class History {n// ...n 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 // 交叉比對當前路由的路由記錄和現在的這個路由的路由記錄n // 以便能準確得到父子路由更新的情況下可以確切的知道n // 哪些組件需要更新 哪些不需要更新n const {n deactivated,n activatedn } = resolveQueue(this.current.matched, route.matched)n n // 整個切換周期的隊列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 if (this.pending !== route) returnn hook(route, current, (to: any) => {n if (to === false) {n // next(false) -> abort navigation, ensure current URLn this.ensureURL(true)n } else if (typeof to === string || typeof to === object) {n // next(/) or next({ path: / }) -> redirectn this.push(to)n } else {n // confirm transition and pass on the valuen next(to)n }n })n }n // 執行隊列n runQueue(queue, iterator, () => {n const postEnterCbs = []n // 組件內的鉤子n const enterGuards = extractEnterGuards(activated, postEnterCbs, () => {n return this.current === routen })n // 在上次的隊列執行完成後再執行組件內的鉤子n // 因為需要等非同步組件以及是OK的情況下才能執行n runQueue(enterGuards, iterator, () => {n // 確保期間還是當前路由n if (this.pending === route) {n this.pending = nulln cb(route)n this.router.app.$nextTick(() => {n postEnterCbs.forEach(cb => cb())n })n }n })n })n }n // 更新當前 route 對象n updateRoute (route: Route) {n const prev = this.currentn this.current = routen // 注意 cb 的值 n // 每次更新都會調用 下邊需要用到!n this.cb && this.cb(route)n // 執行 after hooks 回調n this.router.afterHooks.forEach(hook => {n hook && hook(route, prev)n })n }n}n// ...n
可以看到整個過程就是執行約定的各種鉤子以及處理非同步組件問題,這裡有一些具體函數具體細節被忽略掉了(後續會具體分析)但是不影響具體理解這個流程。但是需要注意一個概念:路由記錄,每一個路由 route 對象都對應有一個 matched 屬性,它對應的就是路由記錄,他的具體含義在調用 match() 中有處理;通過之前的分析可以知道這個 match 是在 src/create-matcher.js 中的:
// ...nimport { createRoute } from ./util/routenimport { createRouteMap } from ./create-route-mapn// ...nexport function createMatcher (routes: Array<RouteConfig>): Matcher {n const { pathMap, nameMap } = createRouteMap(routes)n // 關鍵的 matchn function match (n raw: RawLocation,n currentRoute?: Route,n redirectedFrom?: Locationn ): Route {n const location = normalizeLocation(raw, currentRoute)n const { name } = locationnn // 命名路由處理n if (name) {n // nameMap[name] = 路由記錄n const record = nameMap[name]n const paramNames = getParams(record.path)n// ...n if (record) {n location.path = fillParams(record.path, location.params, `named route "${name}"`)n // 創建 routen return _createRoute(record, location, redirectedFrom)n }n } else if (location.path) {n // 普通路由處理n location.params = {}n for (const path in pathMap) {n if (matchRoute(path, location.params, location.path)) {n // 匹配成功 創建routen // pathMap[path] = 路由記錄n return _createRoute(pathMap[path], location, redirectedFrom)n }n }n }n // no matchn return _createRoute(null, location)n }n// ...n // 創建路由n function _createRoute (n record: ?RouteRecord,n location: Location,n redirectedFrom?: Locationn ): Route {n // 重定向和別名邏輯n if (record && record.redirect) {n return redirect(record, redirectedFrom || location)n }n if (record && record.matchAs) {n return alias(record, location, record.matchAs)n }n // 創建路由對象n return createRoute(record, location, redirectedFrom)n }nn return matchn}n// ...n
路由記錄在分析 match 匹配函數那裡以及分析過了,這裡還需要了解下創建路由對象的 createRoute,存在於 src/util/route.js 中:
// ...nexport function createRoute (n record: ?RouteRecord,n location: Location,n redirectedFrom?: Locationn): Route {n // 可以看到就是一個被凍結的普通對象n const route: Route = {n name: location.name || (record && record.name),n meta: (record && record.meta) || {},n path: location.path || /,n hash: location.hash || ,n query: location.query || {},n params: location.params || {},n fullPath: getFullPath(location),n // 根據記錄層級的得到所有匹配的 路由記錄n matched: record ? formatMatch(record) : []n }n if (redirectedFrom) {n route.redirectedFrom = getFullPath(redirectedFrom)n }n return Object.freeze(route)n}n// ...nfunction formatMatch (record: ?RouteRecord): Array<RouteRecord> {n const res = []n while (record) {n res.unshift(record)n record = record.parentn }n return resn}n// ...n
回到之前看的 init,最後調用了 history.listen 方法:
history.listen(route => {n this.app._route = routen})n
listen 方法很簡單就是設置下當前歷史對象的 cb 的值, 在之前分析 transitionTo 的時候已經知道在 history 更新完畢的時候調用下這個 cb。然後看這裡設置的這個函數的作用就是更新下當前應用實例的 _route 的值,更新這個有什麼用呢?請看下段落的分析。
defineReactive 定義 _route
繼續回到 beforeCreate 鉤子函數中,在最後通過 Vue 的工具方法給當前應用實例定義了一個響應式的 _route 屬性,值就是獲取的 this._router.history.current,也就是當前 history 實例的當前活動路由對象。給應用實例定義了這麼一個響應式的屬性值也就意味著如果該屬性值發生了變化,就會觸發更新機制,繼而調用應用實例的 render 重新渲染。還記得上一段結尾留下的疑問,也就是 history 每次更新成功後都會去更新應用實例的 _route 的值,也就意味著一旦 history 發生改變就會觸發更新機制調用應用實例的 render 方法進行重新渲染。
router-link 和 router-view 組件
回到實例化應用實例的地方:
new Vue({n router,n template: `n <div id="app">n <h1>Basic</h1>n <ul>n <li><router-link to="/">/</router-link></li>n <li><router-link to="/foo">/foo</router-link></li>n <li><router-link to="/bar">/bar</router-link></li>n <router-link tag="li" to="/bar">/bar</router-link>n </ul>n <router-view class="view"></router-view>n </div>n `n}).$mount(#app)n
可以看到這個實例的 template 中包含了兩個自定義組件:router-link 和 router-view。
router-view 組件
router-view 組件比較簡單,所以這裡就先來分析它,他是在源碼的 src/components/view.js 中定義的:
export default {n name: router-view,n functional: true, // 功能組件 純粹渲染n props: {n name: {n type: String,n default: default // 默認default 默認命名視圖的namen }n },n render (h, { props, children, parent, data }) {n // 解決嵌套深度問題n data.routerView = truen // route 對象n const route = parent.$routen // 緩存n const cache = parent._routerViewCache || (parent._routerViewCache = {})n let depth = 0n let inactive = falsen // 當前組件的深度n while (parent) {n if (parent.$vnode && parent.$vnode.data.routerView) {n depth++n }n 處理 keepalive 邏輯n if (parent._inactive) {n inactive = truen }n parent = parent.$parentn }nn data.routerViewDepth = depthn // 得到相匹配的當前組件層級的 路由記錄n const matched = route.matched[depth]n if (!matched) {n return h()n }n // 得到要渲染組件n const name = props.namen const component = inactiven ? cache[name]n : (cache[name] = matched.components[name])nn if (!inactive) {n // 非 keepalive 模式下 每次都需要設置鉤子n // 進而更新(賦值&銷毀)匹配了的實例元素n const hooks = data.hook || (data.hook = {})n hooks.init = vnode => {n matched.instances[name] = vnode.childn }n hooks.prepatch = (oldVnode, vnode) => {n matched.instances[name] = vnode.childn }n hooks.destroy = vnode => {n if (matched.instances[name] === vnode.child) {n matched.instances[name] = undefinedn }n }n }n // 調用 createElement 函數 渲染匹配的組件n return h(component, data, children)n }n}n
可以看到邏輯還是比較簡單的,拿到匹配的組件進行渲染就可以了。
router-link 組件
再來看看導航鏈接組件,他在源碼的 src/components/link.js 中定義的:
// ...nimport { createRoute, isSameRoute, isIncludedRoute } from ../util/routen// ...nexport default {n name: router-link,n props: {n // 傳入的組件屬性們n to: { // 目標路由的鏈接n type: toTypes,n required: truen },n // 創建的html標籤n tag: {n type: String,n default: an },n // 完整模式,如果為 true 那麼也就意味著n // 絕對相等的路由才會增加 activeClassn // 否則是包含關係n exact: Boolean,n // 在當前(相對)路徑附加路徑n append: Boolean,n // 如果為 true 則調用 router.replace() 做替換歷史操作n replace: Boolean,n // 鏈接激活時使用的 CSS 類名n activeClass: Stringn },n render (h: Function) {n // 得到 router 實例以及當前激活的 route 對象n const router = this.$routern const current = this.$routen const to = normalizeLocation(this.to, current, this.append)n // 根據當前目標鏈接和當前激活的 route匹配結果n const resolved = router.match(to, current)n const fullPath = resolved.redirectedFrom || resolved.fullPathn const base = router.history.basen // 創建的 hrefn const href = createHref(base, fullPath, router.mode)n const classes = {}n // 激活class 優先當前組件上獲取 要麼就是 router 配置的 linkActiveClassn // 默認 router-link-activen const activeClass = this.activeClass || router.options.linkActiveClass || router-link-activen // 相比較目標n // 因為有命名路由 所有不一定有pathn const compareTarget = to.path ? createRoute(null, to) : resolvedn // 如果嚴格模式的話 就判斷是否是相同路由(path query params hash)n // 否則就走包含邏輯(path包含,query包含 hash為空或者相同)n classes[activeClass] = this.exactn ? isSameRoute(current, compareTarget)n : isIncludedRoute(current, compareTarget)n n // 事件綁定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}nnfunction findAnchor (children) {n if (children) {n let childn for (let i = 0; i < children.length; i++) {n child = children[i]n if (child.tag === a) {n return childn }n if (child.children && (child = findAnchor(child.children))) {n return childn }n }n }n}nnfunction createHref (base, fullPath, mode) {n var path = mode === hash ? /# + fullPath : fullPathn return base ? cleanPath(base + path) : pathn}n
可以看出 router-link 組件就是在其點擊的時候根據設置的 to 的值去調用 router 的 push 或者 replace 來更新路由的,同時呢,會檢查自身是否和當前路由匹配(嚴格匹配和包含匹配)來決定自身的 activeClass 是否添加。
小結
整個流程的代碼到這裡已經分析的差不多了,再來回顧下:
相信整體看完後和最開始的時候看到這張圖的感覺是不一樣的,且對於 vue-router 的整體的流程了解的比較清楚了。當然由於篇幅有限,這裡還有很多細節的地方沒有細細分析,後續會根據模塊來進行具體的分析。
推薦閱讀:
※如何評價Medium文章【為什麼Angular太小太晚】?
※漸進增強的 CSS 布局:從浮動到 Flexbox 到 Grid
※React v16.2.0:Fragments