前幾天筆者看到一個問題:你真的瞭解vue-router的嗎?你知道vue-router的執行原理嗎?抱著這樣的問題,筆者開始了vue-router的原始碼探索之旅。本文並沒有逐行去深究原始碼,而是跟著筆者畫的流程圖來簡析每一步的執行流程。
剖析執行流程
筆者根據原始碼的結構和自己的理解事先畫好了一張流程圖,乍一看這張執行流程圖可能會有點蒙圈,筆者接下來會現根據這張圖分析下執行流程,然後再一步一步的剖析原始碼的核心部分。
為了便於我們理解這張執行流程圖,我們將掛載完vue-router的Vue例項列印出來看看都增加了什麼東西:- $options下的
router
物件很好理解,這個就是我們在例項化Vue的時候掛載的那個vue-router例項; _route
是一個響應式的路由route物件,這個物件會儲存我們路由資訊,它是通過Vue提供的Vue.util.defineReactive來實現響應式的,下面的get和set便是對它進行的資料劫持;_router
儲存的就是我們從$options中拿到的vue-router物件;_routerRoot
指向我們的Vue根節點;_routerViewCache
是我們對View的快取;$route
和$router
是定義在Vue.prototype上的兩個getter。前者指向_routerRoot下的_route,後者指向_routerRoot下的_router
接下來讓我們順順這個“眼花繚亂的圖”,以便於我們後面更好的理解之後的原始碼分析。
首先我們根據Vue的外掛機制安裝了vue-router,這裡其實做的很簡單,總結起來就是封裝了一個mixin,定義了兩個'原型',註冊了兩個元件。在這個mixin中,beforeCreate鉤子被呼叫然後判斷vue-router是否例項話了並初始化路由相關邏輯,前文提到的_routerRoot、_router、_route
便是在此時被定義的。定義了兩個“原型”是指在Vue.prototype上定一個兩個getter,也就$route和$router
。註冊了兩個元件是指在這裡註冊了我們後續會用到的RouterView和RouterLink這兩個元件。
然後我們建立了一個VueRouter的例項,並將它掛載在Vue的例項上,這時候VueRouter的例項中的constructor初始化了各種鉤子佇列;初始化了matcher用於做我們的路由匹配邏輯並建立路由物件;初始化了history來執行過渡邏輯並執行鉤子佇列。
接下里mixin中beforeCreate做的另一件事就是執行了我們VueRouter例項的init()方法執行初始化,這一套流程和我們點選RouteLink或者函式式控制路由的流程類似,這裡我就一起說了。在init方法中呼叫了history物件的transitionTo方法,然後去通過match獲取當前路由匹配的資料並建立了一個新的路由物件route,接下來拿著這個route物件去執行confirmTransition方法去執行鉤子佇列中的事件,最後通過updateRoute更新儲存當前路由資料的物件current,指向我們剛才建立的路由物件route。
最開始的時候我們說過_route
被定義成了響應式的 那麼一個路由更新之後,_route
物件會接收到響應並通知RouteView去更新檢視。
到此,流程就結束了,接下來我們將深入vue-router的原始碼去深度學習其原理。
剖析原始碼
說在前面
vue-router的原始碼都採用了flow作為型別檢驗,沒有配置flow的話可能會滿屏報錯,本文不對flow做過多的介紹了。為了便於大家的理解,下面的原始碼部分我會將flow相關的語法去掉。順便附上一些flow相關:
flow官方文件(需要科學上網):https://flow.org/ flow入門:https://zhuanlan.zhihu.com/p/26204569 flow配置:https://zhuanlan.zhihu.com/p/24649359
專案結構
在拿到一個專案的原始碼時候,我們首先要去看它的目錄結構:
其中src是我們的專案原始碼部分,它包含如下結構:- componets是RouterLink和RouterView這兩個元件;
- create-matcher.js就是我們建立match的入口檔案;
- create-route-map.js用於建立path列表,path map,name map等;
- history是建立hitory類的邏輯;
- index.js就是我們的入口檔案,其中建立了VueRouter這個類;
- install.js是我們掛載vue-router外掛的邏輯;
- util定義了很多工具函式;
應用入口
通常我們去構建一個Vue應用程式的時候入口檔案通常會這麼寫:
// app.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Main from '../components/main';
Vue.use(VueRouter);
const router = new VueRouter({
routes: [{
path: '/',
component: Main,
}],
});
// app.js
new Vue({
router,
template,
}).$mount('#app')
複製程式碼
我們可以看到vue-router是以外掛的形式安裝的,並且vue-router的例項也會掛載在Vue的例項上面。
外掛安裝
此時我們將目光移入原始碼的入口檔案,發現index.js中引入了install模組,並在VueRouter類上掛載了一個靜態的install方法。而且還判斷了環境中如果已經掛載了Vue則自動去使用這個外掛。
原始碼位置:/src/index.js
import { install } from './install'
import { inBrowser } from './util/dom'
// ...
export default class VueRouter {}
// ...
// 掛載install;
VueRouter.install = install
// 判斷如果window上掛載了Vue則自動使用外掛;
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
複製程式碼
接下來看install.js這個檔案,這個檔案匯出了export方法以供Vue.use去安裝:
原始碼位置:/src/install.js
import View from './components/view'
import Link from './components/link'
// export一個Vue的原因是可以不講Vue打包進外掛中而使用Vue一些方法;
// 只能在install之後才會存在這個Vue的例項;
export let _Vue
export function install (Vue) {
// 如果外掛已經安裝就return
if (install.installed && _Vue === Vue) return
install.installed = true
_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 () {
// this.$options.router為VueRouter例項;
// 這裡判斷例項是否已經掛載;
if (isDef(this.$options.router)) {
// 將router的根元件指向Vue例項
this._routerRoot = this
this._router = this.$options.router
// router初始化,呼叫VueRouter的init方法;
this._router.init(this)
// 使用Vue的defineReactive增加_route的響應式物件
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 將每一個元件的_routerRoot都指向根Vue例項;
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 註冊VueComponent 進行Observer處理;
registerInstance(this, this)
},
destroyed () {
// 登出VueComponent
registerInstance(this)
}
})
// 為$router和4route定義 << getter >> 分別指向_routerRoot的 _router 和 _route
// _router 為VueRouter的例項;
// _route 為一個儲存了路由資料的物件;
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 註冊元件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// Vue鉤子合併策略
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
複製程式碼
這裡需要注意的幾點:
- 匯出一個Vue引用:這是為了不用將整個Vue打包進去就可以使用Vue提供的一些API,當然,這些的前提就是vue-router必須被安裝掛載;
- 在Vue.prototype上定義兩個getter:Vue的元件都是Vue例項的一個擴充套件,他們都可以訪問prototype上的方法和屬性;
- 定義響應式_route物件:有了這個響應式的路由物件,就可以在路由更新的時候及時的通知RouterView去更新元件了;
例項化VueRouter
接下來我們來看VueRouter類的例項化,在constructor中主要做的就兩件事,建立matcher和建立history:
原始碼位置:/src/index.js
// ...
import { createMatcher } from './create-matcher'
import { supportsPushState } from './util/push-state'
import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'
// ...
export default class VueRouter {
constructor (options) {
this.app = null
this.apps = []
// VueRouter 配置項;
this.options = options
// 三個鉤子
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 建立路由匹配例項;傳人我們定義的routes:包含path和component的物件;
this.matcher = createMatcher(options.routes || [], this)
// 判斷模式
let mode = options.mode || 'hash'
// 判斷瀏覽器是否支援history,如果不支援則回退到hash模式;
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
// node執行環境 mode = 'abstract';
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
// 根據模式建立對應的history例項
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}`)
}
}
}
// ...
}
複製程式碼
建立matcher
順著思路我們先看createMatcher這個函式:
原始碼位置:/src/create-matcher.js
import VueRouter from './index'
import { resolvePath } from './util/path'
import { assert, warn } from './util/warn'
import { createRoute } from './util/route'
import { fillParams } from './util/params'
import { createRouteMap } from './create-route-map'
import { normalizeLocation } from './util/location'
// routes為我們初始化VueRouter的路由配置;
// router就是我們的VueRouter例項;
export function createMatcher (routes, router) {
// pathList是根據routes生成的path陣列;
// pathMap是根據path的名稱生成的map;
// 如果我們在路由配置上定義了name,那麼就會有這麼一個name的Map;
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// 根據新的routes生成路由;
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
// 路由匹配函式;
function match (raw, currentRoute, redirectedFrom) {
// 簡單講就是拿出我們path params query等等;
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
// 如果有name的話,就去name map中去找到這條路由記錄;
const record = nameMap[name]
if (process.env.NODE_ENV !== 'production') {
warn(record, `Route with name '${name}' does not exist`)
}
// 如果沒有這條路由記錄就去建立一條路由物件;
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)
}
}
}
// no match
return _createRoute(null, location)
}
// ...
function _createRoute (record, location, redirectedFrom) {
// 根據不同的條件去建立路由物件;
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
return {
match,
addRoutes
}
}
function matchRoute (regex, path, params) {
const m = path.match(regex)
if (!m) {
return false
} else if (!params) {
return true
}
for (let i = 1, len = m.length; i < len; ++i) {
const key = regex.keys[i - 1]
const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i]
if (key) {
params[key.name] = val
}
}
return true
}
function resolveRecordPath (path, record) {
return resolvePath(path, record.parent ? record.parent.path : '/', true)
}
複製程式碼
首先createMatcher會根據我們初始化VueRouter例項時候定義的routes配置,通過createRouteMap生成一份含有對應關係的map,具體邏輯下面我們會說到。然後返回一個包含match和addRoutes兩個方法的物件match,就是我們實現路由匹配的詳細邏輯,他會返回匹配的路由物件;addRoutes會就是新增路由的方法。
接下來我們順著剛才的思路去看create-route-map.js
原始碼位置:/src/create-route-map.js
/* @flow */
import Regexp from 'path-to-regexp'
import { cleanPath } from './util/path'
import { assert, warn } from './util/warn'
export function createRouteMap (routes, oldPathList, oldPathMap, oldNameMap) {
// the path list is used to control path matching priority
const pathList = oldPathList || []
// $flow-disable-line
const pathMap = oldPathMap || Object.create(null)
// $flow-disable-line
const nameMap = oldNameMap || Object.create(null)
// path列表
// path的map對映
// name的map對映
// 為配置的路由項增加路由記錄
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
// ensure wildcard routes are always at the end
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
// 返回包含path陣列,path map和name map的物件;
return {
pathList,
pathMap,
nameMap
}
}
function addRouteRecord (pathList, pathMap, nameMap, route, parent, matchAs) {
const { path, name } = route
if (process.env.NODE_ENV !== 'production') {
assert(path != null, `"path" is required in a route configuration.`)
assert(
typeof route.component !== 'string',
`route config "component" for path: ${String(path || name)} cannot be a ` +
`string id. Use an actual component instead.`
)
}
// 定義 path 到 Reg 的選項;
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
// 序列化path,'/'將會被替換成'';
const normalizedPath = normalizePath(
path,
parent,
pathToRegexpOptions.strict
)
// 正則匹配是否區分大小寫;
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
const record = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
// 如果有巢狀的子路由,則遞迴新增路由記錄;
if (route.children) {
// Warn if route is named, does not redirect and has a default child route.
// If users navigate to this route by name, the default child will
// not be rendered (GH Issue #629)
if (process.env.NODE_ENV !== 'production') {
if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
warn(
false,
`Named Route '${route.name}' has a default child route. ` +
`When navigating to this named route (:to="{name: '${route.name}'"), ` +
`the default child route will not be rendered. Remove the name from ` +
`this route and use the name of the default child route for named ` +
`links instead.`
)
}
}
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 如果路由含有別名,則為其新增別名路由記錄
// 關於alias
// https://router.vuejs.org/zh-cn/essentials/redirect-and-alias.html
if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias)
? route.alias
: [route.alias]
aliases.forEach(alias => {
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
})
}
// 更新path map
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// 為定義了name的路由更新 name map
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
false,
`Duplicate named routes definition: ` +
`{ name: "${name}", path: "${record.path}" }`
)
}
}
}
function compileRouteRegex (path, pathToRegexpOptions) {
const regex = Regexp(path, [], pathToRegexpOptions)
if (process.env.NODE_ENV !== 'production') {
const keys: any = Object.create(null)
regex.keys.forEach(key => {
warn(!keys[key.name], `Duplicate param keys in route with path: "${path}"`)
keys[key.name] = true
})
}
return regex
}
function normalizePath (path, parent, strict): string {
if (!strict) path = path.replace(/\/$/, '')
if (path[0] === '/') return path
if (parent == null) return path
return cleanPath(`${parent.path}/${path}`)
}
複製程式碼
從上述程式碼可以看出,create-route-map.js的就是根據使用者的routes配置的path、alias以及name來生成對應的路由記錄。
建立history
matcher這一部分算是講完了,接下來該說History的例項化了,從原始碼來說history資料夾下是有4個檔案的,base作為基類,另外三個繼承這個基類來分別處理vue-router的各種mode情況,這裡我們主要看base的邏輯就可以了。
// install 到處的Vue,避免Vue打包進專案增加體積;
import { START, isSameRoute } from '../util/route'
export class History {
constructor (router, base) {
this.router = router
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
// 生成一個基礎的route物件;
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
}
// ...
}
// ...
function normalizeBase (base: ?string): string {
if (!base) {
if (inBrowser) {
// respect <base> tag
const baseEl = document.querySelector('base')
base = (baseEl && baseEl.getAttribute('href')) || '/'
// strip full URL origin
base = base.replace(/^https?:\/\/[^\/]+/, '')
} else {
base = '/'
}
}
// make sure there's the starting slash
if (base.charAt(0) !== '/') {
base = '/' + base
}
// remove trailing slash
return base.replace(/\/$/, '')
}
複製程式碼
基礎的掛載和各種例項化都說完了之後,我們可以從init入手去看之後的流程了。
之前在講install的時候知道了在mixin中的beforeCreate鉤子裡執行了init,現在我們移步到VueRouter的init方法。原始碼位置:/src/index.js
// ...
init (app) {
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
// 從install中的呼叫我們知道,這個app就是我們例項化的vVue例項;
this.apps.push(app)
// main app already initialized.
if (this.app) {
return
}
// 將VueRouter內的app指向我們亙Vue例項;
this.app = app
const history = this.history
// 針對於 HTML5History 和 HashHistory 特殊處理,
// 因為在這兩種模式下才有可能存在進入時候的不是預設頁,
// 需要根據當前瀏覽器位址列裡的 path 或者 hash 來啟用對應的路由
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
//...
}
// ...
複製程式碼
可以看到初始化主要就是給app賦值,並且針對於HTML5History和HashHistory進行特殊的處理,因為在這兩種模式下才有可能存在進入時候的不是預設頁,需要根據當前瀏覽器位址列裡的path或者hash來啟用對應的路由,此時就是通過呼叫transitionTo來達到目的;
接下來來看看這個具體的transitionTo:
原始碼位置:/src/history/base.js
transitionTo (location, onComplete, onAbort) {
// localtion為我們當前頁面的路由;
// 呼叫VueRouter的match方法獲取匹配的路由物件,建立下一個狀態的路由物件;
// this.current是我們儲存的當前狀態的路由物件;
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => {
// 更新當前的route物件;
this.updateRoute(route)
onComplete && onComplete(route)
// 呼叫子類的方法更新url
this.ensureURL()
// fire ready cbs once
// 呼叫成功後的ready的回撥函式;
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => { cb(route) })
}
}, err => {
if (onAbort) {
onAbort(err)
}
// 呼叫失敗的err回撥函式;
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => { cb(err) })
}
})
}
confirmTransition (route, onComplete, onAbort) {
const current = this.current
const abort = err => {
if (isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => { cb(err) })
} else {
warn(false, 'uncaught error during route navigation:')
console.error(err)
}
}
onAbort && onAbort(err)
}
// 如果是同一個路由就不去跳轉;
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
// 呼叫子類的方法更新url
this.ensureURL()
return abort()
}
// 交叉比對當前路由的路由記錄和現在的這個路由的路由記錄
// 以便能準確得到父子路由更新的情況下可以確切的知道
// 哪些元件需要更新 哪些不需要更新
const {
updated,
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)
// 注意,matched裡頭儲存的是路由記錄的陣列;
// // 整個切換週期的佇列,待執行的各種鉤子更新佇列
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
// 提取元件的 beforeRouteLeave 鉤子
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
// 提取元件的 beforeRouteUpdate 鉤子
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
// 非同步處理元件
resolveAsyncComponents(activated)
)
// 儲存下一個狀態的路由
this.pending = route
// 每一個佇列執行的 iterator 函式
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' && (
typeof to.path === 'string' ||
typeof to.name === 'string'
))
) {
// next('/') or next({ path: '/' }) -> redirect
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 執行各種鉤子佇列
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// wait until async components are resolved before
// extracting in-component enter guards
// 等待非同步元件 OK 時,執行元件內的鉤子
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
// 在上次的佇列執行完成後再執行元件內的鉤子
// 因為需要等非同步元件以及是OK的情況下才能執行
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() })
})
}
})
})
}
updateRoute (route) {
const prev = this.current
// 將current指向我們更新後的route物件;
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
複製程式碼
邏輯看似複雜,實際上就是各種鉤子函式的來回處理,但是這裡要注意下,每一個路由route物件都會有一個matchd屬性,這個屬性包含一個路由記錄,這個記錄的生成在create-matcher.js中已經提到了。
等一下,我們好像漏了點東西,init後面還有一點沒說:
原始碼位置:/src/index.js
// 設定路由改變時候的監聽;
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
複製程式碼
在這裡設定了route改變之後的回撥函式, 會在confirmTransition中的onComplete回撥中呼叫, 並更新當前的_route的值,前面我們提到,_route是響應式的,那麼當其更新的時候就會去通知元件重新render渲染。
兩個元件
大體流程都看完了,接下來可以看看兩個元件了,我們先看RouterView元件: 原始碼位置:/src/components/view.js
import { warn } from '../util/warn'
export default {
name: 'RouterView',
functional: true,
props: {
// 試圖名稱,預設是default
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
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
// 拿到_route物件和快取物件;
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
// 元件層級
// 當 _routerRoot 指向 Vue 例項時就終止迴圈
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
// 處理 keep-alive 元件
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
// render previous view if the tree is inactive and kept-alive
// 渲染快取的 keep-alive 元件
if (inactive) {
return h(cache[name], data, children)
}
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]
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
// 新增註冊鉤子, 鉤子會被注入到元件的生命週期鉤子中
// 在 src/install.js, 會在 beforeCreate 鉤子中呼叫
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
// also register instance in prepatch hook
// in case the same component instance is reused across different routes
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
// resolve props
let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
if (propsToPass) {
// clone to prevent mutation
propsToPass = data.props = extend({}, propsToPass)
// pass non-declared props as attrs
const attrs = data.attrs = data.attrs || {}
for (const key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key] = propsToPass[key]
delete propsToPass[key]
}
}
}
return h(component, data, children)
}
}
function resolveProps (route, config) {
switch (typeof config) {
case 'undefined':
return
case 'object':
return config
case 'function':
return config(route)
case 'boolean':
return config ? route.params : undefined
default:
if (process.env.NODE_ENV !== 'production') {
warn(
false,
`props in "${route.path}" is a ${typeof config}, ` +
`expecting an object, function or boolean.`
)
}
}
}
function extend (to, from) {
for (const key in from) {
to[key] = from[key]
}
return to
}
複製程式碼
然後是RouterLink元件:
原始碼位置:/src/components/link.js
/* @flow */
import { createRoute, isSameRoute, isIncludedRoute } from '../util/route'
import { _Vue } from '../install'
// work around weird flow bug
const toTypes: Array<Function> = [String, Object]
const eventTypes: Array<Function> = [String, Array]
export default {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
exact: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
event: {
type: eventTypes,
default: 'click'
}
},
render (h: Function) {
// 獲取掛載的VueRouter例項
const router = this.$router
// 獲取當前的路由物件
const current = this.$route
// 獲取當前匹配的路由資訊
const { location, route, href } = router.resolve(this.to, current, this.append)
const classes = {}
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
const activeClass = this.activeClass == null
? activeClassFallback
: this.activeClass
const exactActiveClass = this.exactActiveClass == null
? exactActiveClassFallback
: this.exactActiveClass
const compareTarget = location.path
? createRoute(null, location, null, router)
: route
classes[exactActiveClass] = isSameRoute(current, compareTarget)
classes[activeClass] = this.exact
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location)
} else {
router.push(location)
}
}
}
// 事件繫結
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
this.event.forEach(e => { on[e] = handler })
} else {
on[this.event] = handler
}
const data: any = {
class: classes
}
if (this.tag === 'a') {
data.on = on
data.attrs = { href }
} else {
// find the first <a> child and apply listener and href
// 找到第一個 <a> 給予這個元素事件繫結和href屬性
const a = findAnchor(this.$slots.default)
if (a) {
// in case the <a> is a static node
a.isStatic = false
const extend = _Vue.util.extend
const aData = a.data = extend({}, a.data)
aData.on = on
const aAttrs = a.data.attrs = extend({}, a.data.attrs)
aAttrs.href = href
} else {
// doesn't have <a> child, apply listener to self
// 沒有 <a> 的話就給當前元素自身繫結事件
data.on = on
}
}
return h(this.tag, data, this.$slots.default)
}
}
function guardEvent (e) {
// don't redirect with control keys
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
// don't redirect when preventDefault called
if (e.defaultPrevented) return
// don't redirect on right click
if (e.button !== undefined && e.button !== 0) return
// don't redirect if `target="_blank"`
if (e.currentTarget && e.currentTarget.getAttribute) {
const target = e.currentTarget.getAttribute('target')
if (/\b_blank\b/i.test(target)) return
}
// this may be a Weex event which doesn't have this method
if (e.preventDefault) {
e.preventDefault()
}
return true
}
function findAnchor (children) {
if (children) {
let child
for (let i = 0; i < children.length; i++) {
child = children[i]
if (child.tag === 'a') {
return child
}
if (child.children && (child = findAnchor(child.children))) {
return child
}
}
}
}
複製程式碼
結語
到這裡,vue-router的原始碼剖析就告一段落了,雖然沒有逐行去理解作者的思想,但也算是整體上捋順了專案的執行原理,理解了原理也就更方便我們日常的需求開發了。最後,謝謝大家喜歡。