Vue-Router原始碼分析之index.js
上一篇我們聊了: Vue-Router原始碼學習之install方法
雖然最近需求著實不少,但是感覺自己學習勁頭還是蠻足的,並沒有被需求壓垮。今天,帶來Vue-Router原始碼解析系列的第二篇文章:index.js。
正文
vue-router類裡面都做了什麼?
index.js是vue-router這個類的主建構函式,所以內容上算是比較關鍵的:

constructor (options: RouterOptions = {})
,在vue-router中使用了flow.js做了型別的檢查,
什麼是flow.js?flow.js怎麼使用呢?因為篇幅原因,這裡就暫時先不做涉及。各位小夥伴,可以參看官網: flow.org/en/docs/typ…
解析:constructor
首先我們來看一下constructor內的程式碼,
constructor (options: RouterOptions = {}) { this.app = null this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] this.matcher = createMatcher(options.routes || [], this) //預設為hash錨點 let mode = options.mode || 'hash' //當然使用的是history模式 h5的pushState的方式來實現路由跳轉的,對options設定fallback屬性為true時會回退到hash模式 // 是否支援回退 this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' } //沒有fallback的話選擇錨點模式,node環境選擇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, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } } 複製程式碼
我們宣告一個vue-router例項的時候是怎麼做的?
let router = new Router({ base : '/', mode : 'history', routes : [{ component : xxx, path : xxx },xxx] }) 複製程式碼
constructor (options: RouterOptions = {}) options就是我們剛才上面的一個物件,裡面有base、mode、routes等屬性 複製程式碼
這時候我們知道options是個什麼東西了,我們來看看內部對options進行了哪些處理。
首先我們說一下vue-router最核心的內容之一:
解析:mode
我們知到vue-router的路由有兩種方式,一種是#錨點性的,第二種是和正常路徑一樣的,可是vue構建的應用是一個但頁面應用如何讓他像正常的多頁面應用一樣,是在不停的改變路徑呢? 這裡面就使用了html5的history的pushState與replaceState(讓頁面看起來無重新整理的改變路徑),具體內容大家可以看一下官網文件和大神張鑫旭的部落格( www.zhangxinxu.com/wordpress/2…
在vue-router原始碼中有一個工具類專門做了這個事情:

我們來看一下vue-router是如何匹配mode的吧:
// vue-router預設使用hash模式 let mode = options.mode || 'hash'; this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false // 如果選擇了history但是pushState方法並不能使用並且設定了 // 在當瀏覽器不支援 history.pushState 控制路由是否應該回退到 hash 模式的情況下。 // (options.fallback預設就是true) // 如果發現需要回退了,就回到hash錨點模式 if (this.fallback) { mode = 'hash' } // 不在瀏覽器環境就選擇abstract模式(在node環境) if (!inBrowser) { mode = 'abstract' } this.mode = mode // 根據三種情況是成不同的路由轉換例項。 // 如果沒有mode不是這三種情況就報錯。 // HTML5History、HTML5History、HTML5History三個類都是繼承與一個base類 // 裡面有這三種模式對於路徑轉換時做的事情進行了一定的封裝。 switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HTML5History(this, options.base, this.fallback) break case 'abstract': this.history = new HTML5History(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } 複製程式碼
到這裡大家應該對我們寫的mode模式有一點的瞭解了吧。
解析:init
下面說一下init方法,上一章我們講了在根節點的beforeCreate生命週期鉤子中,使用了init方法,如果忘記了可以翻看上一篇install方法的學習來回歸一下
所以app就是根元件,init在執行前要判斷一下,vue-router是不是被vue成功use了,因為成功use之後,會把install方法的installed屬性設定為true:

init (app: any /* Vue component instance */) { process.env.NODE_ENV !== 'production' && assert( install.installed, `not installed. Make sure to call \`Vue.use(VueRouter)\` ` + `before creating root instance.` ) this.apps.push(app) // main app already initialized. (根元件已經被初始化) if (this.app) { return } this.app = app const history = this.history // 當我們的根元件完成了對vue-router的init的時候我們就要完成第一次路由的跳轉了 // 當我們的專案啟動的時候肯定會有一個路徑,這個路徑是什麼不重要 // 我們在第一次進入這個路徑的時候,會進行vue-router的初始化 // 初始化之後要開始展示對應的元件,可是我們vue-router那些popState的事件肯定沒有繫結,不會觸發啊, // 怎麼辦?? 那就手動觸發一下這個事情, // 第一次進入肯定沒有對應的事件,不會完成跳轉時該做的事情。 if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } // 針對設定的不同mode(模式) // 將mode的時候,每種模式的history例項來源於三個不同的類,所以instanceof足夠判斷是哪種模式。 複製程式碼
到現在,我們上一章的init函式已經串聯起來了,不知道大家感覺怎麼樣?我感覺舒服了很多了。
路由守衛
用過vue-router的同學們都知道 路由守衛 的概念,這是在路由跳轉的前後等三個地方設定了不同的鉤子,幫助我們在進入離開路由前做一些事情。這個鉤子是怎麼做的呢?

聲明瞭三個陣列,存放每個週期內的鉤子上繫結的函式。
vue-router全域性級別的beforeEach、beforeResolve、afterEach做了什麼?

就這麼兩行程式碼你敢信??我也很糾結你就幹了這麼點事情。 執行一下registerHook函式,從字面意思一看就是註冊鉤子,
怎麼註冊的呢?
function registerHook (list: Array<any>, fn: Function): Function { list.push(fn) // 返回值是一個function return () => { const i = list.indexOf(fn) if (i > -1) list.splice(i, 1) } } 複製程式碼
接收一個生命週期的鉤子陣列,將我們要執行的函式傳到陣列內就可以完成註冊了,我還沒看到這三個陣列的內容,但是直覺告訴我很有可能就是,觀察者模式(以後就探索去)。註冊到這應該就OK了,
為什麼還有個返回值呢? 返回值的內容一看就是要清楚鉤子內的函式呀,我們呼叫這個registerHook函式後,可以得到註冊函式的清除函式,清除的是鉤子陣列中對應的函式,還有這麼一手,牛的一匹。(讓程式碼教你如何熟練使用閉包~)
vue-router的程式設計式導航是怎麼做的?

push方法與replace、go方法呼叫對應路由轉換例項的對應方法,因為不同模式大家方法肯定都不一樣, back與forward都是go方法傳入特殊引數,所以看到這裡我們發現history這個例項的內容很關鍵。
清一清嗓
到了這裡我們vue-router的主線流程我們已經進行了一個梳理,不知道大家對這一塊內容感覺滿意嗎? 不滿意就請不要郵寄刀片哈。
所以到現在我們簡單進行一下總結
1:mode是設定模式的,有hash、history、abstract三種模式、每個模式會導致vue-router例項的history不同,來自三個不同的類、每個類又繼承於總的base類。 2:init方法會初始化整個元件、並且在vue-router的例項中設定了app屬性存放根元件(這個確實很有用)、手動的完成初始化後的第一次路由跳轉。 3:beforeEach、beforeREsolve、afterEach三個全域性的鉤子都有對應的鉤子函式陣列,存放每個週期鉤子內要做的事情、使用registerHook方法來完成鉤子函式的註冊,registerHook也可以清除鉤子內對應的函式。 4:push、replace、go等方法都是使用history方法內的對應方法。
總結完畢 我們學到了什麼? history真重要,我要好好看看他內部的實現
真的沒出息的總結。
vue-router類中還有一部分對options.routes的處理
options.routes 就是 [{path : 'xxx',components : 'xxx'}] 這個陣列 複製程式碼
生成一個根據options.routes的一份比對程式,完成程式的比對。
下一期的內容要在繼續學習index.js和開荒history中進行一個抉擇,具體是什麼內容大家可以積極留言,給個方向呀。
結束語
每一個前端er(boy and girl) 你們都不是一個人在戰鬥。
安排!!!!
我是一個應屆生,最近和朋友們維護了一個公眾號,內容是我們在從應屆生過渡到開發這一路所踩過的坑,已經我們一步步學習的記錄,如果感興趣的朋友可以關注一下,一同加油~
