[Vue.js進階]從源碼角度剖析vue-router(三)

前言

上篇中主要敘述了 vue-router 中生成 $route 對象的時機,路由懶加載的原理,以及異步路由以前執行的一系列路由守衛vue

在本篇中會講述:node

  • 異步路由解析成功後執行的一系列路由守衛
  • vue-router 是如何經過路由來實現頁面之間的切換
  • 爲何 beforeRouteEnter 守衛須要經過回調的形式獲取組件實例

同時本文會按照 vue-router 官網完整的導航解析流程的 7-12 步,逐個解析每一步的背後的原理git

圖1:github

文中的源碼截圖只保留核心邏輯 完整源碼地址vue-router

有興趣的朋友也能夠看我學習源碼時的詳細註釋源碼地址編程

vue-router 版本:3.0.2數組

生成 beforeRouteEnter 守衛

上文說到,當異步路由(組件)所有解析完畢後,會執行 next 方法遍歷 queue 數組中的下個元素,但此時 queue 數組中的元素已經所有遍歷完畢,因此會直接執行 runQueue 的第三個參數,即成功的回調函數瀏覽器

圖2:閉包

緊接着會執行 extractEnterGuards 這個函數,而上文中介紹到 extract 開頭的函數會根據傳入的路由記錄這個參數,從中獲取組件配置項中的指定的路由守衛,這裏 vue-router 會根據 activated 數組,也就是跳轉先後新增的路由記錄數組,從中獲取 beforeRouteEnter 守衛app

和以前的那些路由守衛不一樣的是,它會額外傳入一個 postEnterCbs 參數來存儲 beforeRouteEnter 守衛中,經過 next 方法傳入的回調參數

圖3:

若是在組件中 beforeRouteEnter 守衛裏的 next 函數裏,傳入了一個回調函數,就會往 postEnterCbs 數組中添加這個回調,同時回調會被包裹一層 poll 函數用來指定參數,即組件實例 vm

圖4:

經過 instance[key] 從路由記錄的 instance 屬性獲取到組件實例,可是在註冊回調時,這個時候組件實例爲空對象

圖5:

這是爲何呢?咱們同時再來思考一個問題,爲何 vue-router 的其餘守衛能夠直接在內部經過 this 訪問組件實例,而 beforeRouteEnter 必須經過在 next 函數中傳入回調的形式來獲取組件實例?這2個問題咱們放到後面來討論,繼續往下走主線的流程

調用 beforeResolve 守衛

以後包含 beforeRouteEnter 守衛的數組會和 beforeResolve 守衛合併,而且再一次的執行 runQueue,即開始第二輪的遍歷

遍歷邏輯在上文中也詳細敘述過,主要就是每次遍歷 queue 的一個路由守衛,而且當路由守衛調用 next 方法後纔會繼續遍歷下個守衛,也就是說 beforeRouteEnter 和 beforeResolve 會依次執行,對應圖1官網流程的 7,8 兩步

確認導航

當第二輪 queue 遍歷完畢後,再一次執行 runQueue 方法成功的回調,在 runQueue 成功回調中會又執行到 onComplete 這個函數,它是 confirmTransition 的成功回調,執行確認導航的邏輯

由於 queue 數組是在 confirmTransition 這個方法內被遍歷的的,而onComplete 也是在執行 confirmTransition 被傳入的

圖6:

其中的第二個參數即爲 onComplete 函數,這個函數的第一行中會執行 history 實例的 updateRoute 方法

圖7:

這個時候 vue-router 會更新 current 屬性,也就是說此時的 current 已經不在是跳轉前的 $route 對象了,更新成跳轉後的 $route 對象,接着會執行 cb 方法

cb 方法定義在 vue-router 類中

圖8:

當 vue-router 初始化的時候會執行 history.listen 並傳入一個回調,而這個回調最終會成爲 history 實例的 cb 方法,當執行這個回調時,就能夠實現頁面之間的切換

註冊頁面更新的回調

圖9:

接下來咱們來分析這個能改變視圖的函數, this.apps 咱們第一章分析過,是一個保存根 Vue 實例的數組,最終會將根實例的 _route 屬性更新爲當前的 $route 對象,就是這樣短短一行代碼就能夠實現整個頁面的切換,這是爲何呢?

在第一章混入全局鉤子那節,我留了一個懸念

圖10:

觀察圖中第 8 行能夠發現,vue-router 會調用 Vue 核心庫中的 defineReactive 將根實例的 _route 屬性變成響應式, 另外還經過 Object.defineProperty 定義了 $route 屬性指向 _route,結合 Vue 的響應式原理,也就是說當 $route 被修改後,經過 defineReactive 會通知全部依賴 $route 的 watcher

而只有 render watcher 纔有改變視圖的功能,因此能夠推測出在某個組件的 render 函數中依賴到了 $route,而這個組件就是 vue-router 內置的全局視圖組件 router-view

圖11 router-view 組件:

router-view 內部會經過 render 函數根據 $route 中的 components 屬性也就是組件配置項,生成 vnode 最後交給 Vue 渲染出視圖,因此就會依賴到 $route

異步更新視圖

回到圖7,在確認導航的 updateRoute 方法中,執行 cb 就會觸發視圖的改變,可是這個行爲不會當即被觸發,即

視圖並不會當即被改變

視圖並不會當即被改變

視圖並不會當即被改變

重要的事情說三遍,這裏就簡單提一下 Vue 的視圖更新原理

Vue 會維護一個隊列,保存全部 watcher,當 cb 執行後爲了更新視圖,會將 router-view 的 render watcher 推入這個隊列,在推入的過程當中會進行惟一值的判斷,使得同一個 watcher 在隊列中只存在一個,並在 nextTick 後再執行全部的 watcher 回調,這個時候纔會改變視圖

