在使用 vue 開發單頁面(SPA)應用時,vue-router 是必不可少的技術;它的本質是監聽 URL 的變化然後比對路由規則顯示相應的頁面,并且不重新整理頁面;簡單的說就是,更新視圖但不重新請求頁面。
下面通過源碼來看一下 vue-router 的整個實作流程:
一、vue-router 源碼目錄結構
github位址:https://github.com/vuejs/vue-router
目前版本:
vue 2.6.11
vue-router 3.5.1
components:這裡面是兩個元件 router-view 和 router-link
history:這個是路由模式(mode),有三種方式
util:這裡是路由的功能函數和類
create-matcher 和 create-router-map 是路由解析和生成配置表
index:VueRouter類,也是整個插件的入口
install:提供插件安裝方法
二、Vue.use() 注冊插件
vue 在使用路由時需要調用 Vue.use(plugin) 方法進行注冊,代碼在 vue 源碼裡面 src/core/global-api/use.js檔案裡主要作用兩個:
1、緩存判斷是否已經注冊過,避免重複注冊
2、使用插件的 install 方法或者直接運作插件來注冊
3、這裡使用了 flow 的文法,在編譯時對 js 變量進行類型檢查,縮短調式時間減少類型錯誤引起的 bug
// 初始化use
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// 檢測插件是否已經被安裝
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1)
args.unshift(this)
// 調用插件的 install 方法或者直接運作插件,以實作插件的 install
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
三、路由安裝 install
注冊路由的時候需要調用路由的 install 方法,代碼在 vue-router 源碼的 src/install.js 檔案裡,是 vue-router 的安裝程式,該方法做了下面四件事:
1、緩存判斷是否已經安裝過,避免重複安裝
2、使用 Vue.mixin 混入 beforeCreate 和 destroyed 鈎子函數,這樣在 Vue 生命周期階段就會被調用
3、通過 Vue.prototype 定義 router 和 route 屬性,友善所有元件使用
4、全局注冊 router-view 和 router-link 元件;router-link 用于觸發路由的變化,router-view 用于觸發對應路由視圖的變化
import View from './components/view'
import Link from './components/link'
export let _Vue
// Vue.use安裝插件時候需要暴露的install方法
export function install (Vue) {
// 判斷是否已安裝過,安裝過直接 return 出來,沒安裝執行安裝程式
if (install.installed && _Vue === Vue) return
install.installed = true
// 把Vue指派給全局變量
_Vue = Vue
// 判斷是否已定義
const isDef = v => v !== undefined
//通過registerRouteInstance方法注冊router執行個體
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// 混淆進Vue執行個體,在boforeCreate與destroyed鈎子上混淆
Vue.mixin({
beforeCreate () {
// 在option上面存在router則代表是根元件
if (isDef(this.$options.router)) {
// 根路由設定為自己
this._routerRoot = this
// 儲存router
this._router = this.$options.router
// VueRouter對象的init方法
this._router.init(this)
// Vue内部方法,為對象defineProperty上在變化時通知的屬性,實作響應式
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 非根元件則直接從父元件中擷取,用于 router-view 層級判斷
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 通過registerRouteInstance方法注冊router執行個體
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
//在Vue.prototype挂載屬性,可以通過 this.$router、this.$route 來通路 Vue.prototype 上的 _router、_route
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 注冊router-view以及router-link元件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// 該對象儲存了兩個option合并的規則
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
四、VueRouter 執行個體化
安裝路由插件之後,會對 VueRouter 進行執行個體化然後将其傳入 Vue 執行個體的 options 中,在 vue-router 源碼的 src/index.js 檔案裡;下面是 VueRouter 的構造函數。
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}`)
}
}
}
構造函數有兩個重要的東西:
1、建立路由比對對象 matcher(核心)
這裡用到的 createMatcher 函數在 src/create-matcher.js 檔案裡,其作用是建立路由映射表,然後使用閉包的方法讓 addRoutes 和 match 函數能夠使用路由映射表的幾個對象,最後傳回一個 Matcher 對象。
//Matcher 的資料結構
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
};
//createMatcher 具體實作
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
//建立路由映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes (routes) {...}
//路由比對
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {...}
function redirect (
record: RouteRecord,
location: Location
): Route {...}
function alias (
record: RouteRecord,
location: Location,
matchAs: string
): Route {...}
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {...}
return {
match,
addRoutes
}
}
createMatcher()有兩個參數routes表示建立VueRouter新對象傳入的routes配置資訊,router表示VueRouter執行個體。
createRouteMap() 的作用就是對目前開發者傳入的 options.routes 進行路由映射化處理,并得到了三個路由容器 pathList、pathMap、nameMap,方法在 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 || []
// $flow-disable-line
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// $flow-disable-line
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
// 周遊路由配置,為每個配置添加路由記錄
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
// 確定通配符在最後
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
return {
pathList,
pathMap,
nameMap
}
}
createRouteMap 函數傳回三個屬性 —— pathList、pathMap、nameMap。然後通過 addRouteRecord 函數去向這三個屬性中增添資料。
下面是 addRouteRecord 方法, 利用遞歸方式解析嵌套路由。
//添加路由記錄
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
//擷取路由配置下的屬性
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.`
)
}
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
//格式化url 替換 /
const normalizedPath = normalizePath(
path,
parent,
pathToRegexpOptions.strict
)
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
//生成記錄對象
const record: RouteRecord = {
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) {
// 遞歸路由配置的 children 屬性,添加路由記錄
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)
})
}
// 如果路由有别名的話,給别名也添加路由記錄
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隻有第一個會起作用,後面的都會忽略
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
//命名路由添加記錄
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}" }`
)
}
}
}
2、根據 mode 采取不同的路由方式
1、vue-router 一共有三種路由模式(mode):hash、history、abstract,其中 abstract 是在非浏覽器環境下使用的路由模式,如 weex
2、預設是 hash 模式,如果傳入的是 history 模式,但是目前環境不支援則會降級為 hash 模式
3、如果目前環境是非浏覽器環境,則強制使用 abstract 模式
4、模式比對成功則會進行對應的初始化操作
五、路由初始化
當根元件調用 beforeCreate 鈎子函數的時候會執行路由初始化代碼,代碼在 src/index 檔案下面,是路由執行個體提供的一個方法。
/* 初始化 */
init (app: any /* Vue component instance */) {
/* 未安裝就調用init會抛出異常 */
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
/* 将目前vm執行個體儲存在app中 */
this.apps.push(app)
// main app already initialized.
/* 已存在說明已經被init過了,直接傳回 */
if (this.app) {
return
}
/* this.app儲存目前vm執行個體 */
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
路由初始化會進行路由跳轉,改變 URL 後渲染對應的元件。路由跳轉的核心的 history 的 transitionTo 方法。
六、路由切換
在 src/history/base 檔案下 History 執行個體提供的一個方法。
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 根據跳轉的 location 得到新的route
const route = this.router.match(location, this.current)
//确認切換路由
this.confirmTransition(route, () => {
//更新 route
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) })
}
})
}
在 transitionTo 方法中先調用match方法得到新的路由對象,然後調用 confirmTransition 方法是處理導航守衛的邏輯。
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
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
) {
this.ensureURL()
return abort()
}
//對比路由,解析出可複用的元件、失活的元件、需要渲染的元件
const {
updated,
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)
//導航守衛數組
const queue: Array<?NavigationGuard> = [].concat(
//失活的元件鈎子
extractLeaveGuards(deactivated),
//全局的 beforeEach 鈎子
this.router.beforeHooks,
//在目前路由改變,但是該元件被複用時調用
extractUpdateHooks(updated),
//需要渲染元件 enter 守衛鈎子
activated.map(m => m.beforeEnter),
//解析異步路由元件
resolveAsyncComponents(activated)
)
//儲存路由
this.pending = route
//疊代器,用于執行 queue 裡面的導航守衛鈎子
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: '/' }) -> 重定向
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
//上一次隊列執行完成之後再執行元件内的鈎子
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
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() })
})
}
})
})
}
通過 next 進行導航守衛的回調疊代,是以如果在代碼中使用了路由鈎子函數,那麼就必須在最後調用 next(),否則回調不執行,導航将無法繼續
七、路由同步(以hash模式為例)
在路由切換的時候,vue-router 會調用 push、go 等方法實作視圖與位址 url 的同步。
1、主動觸發
點選事件跳轉頁面,觸發 push 或 replace 方法,然後調用 transitionTo 方法裡面的 updateRoute 方法來更新 _route,進而觸發 router-view 的變化。
// src/history/hash.js
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
push 方法先檢測目前浏覽器是否支援 html5的 History API,如果支援則調用此 API進行 href的修改,否則直接對window.location.hash進行指派
2、改變位址欄 url,然後視圖同步
在路由初始化的時候會添加事件 setupHashListener 來監聽 hashchange 或 popstate;當路由變化時,會觸發對應的 push 或 replace 方法,然後調用 transitionTo 方法裡面的 updateRoute 方法來更新 _route,進而觸發 router-view 的變化。
// src/history/hash.js
setupListeners () {
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
})
}
八、路由元件
在 src/components/ 檔案夾下
1、router-view:元件挂載
該元件是無狀态 (沒有 data ) 和無執行個體 (沒有 this 上下文)的(功能元件)函數式元件。其通過路由比對擷取到對應的元件執行個體,通過 h函數動态生成元件,如果目前路由沒有比對到任何元件,則渲染一個注釋節點。
export default {
name: 'RouterView',
/*
https://cn.vuejs.org/v2/api/#functional
使元件無狀态 (沒有 data ) 和無執行個體 (沒有 this 上下文)。他們用一個簡單的 render 函數傳回虛拟節點使他們更容易渲染。
*/
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
// 标記位,标記是route-view元件
data.routerView = true
// 直接使用父元件的createElement函數
const h = parent.$createElement
// props的name,預設'default'
const name = props.name
// option中的VueRouter對象
const route = parent.$route
// 在parent上建立一個緩存對象
const cache = parent._routerViewCache || (parent._routerViewCache = {})
// 記錄元件深度
let depth = 0
// 标記是否是待用(非alive狀态)
let inactive = false
// _routerRoot中中存放了根元件的執行個體,這邊循環向上級通路,直到通路到根元件,得到depth深度
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
// 如果_inactive為true,代表是在keep-alive中且是待用(非alive狀态)
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
// 存放route-view元件的深度
data.routerViewDepth = depth
// 如果inactive為true說明在keep-alive元件中,直接從緩存中取
if (inactive) {
return h(cache[name], data, children)
}
const matched = route.matched[depth]
// 如果沒有比對到的路由,則渲染一個空節點
if (!matched) {
cache[name] = null
return h()
}
// 從成功比對到的路由中取出元件
const component = cache[name] = matched.components[name]
// 注冊執行個體的registration鈎子,這個函數将在執行個體被注入的加入到元件的生命鈎子(beforeCreate與destroyed)中被調用
data.registerRouteInstance = (vm, val) => {
// 第二個值不存在的時候為登出
// 擷取元件執行個體
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
//這裡有兩種情況,一種是val存在,則用val替換目前元件執行個體,另一種則是val不存在,則直接将val賦給instances
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)
}
}
主要作用就是拿到比對的元件進行渲染。
2、router-link:路由跳轉
router-link在執行 render函數的時候,會根據目前的路由狀态,給渲染出來的active元素添加 class,是以你可以借助此給active路由元素設定樣式等
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) {
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
}
// 渲染出 <a> 标簽,然後添加 href 屬性和點選事件
if (this.tag === 'a') {
data.on = on
data.attrs = { href }
} else {
// find the first <a> child and apply listener and 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
data.on = on
}
}
return h(this.tag, data, this.$slots.default)
}
}
最後、路由實作的過程簡化版
頁面渲染
1、Vue.use(Router) 注冊
2、注冊時調用 install 方法混入生命周期,定義 router 和 route 屬性,注冊 router-view 和 router-link 元件
3、生成 router 執行個體,根據配置數組(傳入的routes)生成路由配置記錄表,根據不同模式生成監控路由變化的History對象
4、生成 vue 執行個體,将 router 執行個體挂載到 vue 執行個體上面,挂載的時候 router 會執行最開始混入的生命周期函數
5、初始化結束,顯示預設頁面
路由點選更新
1、 router-link 綁定 click 方法,觸發 history.push 或 history.replace ,進而觸發 history.transitionTo 方法
2、ransitionTo 用于處理路由轉換,其中包含了 updateRoute 用于更新 _route
3、在 beforeCreate 中有劫持 _route 的方法,當 _route 變化後,觸發 router-view 的變化
位址變化路由更新
1、HashHistory 和 HTML5History 會分别監控 hashchange 和 popstate 來對路由變化作對用的處理
2、HashHistory 和 HTML5History 捕獲到變化後會對應執行 push 或 replace 方法,進而調用 transitionTo
3、然後更新 _route 觸發 router-view 的變化