在上篇中主要敘述了 vue-router 中生成 $route 對象的時機,路由懶加載的原理,以及異步路由以前執行的一系列路由守衛vue
在本篇中會講述:node
同時本文會按照 vue-router 官網完整的導航解析流程的 7-12 步,逐個解析每一步的背後的原理git
圖1:github
文中的源碼截圖只保留核心邏輯 完整源碼地址vue-router
有興趣的朋友也能夠看我學習源碼時的詳細註釋源碼地址編程
vue-router 版本:3.0.2
數組
上文說到,當異步路由(組件)所有解析完畢後,會執行 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個問題咱們放到後面來討論,繼續往下走主線的流程
以後包含 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 才能獲取到最終的數據,在同一個事件循環輪次中,異步任務永遠是晚於同步任務的
因此視圖的更新就被 Vue 延遲到 nextTick 後執行,先會在 updateRoute
中遍歷 afterHooks 執行 afterEach 守衛
在執行完 afterEach 後,文檔的下一步是觸發 DOM 更新也就是視圖的更新,但其實 vue-router 還會作一些別的邏輯,例如給 hash 模式下的路由設置監聽事件,監聽瀏覽器的前進後退,以及一些滾動事件
在 updateRoute
方法執行後會執行 transitionTo
方法的成功回調,hash 模式最終會執行 setupListeners
設置監聽事件
圖12:
當瀏覽器點擊前進後退時,會再次執行 transitionTo
方法,即路由的跳轉邏輯,達到視圖的跳轉
history 模式一樣也會監聽這2個事件,只是監聽的時機不一樣,它是在實例化時進行監聽
圖13:
隨後會執行 ensureURL
方法,使用 pushState 或者 location.hash 的形式設置 url
前面介紹 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 的回調中就能夠獲取到組件實例
defineReactive
方法,當 $route 被賦值時就會觸發 router-view 組件的從新渲染,達到更新視圖的功能我的認爲 vue-router 的源碼並非那麼容易理解,多層的回調很是跳躍(我的認爲若是 vue-router 使用 async/await 語法會容易理解的多),而且伴隨着不少邊緣狀況的處理,在閱讀源碼時,建議新建一個工程,找到源碼文件,多經過 debugger 的形式執行文中所說的關鍵函數,觀察參數以及調用棧的依賴關係
或許源碼的閱讀並不能像某些文章同樣直接對平常開發有所幫助,它的影響是長遠的,在源碼中每每用到了不少 JavaScript 技巧,例如閉包,柯里化,回調,異步編程,事件循環,原型繼承。而這些都是須要有足夠紮實的 JavaScript 基礎纔可以理解的,同時在閱讀的過程當中能夠進一步提高你的 JavaScript 基礎
不只如此,經過閱讀源碼可以對這個框架有着更深層的理解,而不是死記硬背框架某些的行爲,就好比爲何 beforeRouteEnter 中必需要經過 next 方法的回調形式才能得到 Vue 實例,以及路由守衛是怎麼根據文檔中的執行順序一步步執行的
我的以爲,關於源碼分析的文章並非那麼好理解,若是點開文章的你以爲有什麼不理解的,但願在評論區留言我會第一時間回答,這會幫助我改善文章質量,很是感謝~