Vue 之因此這麼作是防止沒必要要的屢次渲染,例如你在 methods 中寫了個 10000 次的循環的方法,每一個循環都會改變一次視圖,致使隊列中有 10000 個 render watcher,最終觸發了 10000 次渲染,這就很是的不合理

而優化後只在第一次循環時將 render watcher 推入隊列,以後的 9999 次則只是數據的更新不會把相同的 render watcher 推入隊列,最終隊列中只有 1 個 render watcher

另外之因此數據更新是通常是同步的,而視圖是在 nextTick 後異步更新的,緣由在於只有這樣全部的 watcher 才能獲取到最終的數據,在同一個事件循環輪次中,異步任務永遠是晚於同步任務的

執行 afterEach 守衛

因此視圖的更新就被 Vue 延遲到 nextTick 後執行,先會在 updateRoute 中遍歷 afterHooks 執行 afterEach 守衛

監聽瀏覽器的前進後退事件

在執行完 afterEach 後,文檔的下一步是觸發 DOM 更新也就是視圖的更新,但其實 vue-router 還會作一些別的邏輯,例如給 hash 模式下的路由設置監聽事件,監聽瀏覽器的前進後退,以及一些滾動事件

updateRoute 方法執行後會執行 transitionTo 方法的成功回調,hash 模式最終會執行 setupListeners 設置監聽事件

圖12:

當瀏覽器點擊前進後退時,會再次執行 transitionTo 方法,即路由的跳轉邏輯,達到視圖的跳轉

history 模式一樣也會監聽這2個事件,只是監聽的時機不一樣,它是在實例化時進行監聽

圖13:

隨後會執行 ensureURL 方法,使用 pushState 或者 location.hash 的形式設置 url

執行 beforeRouteEnter 守衛中的回調

前面介紹 beforeRouterEnter 時提到,vue-router 會將 next 方法中的回調推入 postEnterCbs 數組中,當 confirmTransition 的成功回調執行完畢後,會把 postEnterCbs 數組放到 nextTick 後執行

圖14:

前面還提到,當在更新視圖的時候,Vue 會將視圖更新的 render watcher 也放在 nextTick 後執行,也就是說當 postEnterCbs 數組被執行前,會先執行視圖更新的邏輯

這就是爲何只有 beforeRouteEnter 守衛得到組件實例時,須要定義一個回調並傳入 next 函數中的緣由,由於守衛執行的時候是同步的,可是隻有在 nextTick 後才能得到組件實例, vue-router 經過回調的形式,將回調的觸發時機放到視圖更新以後,這樣就能保證可以得到組件實例

回調的參數

以前還留下一個問題是,在註冊回調時,會給回調傳入組件實例,也就是路由記錄中 instance[key], 而在註冊時它倒是一個空對象

答案顯而易見,仍是由於這個時候組件並無生成,因此不會有組件實例,可是當組件生成後咱們須要將 instance[key] 賦值爲當前組件

回到最初安裝 vue-router 的時候,vue-router 會全局混入 beforeCreate 和 destroyed 2個鉤子,以前我省略了 registerInstance 這個函數,完整的代碼是這樣的

圖15:

而這個 registerInstance 的做用正是當組件被生成時,給路由記錄的 instance 屬性添加當前視圖的組件實例( registerInstance 必定會在 next 的回調執行前執行,由於組件更新順序在 next 的回調以前,而 beforeCreate 是組件更新時執行的邏輯)

圖16:

最終在 router-view 組件中調用 matched.instances[name] = val 進行賦值,這樣在執行 next 的回調中就能夠獲取到組件實例

總結

  • 當異步組件解析成功後,會執行 beforeRouteEnter 守衛
  • 經過 Vue 核心庫的 defineReactive 方法,當 $route 被賦值時就會觸發 router-view 組件的從新渲染,達到更新視圖的功能
  • Vue 會異步更新視圖,因此 beforeRouteEnter 中須要使用回調的形式訪問到組件實例
  • vue-router 經過監聽瀏覽器的 popState 或者 hashChange 使得點擊前進後退也能更新視圖

一些感悟

我的認爲 vue-router 的源碼並非那麼容易理解,多層的回調很是跳躍(我的認爲若是 vue-router 使用 async/await 語法會容易理解的多),而且伴隨着不少邊緣狀況的處理,在閱讀源碼時,建議新建一個工程,找到源碼文件,多經過 debugger 的形式執行文中所說的關鍵函數,觀察參數以及調用棧的依賴關係

或許源碼的閱讀並不能像某些文章同樣直接對平常開發有所幫助,它的影響是長遠的,在源碼中每每用到了不少 JavaScript 技巧,例如閉包,柯里化,回調,異步編程,事件循環,原型繼承。而這些都是須要有足夠紮實的 JavaScript 基礎纔可以理解的,同時在閱讀的過程當中能夠進一步提高你的 JavaScript 基礎

不只如此,經過閱讀源碼可以對這個框架有着更深層的理解,而不是死記硬背框架某些的行爲,就好比爲何 beforeRouteEnter 中必需要經過 next 方法的回調形式才能得到 Vue 實例,以及路由守衛是怎麼根據文檔中的執行順序一步步執行的

我的以爲,關於源碼分析的文章並非那麼好理解,若是點開文章的你以爲有什麼不理解的,但願在評論區留言我會第一時間回答,這會幫助我改善文章質量,很是感謝~

參考資料

Vue.js 技術揭祕

相關文章
相關標籤/搜索