>PS:個人筆記,僅供參考,需要深入了解請閱讀參考資料。
[TOC]
# 參考資料
[https://ustbhuangyi.github.io/vue-analysis/prepare/directory.html#sfc](https://ustbhuangyi.github.io/vue-analysis/prepare/directory.html#sfc)
[https://yuchengkai.cn/docs/frontend/vue.html#%E8%B7%AF%E7%94%B1%E6%B3%A8%E5%86%8C](https://yuchengkai.cn/docs/frontend/vue.html#%E8%B7%AF%E7%94%B1%E6%B3%A8%E5%86%8C)
# 例子
看其提供的 API 來進行分析:
```js
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App'
Vue.use(VueRouter)
// 1. 定義(路由)組件。
// 可以從其他文件 import 進來
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
// 2. 定義路由
// 每個路由應該映射一個組件。 其中"component" 可以是
// 通過 Vue.extend() 創建的組件構造器,
// 或者,只是一個組件配置對象。
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
// 3. 創建 router 實例,然后傳 `routes` 配置
const router = new VueRouter({
routes // (縮寫)相當于 routes: routes
})
// 4. 創建和掛載根實例。
// 記得要通過 router 配置參數注入路由,
// 從而讓整個應用都有路由功能
const app = new Vue({
el: '#app',
render(h) {
return h(App)
},
router
})
```
# 路由注冊
先從`Vue.use(VueRouter)`說起。
Vue 從它的設計上就是一個漸進式 JavaScript 框架,它本身的核心是解決視圖渲染的問題,其它的能力就通過插件的方式來解決。Vue-Router 就是官方維護的路由插件,在介紹它的注冊實現之前,我們先來分析一下 Vue 通用的插件注冊原理。
Vue 提供了`Vue.use`的全局 API 來注冊這些插件,定義在`vue/src/core/global-api/use.js`中:
```js
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
```
`Vue.use`接受一個`plugin`參數,并且維護了一個`_installedPlugins`數組,它存儲所有注冊過的`plugin`;接著又會判斷`plugin`有沒有定義`install`方法,如果有的話則調用該方法,并且該方法執行的第一個參數是`Vue`;最后把`plugin`存儲到`installedPlugins`中。
可以看到 Vue 提供的插件注冊機制很簡單,每個插件都需要實現一個靜態的`install`方法,當我們執行`Vue.use`注冊插件的時候,就會執行這個`install`方法,并且在這個`install`方法的第一個參數我們可以拿到`Vue`對象,這樣的好處就是作為插件的編寫方不需要再額外去`import Vue`了。
# 路由安裝
Vue-Router 的入口文件是`src/index.js`,其中定義了`VueRouter`類,也實現了`install`的靜態方法:`VueRouter.install = install`,它的定義在`src/install.js`中:
```js
export let _Vue
export function install (Vue) {
// 確保 install 只調用一次
if (install.installed && _Vue === Vue) return
install.installed = true
// 把 Vue 賦值給全局變量
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// 混入鉤子函數
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
// 為 _route 屬性實現雙向綁定
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 全局注冊 router-link 和 router-view
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
const strats = Vue.config.optionMergeStrategies
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
```
通過`Vue.use(plugin)`時候,就是在執行`install`方法。`Vue-Router`的`install`方法會給每一個組件注入`beforeCreate`和`destoryed`鉤子函數,在`beforeCreate`做一些私有屬性定義和路由初始化工作.
# VueRouter 實例化
VueRouter 的實現是一個類,我們先對它做一個簡單地分析,它的定義在`src/index.js`中:
```js
export default class VueRouter {
static install: () => void;
static version: string;
app: any;
apps: Array<any>;
ready: boolean;
readyCbs: Array<Function>;
options: RouterOptions;
mode: string;
history: HashHistory | HTML5History | AbstractHistory;
matcher: Matcher;
fallback: boolean;
beforeHooks: Array<?NavigationGuard>;
resolveHooks: Array<?NavigationGuard>;
afterHooks: Array<?AfterNavigationHook>;
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 路由匹配對象
this.matcher = createMatcher(options.routes || [], this)
// 根據 mode 采取不同的路由方式
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = '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}`)
}
}
}
...
```
在實例化 VueRouter 的過程中,核心是創建一個路由匹配對象,并且根據 mode 來采取不同的路由方式。
## 路由匹配對象(matcher)
`matcher`相關的實現都在`src/create-matcher.js`中,我們先來看一下`matcher`的數據結構:
```js
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
};
```
`Matcher`返回了 2 個方法,`match`和`addRoutes`,`match`方法,顧名思義它是做匹配,那么匹配的是什么,在介紹之前,我們先了解路由中重要的 2 個概念,`Loaction`和`Route`,它們的數據結構定義在`flow/declarations.js`中。
- Location
```
declare type Location = {
_normalized?: boolean;
name?: string;
path?: string;
hash?: string;
query?: Dictionary<string>;
params?: Dictionary<string>;
append?: boolean;
replace?: boolean;
}
```
Vue-Router 中定義的`Location`數據結構和瀏覽器提供的`window.location`部分結構有點類似,它們都是對`url`的結構化描述。舉個例子:`/abc?foo=bar&baz=qux#hello`,它的`path`是`/abc`,`query`是`{foo:'bar',baz:'qux'}`。
- Route
```js
eclare type Route = {
path: string;
name: ?string;
hash: string;
query: Dictionary<string>;
params: Dictionary<string>;
fullPath: string;
matched: Array<RouteRecord>;
redirectedFrom?: string;
meta?: any;
}
```
`Route`表示的是路由中的一條線路,它除了描述了類似`Loctaion`的`path`、`query`、`hash`這些概念,還有`matched`表示匹配到的所有的`RouteRecord`。
### createMatcher
```js
export function createMatcher(
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
// 創建路由映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
// 路由匹配
function match(
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
//...
}
return {
match,
addRoutes
}
}
```
`createMatcher`函數的作用就是創建路由映射表,然后通過閉包的方式讓`addRoutes`和`match`函數能夠使用路由映射表的幾個對象,最后返回一個`Matcher`對象。
<br />
`createMathcer`首先執行的邏輯是`const { pathList, pathMap, nameMap } = createRouteMap(routes)`創建一個路由映射表,`createRouteMap`的定義在`src/create-route-map`中:
```js
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>;
pathMap: Dictionary<RouteRecord>;
nameMap: Dictionary<RouteRecord>;
} {
const pathList: Array<string> = oldPathList || []
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route) // 為每一個 route 生成 RouteRecord
})
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
return {
pathList, // 存儲所有的 path
pathMap, // 表示一個 path 到 RouteRecord 的映射關系
nameMap // 表示 name 到 RouteRecord 的映射關系
}
}
```
`createRouteMap`函數的目標是把用戶的路由配置轉換成一張路由映射表,它包含 3 個部分,`pathList`存儲所有的`path`,`pathMap`表示一個`path`到`RouteRecord`的映射關系,而`nameMap`表示`name`到`RouteRecord`的映射關系。
`RouteRecord`的數據結構如下:
```js
declare type RouteRecord = {
path: string;
regex: RouteRegExp;
components: Dictionary<any>;
instances: Dictionary<any>;
name: ?string;
parent: ?RouteRecord;
redirect: ?RedirectOption;
matchAs: ?string;
beforeEnter: ?NavigationGuard;
meta: any;
props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
}
```
由于`pathList`、`pathMap`、`nameMap`都是引用類型,所以在遍歷整個`routes`過程中去執行`addRouteRecord`方法,會不斷給他們添加數據。那么經過整個`createRouteMap`方法的執行,我們得到的就是`pathList`、`pathMap`和`nameMap`。其中`pathList`是為了記錄路由配置中的所有`path`,而`pathMap`和`nameMap`都是為了通過`path`和`name`能快速查到對應的`RouteRecord`。(忽略中間的詳細過程)
# 路由初始化和路由跳轉
## 路由初始化
當根組件調用`beforeCreate`鉤子函數時,會執行以下代碼
```js
beforeCreate () {
// 只有根組件有 router 屬性,所以根組件初始化時會初始化路由
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)
}
```
接下來看下路由初始化會做些什么
```js
init(app: any /* Vue component instance */) {
// 保存組件實例
this.apps.push(app)
// 如果根組件已經有了就返回
if (this.app) {
return
}
this.app = app
// 賦值路由模式
const history = this.history
// 判斷路由模式,以哈希模式為例
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
// 添加 hashchange 監聽
const setupHashListener = () => {
history.setupListeners()
}
// 路由跳轉
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// 該回調會在 transitionTo 中調用
// 對組件的 _route 屬性進行賦值,觸發組件渲染
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
```
在路由初始化時,核心就是進行路由的跳轉,改變 URL 然后渲染對應的組件。接下來來看一下路由是如何進行跳轉的。
## 路由跳轉
```js
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 獲取匹配的路由信息
const route = this.router.match(location, this.current)
// 確認切換路由
this.confirmTransition(route, () => {
// 以下為切換路由成功或失敗的回調
// 更新路由信息,對組件的 _route 屬性進行賦值,觸發組件渲染
// 調用 afterHooks 中的鉤子函數
this.updateRoute(route)
// 添加 hashchange 監聽
onComplete && onComplete(route)
// 更新 URL
this.ensureURL()
// 只執行一次 ready 回調
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => { cb(route) })
}
}, err => {
// 錯誤處理
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => { cb(err) })
}
})
}
```
在路由跳轉中,需要先獲取匹配的路由信息,所以先來看下如何獲取匹配的路由信息
```js
function match(
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// 序列化 url
// 比如對于該 url 來說 /abc?foo=bar&baz=qux##hello
// 會序列化路徑為 /abc
// 哈希為 ##hello
// 參數為 foo: 'bar', baz: 'qux'
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
// 如果是命名路由,就判斷記錄中是否有該命名路由配置
if (name) {
const record = nameMap[name]
// 沒找到表示沒有匹配的路由
if (!record) return _createRoute(null, location)
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
// 參數處理
if (typeof location.params !== 'object') {
location.params = {}
}
if (currentRoute && typeof currentRoute.params === 'object') {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key]
}
}
}
if (record) {
location.path = fillParams(
record.path,
location.params,
`named route "${name}"`
)
return _createRoute(record, location, redirectedFrom)
}
} 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.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
// 沒有匹配的路由
return _createRoute(null, location)
}
```
`createRoute`可以根據`record`和`location`創建出來,最終返回的是一條`Route`路徑,我們之前也介紹過它的數據結構。在 Vue-Router 中,所有的`Route`最終都會通過`createRoute`函數創建,并且它最后是不可以被外部修改的。
<br />
得到匹配的路由信息后就是做路由跳轉了,即執行`confirmTransition`。其核心就是判斷需要跳轉的路由是否存在于記錄中,然后執行各種導航守衛函數,最后完成 URL 的改變和組件的渲染。
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs