這是對vue-router 3 版本的源碼分析。
本次分析會按以下方法進行:
- 按官網的使用文檔順序,圍繞著某一功能點進行分析。這樣不僅能學習優秀的項目源碼,更能加深對項目的某個功能是如何實現的理解。這個對自己的技能提升,甚至面試時的回答都非常有幫助。
- 在圍繞某個功能展開講解時,所有不相干的內容都會暫時去掉,等后續涉及到對應的功能時再加上。這樣最大的好處就是能循序漸進地學習,同時也不會被不相干的內容影響。省略的內容都會在代碼中以…表示。
- 每段代碼的開頭都會說明它所在的文件目錄,方便定位和查閱。如果一個函數內容有多個函數引用,這些都會放在同一個代碼塊中進行分析,不同路徑的內容會在其頭部加上所在的文件目錄。
本章講解router中 router-link 組件是如何實現導航的。
另外我的vuex3源碼分析也發布完了,歡迎大家學習:
vuex3 最全面最透徹的源碼分析
還有vue-router的源碼分析:
vue-router 源碼分析——1. 路由匹配
vue-router 源碼分析——2. router-link 組件是如何實現導航的
官網例子
- 在官網例子中,對做了三個注釋:這是個組件、傳入to屬性,渲染成標簽。
- 以我們主要分析 router-link 組件如果利用必要的數據來實現導航的。
- 這里先解釋一下,渲染html頁面的功能,是vue-router調用了vue的render函數,這個是vue的核心功能不做分析。所以這里是分析router是如何定位到對應路由以及做了哪些信息收集和處理的。
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script><div id="app"><h1>Hello App!</h1><p><!-- 使用 router-link 組件來導航. --><!-- 通過傳入 `to` 屬性指定鏈接. --><!-- <router-link> 默認會被渲染成一個 `<a>` 標簽 --><router-link to="/foo">Go to Foo</router-link><router-link to="/bar">Go to Bar</router-link></p>
</div>
Vue實例掛載link組件,從而可以使用標簽
- 這里的install.js實際上是一個VUE的插件,這樣當創建和掛載根實例時自動導入了插件。
- 這里面還涉及到VUE的混入(Vue.mixin),實現了在任意組件內通過this.$ router和this.$route來訪問路由器和當前路由。感興趣的小伙伴可以去看VUE官網的解釋。
// ./install.js
import Link from './components/link'export function install(Vue) {Vue.component('RouterLink', Link)
}
link.js源碼內容
- 傳入了一個to變量,對應’/foo’,tag默認為’a’,即標簽.
- render函數是vue用來描述如果構建虛擬DOM的,即如何按某個定義來構建組件。
- 如何渲染為html是vue中的核心內容,這里不做討論,只分析vue-router中的源碼內容,即resolve方法。
const { location, route, href } = router.resolve(this.to,current,this.append ) ...const classes = {}const data: any = { class: classes }...return h(this.tag, data, this.$slots.default) }
}
路由實例的 resolve 方法
- resolve入參的to是我們傳入的字符串’/foo’,current是當前路由this.$route,append是undefine。
- resolve 方法內部又調用了很多函數和方法,得到需要的數據并返回。
- 讓我們接著分析調用的函數(normalizeLocation,this.match, createHref)。
// ./router.js
export default class VueRouter {...resolve (to: RawLocation,current?: Route,append?: boolean) {...const location = normalizeLocation(to, current, append, this)const route = this.match(location, current)const fullPath = route.redirectedFrom || route.fullPathconst base = this.history.baseconst href = createHref(base, fullPath, this.mode) // this.mode 看做 'hash' 即可return {location,route,href...} }
}
normalizeLocation 函數分析
- 入參說明:raw = ‘/foo’, current = this.route, append = undifune, router = this.$router
- parsePath函數暫時沒有額外影響,這里不做分析,只是說明 parsePath 的具體內容
- 最終返回一個對象,里面有 _normalized: true 和 path: '/foo’相關內容。
// ./util/location.js
export function normalizeLocation(raw: RawLocation,current: ?Route,append: ?boolean,router: ?VueRouter
): Location {let next: Location = typeof raw === 'string' ? { path: raw } : rawif (next._normalized) {return next }...// parsePath 的實際內容為 {'path': '/foo', 'query': '', 'hash': ''}const parsedPath = parsePath(next.path || '')// basePath 為當前的路由的路徑const basePath = (current && current.path) || '/'// resolvePath函數對'/foo'也沒有額外影響,可以理解直接返回了'/foo'賦值給pathconst path = parsedPath.path? resolvePath(parsedPath.path, basePath, append || next.append): basePath...return {_normalized: true,path,... }
}
this.match方法獲得的route是什么
- 對應代碼 const route = this.match(location, current),current = this.$route。
- match方法和resolve方法一樣,是定義在VueRouter中的,它直接返回了路由匹配器的match方法。
- 路由匹配器中的match方法會遍歷pathList和pathMap,利用正則表達式查看對應的path是否匹配,如果匹配,這里則返回 _createRoute 函數調用。
-
- pathList和pathMap是在初始化router時生成的,這里為方便理解,再說明一下。
-
- pathList是一個數組,記錄著我們初始化時定義的各個url path,例如’/foo’,‘/bar’
-
- pathMap是一個哈希結構,key為path,value為相關的數據(比如path, component, regex等等)。
- _createRoute 函數又調用了 ./util/route.js 的 createRoute 函數。它返回了一個被凍結的對象,里面主要有path和matched屬性,matched在這里可以看做等于 [record]。
- 所以this,match方法返回的是一個和我們輸入的路徑to匹配的一個route對象,里面有我們需要的path,record等內容。
// ./router.js
export default class VueRouter {...match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {return this.matcher.match(raw, current, redirectedFrom)}
}// ./create-matcher.js
export function createMatcher(...) {...function match {raw: RawLocation,currentRoute?: Route,redirectedFrom?: Location}: Route {// 由于傳入的raw是已經標準化過的,所以這里的location和raw沒有任何區別const location = normalizedLocation(raw, currentRoute, false, router)const { name } = locationif (name) {... } else if (location.path) {location.params = {}for (let i = 0; i < pathList.length; i++) {const path = pathList[i]const record = pathMap[path]if (matchRoute(record.regex, location.path, location.param)) {return _createRoute(record, location, redirectedFrom) } } }}function matchRoute(regex: RouteRegExp,path: string,params: Object): boolean {const m = path.match(regex)if (!m) {return false } else if (!params) {return true }...return true}function _createRoute(record: ?RouteRecord,location: Location,redirectedFrom?: Location ): Route {...return createRoute(record, location. redirectedFrom, router) }
}// ./util/route.js
export function createRoute(record: ?RouteRecord,location: Location,redirectedFrom?: ?Location,router?: VueRouter
): Route {const route: Route = {path: location.path || '/',matched: record ? formatMatch(record) :[], // 這里可以先簡單理解為 [record]... }return Object.freeze(route)
}
createHref函數分析
- 對應代碼 href = createHref(base, fullPath, this.mode),對應base = undefine, fullPath = ‘/foo’, this.mode = ‘hash’。
- 在vue-route中,默認為hash模式,所以所有的的path前面都會帶一個#號
- 這個#號就是在這個函數中體現的
// ./router.js
function createHref(base: string, fullPath: string, mode) {var path = mode === 'hash' ? '#' + fullPath : fullPathreturn base ? cleanPath(base + '/' + path) : path
}
總結
- recolve返回的對象里面的內容主要為location, route , href。
- location是標準化后的to,并且打上了標記表示已標準化,防止多次標準化,提升效率。
- route是通過遍歷pathList和pathMap,利用正則表達式找到的和to匹配的路由對象,里面包含很多需要的內容。
- href在默認的hash模式下,會在to的前面加上#號,例如這里的’#/foo’。
// ./router.jsexport default class VueRouter {...resolve(to, current, append) {const location = normalizeLocation(to, current, append, this)const route = this.match(location, current)const fullPath = route.redirectedFrom || route.fullPathconst base = this.history.baseconst href = createHref(base, fullPath, this.mode)return {location,route,href,... } }
}
- 后續的內容就是vue利用h函數將link組件渲染為html的內容,例如設置類名,定義handler函數處理跳轉,綁定點擊事件,指定 a 標簽等等。
- 這里再對vue-router官方文檔中提到的匹配成功后自動設置的class屬性值,通過源碼分析一下:
-
- 通過下面的代碼可以看出,如果你不想要 router-link-active 的類名,可以在初始化router時,加入options.linkActiveClass屬性就可以了