vue router 源碼概覽案例分析
源碼這個東西對于實(shí)際的工作其實(shí)沒有立竿見影的效果,不會像那些針對性極強(qiáng)的文章一樣看了之后就立馬可以運(yùn)用到實(shí)際項(xiàng)目中,產(chǎn)生什么樣的效果,源碼的作用是一個潛移默化的過程,它的理念、設(shè)計(jì)模式、代碼結(jié)構(gòu)等看了之后可能不會立即知識變現(xiàn)(或者說變現(xiàn)很少),而是在日后的工作過程中悄無聲息地發(fā)揮出來,你甚至都感覺不到這個過程
另外,優(yōu)秀的源碼案例,例如 vue 、 react 這種,內(nèi)容量比較龐大,根本不是三篇五篇十篇八篇文章就能說完的,而且寫起來也很難寫得清楚,也挺浪費(fèi)時(shí)間的,而如果只是分析其中一個點(diǎn),例如 vue 的響應(yīng)式,類似的文章也已經(jīng)夠多了,沒必要再 repeat
所以我之前沒專門寫過源碼分析的文章,只是自己看看,不過最近閑來無事看了 vue-router 的源碼,發(fā)現(xiàn)這種插件級別的東西,相比 vue 這種框架級別的東西,邏輯簡單清晰,沒有那么多道道,代碼量也不多,但是其中包含的理念等東西卻很精煉,值得一寫,當(dāng)然,文如其名,只是概覽,不會一行行代碼分析過去,細(xì)節(jié)的東西還是要自己看看的
vue.use
vue 插件必須通過 vue.use 進(jìn)行注冊, vue.use 的代碼位于 vue 源碼的 src/core/global-api/use.js 文件中,此方法的主要作用有兩個:
- 對注冊的組件進(jìn)行緩存,避免多次注冊同一個插件
if (installedPlugins.indexOf(plugin) > -1) { return this }
- 調(diào)用插件的 install 方法或者直接運(yùn)行插件,以實(shí)現(xiàn)插件的 install
if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, args) }
路由安裝
vue-router 的 install 方法位于 vue-router 源碼的 src/install.js 中 主要是通過 vue.minxin 混入 beforeCreate 和 destroyed 鉤子函數(shù),并全局注冊 router-view 和 router-link 組件
// src/install.js Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { this._routerRoot = this this._router = this.$options.router this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } registerInstance(this, this) }, destroyed () { registerInstance(this) } }) ... // 全局注冊 `router-view` 和 `router-link`組件 Vue.component('RouterView', View) Vue.component('RouterLink', Link)
路由模式
vue-router 支持三種路由模式( mode ): hash 、 history 、 abstract ,其中 abstract 是在非瀏覽器環(huán)境下使用的路由模式,例如 weex
路由內(nèi)部會對外部指定傳入的路由模式進(jìn)行判斷,例如當(dāng)前環(huán)境是非瀏覽器環(huán)境,則無論傳入何種 mode ,最后都會被強(qiáng)制指定為 abstract ,如果判斷當(dāng)前環(huán)境不支持 HTML5 History ,則最終會被降級為 hash 模式
// src/index.js let mode = options.mode || 'hash' this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' }
最后會對符合要求的 mode 進(jìn)行對應(yīng)的初始化操作
// src/index.js 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, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } }
路由解析
通過遞歸的方式來解析嵌套路由
// src/create-route-map.js function addRouteRecord ( pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) { ... route.children.forEach(child => { const childMatchAs = matchAs ? cleanPath(`${matchAs}/${child.path}`) : undefined addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs) }) ... }
解析完畢之后,會通過 key-value 對的形式對解析好的路由進(jìn)行記錄,所以如果聲明多個相同路徑( path )的路由映射,只有第一個會起作用,后面的會被忽略
// src/create-route-map.js if (!pathMap[record.path]) { pathList.push(record.path) pathMap[record.path] = record }
例如如下路由配置,路由 /bar 只會匹配 Bar1 , Bar2 這一條配置會被忽略
const routes = [ { path: '/foo', component: Foo }, { path: '/bar', component: Bar1 }, { path: '/bar', component: Bar2 }, ];
路由切換
當(dāng)訪問一個 url 的時(shí)候, vue-router 會根據(jù)路徑進(jìn)行匹配,創(chuàng)建出一個 route 對象,可通過 this.$route 進(jìn)行訪問
// src/util/route.js const route: Route = { name: location.name || (record && record.name), meta: (record && record.meta) || {}, path: location.path || '/', hash: location.hash || '', query, params: location.params || {}, fullPath: getFullPath(location, stringifyQuery), matched: record ? formatMatch(record) : [] }
src/history/base.js 源碼文件中的 transitionTo() 是路由切換的核心方法
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { const route = this.router.match(location, this.current) this.confirmTransition(route, () => { ... }
路由實(shí)例的 push 和 replace 等路由切換方法,都是基于此方法實(shí)現(xiàn)路由切換的,例如 hash 模式的 push 方法:
// src/history/hash.js push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this // 利用了 transitionTo 方法 this.transitionTo(location, route => { pushHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) }
transitionTo 方法內(nèi)部通過一種異步函數(shù)隊(duì)列化執(zhí)⾏的模式來更新切換路由,通過 next 函數(shù)執(zhí)行異步回調(diào),并在異步回調(diào)方法中執(zhí)行相應(yīng)的鉤子函數(shù)(即 導(dǎo)航守衛(wèi)) beforeEach 、 beforeRouteUpdate 、 beforeRouteEnter 、 beforeRouteLeave
通過 queue 這個數(shù)組保存相應(yīng)的路由參數(shù):
// src/history/base.js const queue: Array<?NavigationGuard> = [].concat( // in-component leave guards extractLeaveGuards(deactivated), // global before hooks this.router.beforeHooks, // in-component update hooks extractUpdateHooks(updated), // in-config enter guards activated.map(m => m.beforeEnter), // async components resolveAsyncComponents(activated) )
通過 runQueue 以一種遞歸回調(diào)的方式來啟動異步函數(shù)隊(duì)列化的執(zhí)⾏:
// src/history/base.js // 異步回調(diào)函數(shù) runQueue(queue, iterator, () => { const postEnterCbs = [] const isValid = () => this.current === route // wait until async components are resolved before // extracting in-component enter guards const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) const queue = enterGuards.concat(this.router.resolveHooks) // 遞歸執(zhí)行 runQueue(queue, iterator, () => { if (this.pending !== route) { return abort() } this.pending = null onComplete(route) if (this.router.app) { this.router.app.$nextTick(() => { postEnterCbs.forEach(cb => { cb() }) }) } }) })
通過 next 進(jìn)行導(dǎo)航守衛(wèi)的回調(diào)迭代,所以如果在代碼中顯式聲明了導(dǎo)航鉤子函數(shù),那么就必須在最后調(diào)用 next() ,否則回調(diào)不執(zhí)行,導(dǎo)航將無法繼續(xù)
// src/history/base.js const iterator = (hook: NavigationGuard, next) => { ... hook(route, current, (to: any) => { ... } else { // confirm transition and pass on the value next(to) } }) ... }
路由同步
在路由切換的時(shí)候, vue-router 會調(diào)用 push 、 go 等方法實(shí)現(xiàn)視圖與地址 url 的同步
地址欄 url 與視圖的同步
當(dāng)進(jìn)行點(diǎn)擊頁面上按鈕等操作進(jìn)行路由切換時(shí), vue-router 會通過改變 window.location.href 來保持視圖與 url 的同步,例如 hash 模式的路由切換:
// src/history/hash.js function pushHash (path) { if (supportsPushState) { pushState(getUrl(path)) } else { window.location.hash = path } }
上述代碼,先檢測當(dāng)前瀏覽器是否支持 html5 的 History API ,如果支持則調(diào)用此 API 進(jìn)行 href 的修改,否則直接對 window.location.hash 進(jìn)行賦值 history 的原理與此相同,也是利用了 History API
視圖與地址欄 url 的同步
當(dāng)點(diǎn)擊瀏覽器的前進(jìn)后退按鈕時(shí),同樣可以實(shí)現(xiàn)視圖的同步,這是因?yàn)樵诼酚沙跏蓟臅r(shí)候,設(shè)置了對瀏覽器前進(jìn)后退的事件監(jiān)聽器
下述是 hash 模式的事件監(jiān)聽:
// src/history/hash.js setupListeners () { ... window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => { const current = this.current if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { if (supportsScroll) { handleScroll(this.router, route, current, true) } if (!supportsPushState) { replaceHash(route.fullPath) } }) }) }
history 模式與此類似:
// src/history/html5.js window.addEventListener('popstate', e => { const current = this.current // Avoiding first `popstate` event dispatched in some browsers but first // history route not updated since async guard at the same time. const location = getLocation(this.base) if (this.current === START && location === initLocation) { return } this.transitionTo(location, route => { if (supportsScroll) { handleScroll(router, route, current, true) } }) })
無論是 hash 還是 history ,都是通過監(jiān)聽事件最后來調(diào)用 transitionTo 這個方法,從而實(shí)現(xiàn)路由與視圖的統(tǒng)一
另外,當(dāng)?shù)谝淮卧L問頁面,路由進(jìn)行初始化的時(shí)候,如果是 hash 模式,則會對 url 進(jìn)行檢查,如果發(fā)現(xiàn)訪問的 url 沒有帶 # 字符,則會自動追加,例如初次訪問 http://localhost:8080 這個 url , vue-router 會自動置換為 http://localhost:8080/#/ ,方便之后的路由管理:
// src/history/hash.js function ensureSlash (): boolean { const path = getHash() if (path.charAt(0) === '/') { return true } replaceHash('/' + path) return false }
scrollBehavior
當(dāng)從一個路由 /a 跳轉(zhuǎn)到另外的路由 /b 后,如果在路由 /a 的頁面中進(jìn)行了滾動條的滾動行為,那么頁面跳轉(zhuǎn)到 /b 時(shí),會發(fā)現(xiàn)瀏覽器的滾動條位置和 /a 的一樣(如果 /b 也能滾動的話),或者刷新當(dāng)前頁面,瀏覽器的滾動條位置依舊不變,不會直接返回到頂部的 而如果是通過點(diǎn)擊瀏覽器的前進(jìn)、后退按鈕來控制路由切換時(shí),則部門瀏覽器(例如微信)滾動條在路由切換時(shí)都會自動返回到頂部,即 scrollTop=0 的位置 這些都是瀏覽器默認(rèn)的行為,如果想要定制頁面切換時(shí)的滾動條位置,則可以借助 scrollBehavior 這個 vue-router 的 options
當(dāng)路由初始化時(shí), vue-router 會對路由的切換事件進(jìn)行監(jiān)聽,監(jiān)聽邏輯的一部分就是用于控制瀏覽器滾動條的位置:
// src/history/hash.js setupListeners () { ... if (supportsScroll) { // 進(jìn)行瀏覽器滾動條的事件控制 setupScroll() } ... }
這個 set 方法定義在 src/util/scroll.js ,這個文件就是專門用于控制滾動條位置的,通過監(jiān)聽路由切換事件從而進(jìn)行滾動條位置控制:
// src/util/scroll.js window.addEventListener('popstate', e => { saveScrollPosition() if (e.state && e.state.key) { setStateKey(e.state.key) } })
通過 scrollBehavior 可以定制路由切換的滾動條位置, vue-router 的github上的源碼中,有相關(guān)的 example ,源碼位置在 vue-router/examples/scroll-behavior/app.js
router-view & router-link
router-view 和 router-link 這兩個 vue-router 的內(nèi)置組件,源碼位于 src/components 下
router-view
router-view 是無狀態(tài)(沒有響應(yīng)式數(shù)據(jù))、無實(shí)例(沒有 this 上下文)的函數(shù)式組件,其通過路由匹配獲取到對應(yīng)的組件實(shí)例,通過 h 函數(shù)動態(tài)生成組件,如果當(dāng)前路由沒有匹配到任何組件,則渲染一個注釋節(jié)點(diǎn)
// vue-router/src/components/view.js ... const matched = route.matched[depth] // render empty node if no matched route if (!matched) { cache[name] = null return h() } const component = cache[name] = matched.components[name] ... return h(component, data, children)
每次路由切換都會觸發(fā) router-view 重新 render 從而渲染出新的視圖,這個觸發(fā)的動作是在 vue-router 初始化 init 的時(shí)候就聲明了的:
// src/install.js Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { this._routerRoot = this this._router = this.$options.router this._router.init(this) // 觸發(fā) router-view重渲染 Vue.util.defineReactive(this, '_route', this._router.history.current) ... })
將 this._route 通過 defineReactive 變成一個響應(yīng)式的數(shù)據(jù),這個 defineReactive 就是 vue 中定義的,用于將數(shù)據(jù)變成響應(yīng)式的一個方法,源碼在 vue/src/core/observer/index.js 中,其核心就是通過 Object.defineProperty 方法修改數(shù)據(jù)的 getter 和 setter :
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { // 進(jìn)行依賴收集 dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { ... // 通知訂閱當(dāng)前數(shù)據(jù) watcher的觀察者進(jìn)行響應(yīng) dep.notify() }
當(dāng)路由發(fā)生變化時(shí),將會調(diào)用 router-view 的 render 函數(shù),此函數(shù)中訪問了 this._route 這個數(shù)據(jù),也就相當(dāng)于是調(diào)用了 this._route 的 getter 方法,觸發(fā)依賴收集,建立一個 Watcher ,執(zhí)行 _update 方法,從而讓頁面重新渲染
// vue-router/src/components/view.js render (_, { props, children, parent, data }) { // used by devtools to display a router-view badge data.routerView = true // directly use parent context's createElement() function // so that components rendered by router-view can resolve named slots const h = parent.$createElement const name = props.name // 觸發(fā)依賴收集,建立 render watcher const route = parent.$route ... }
這個 render watcher 的派發(fā)更新,也就是 setter 的調(diào)用,位于 src/index.js :
history.listen(route => { this.apps.forEach((app) => { // 觸發(fā) setter app._route = route }) })
router-link
router-link 在執(zhí)行 render 函數(shù)的時(shí)候,會根據(jù)當(dāng)前的路由狀態(tài),給渲染出來的 active 元素添加 class ,所以你可以借助此給 active 路由元素設(shè)置樣式等:
// src/components/link.js render (h: Function) { ... const globalActiveClass = router.options.linkActiveClass const globalExactActiveClass = router.options.linkExactActiveClass // Support global empty active class const activeClassFallback = globalActiveClass == null ? 'router-link-active' : globalActiveClass const exactActiveClassFallback = globalExactActiveClass == null ? 'router-link-exact-active' : globalExactActiveClass ... }
router-link 默認(rèn)渲染出來的元素是 <a> 標(biāo)簽,其會給這個 <a> 添加 href 屬性值,以及一些用于監(jiān)聽能夠觸發(fā)路由切換的事件,默認(rèn)是 click 事件:
// src/components/link.js data.on = on data.attrs = { href }
另外,你可以可以通過傳入 tag 這個 props 來定制 router-link 渲染出來的元素標(biāo)簽:
<router-link to="/foo" tag="div">Go to foo</router-link>
如果 tag 值不為 a ,則會遞歸遍歷 router-link 的子元素,直到找到一個 a 標(biāo)簽,則將事件和路由賦值到這個 <a> 上,如果沒找到 a 標(biāo)簽,則將事件和路由放到 router-link 渲染出的本身元素上:
if (this.tag === 'a') { data.on = on data.attrs = { href } } else { // find the first <a> child and apply listener and href // findAnchor即為遞歸遍歷子元素的方法 const a = findAnchor(this.$slots.default) ... } }
當(dāng)觸發(fā)這些路由切換事件時(shí),會調(diào)用相應(yīng)的方法來切換路由刷新視圖:
// src/components/link.js const handler = e => { if (guardEvent(e)) { if (this.replace) { // replace路由 router.replace(location) } else { // push 路由 router.push(location) } } }
總結(jié)
可以看到, vue-router 的源碼是很簡單的,比較適合新手進(jìn)行閱讀分析
源碼這種東西,我的理解是沒必要非要 專門騰出時(shí)間來看 ,只要你熟讀文檔,能正確而熟練地運(yùn)用 API 實(shí)現(xiàn)各種需求那就行了,輪子的出現(xiàn)本就是為實(shí)際開發(fā)所服務(wù)而不是用來折騰開發(fā)者的,注意,我不是說不要去看,有時(shí)間還是要看看的,就算弄不明白其中的道道,但看了一遍總會有收獲的,比如我在看 vue 源碼的時(shí)候,經(jīng)??吹筋愃朴谶@種的賦值寫法:
// vue/src/core/vdom/create-functional-component.js (clone.data || (clone.data = {})).slot = data.slot
如果是之前,對于這段邏輯我通常會這么寫:
if (clone.data) { clone.data.slot = data.slot } else { clone.data = { slot: data.slot } }
也不是說第一種寫法有什么難度或者看不明白,只是習(xí)慣了第二種寫法,平時(shí)寫代碼的過程中自然而然不假思索地就寫出來了,習(xí)慣成自然了,但是當(dāng)看到第一種寫法的時(shí)候才會一拍腦袋想著原來這么寫也可以,以前白敲了那么多次鍵盤,所以沒事要多看看別人優(yōu)秀的源碼,避免沉迷于自己的世界閉門造車,這樣才能查漏補(bǔ)缺,這同樣也是我認(rèn)為代碼 review 比較重要的原因,自己很難發(fā)現(xiàn)的問題,別人可能一眼就看出來了,此之謂 當(dāng)局者迷旁觀者清也
以上所述是小編給大家介紹的vue router 源碼概覽,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時(shí)回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
詳解Vue內(nèi)部怎樣處理props選項(xiàng)的多種寫法
這篇文章主要介紹了詳解Vue內(nèi)部怎樣處理props選項(xiàng)的多種寫法,詳細(xì)的介紹了props的使用的寫法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-11-11vue + node如何通過一個Txt文件批量生成MP3并壓縮成Zip
這篇文章主要介紹了vue + node如何通過一個Txt文件批量生成MP3并壓縮成Zip的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06vue項(xiàng)目使用CDN引入的配置與易出錯點(diǎn)
在日常開發(fā)過程中,為了減少最后打包出來的體積,我們會用到cdn引入一些比較大的庫來解決,下面這篇文章主要給大家介紹了關(guān)于vue項(xiàng)目使用CDN引入的配置與易出錯點(diǎn)的相關(guān)資料,需要的朋友可以參考下2022-05-05vue2和vue3部署到服務(wù)器子目錄為空白頁問題及解決
這篇文章主要介紹了vue2和vue3部署到服務(wù)器子目錄為空白頁問題及解決,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07