开始
我们上一章学习了整个Vue2
初始化的过程,那么这一章我们来学习一些Vue路由相关的知识
Keep-Alive
比如常见的Keep-Alive的场景,不熟悉的同学可以点击这里学习, 那么我就直接贴代码,来看看对应组件的内部实现
/* @flow */
import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'
type CacheEntry = {
name: ?string;
tag: ?string;
componentInstance: Component;
};
type CacheEntryMap = { [key: string]: ?CacheEntry };
// 获取组件的名称
function getComponentName (opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
// 匹配
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
// 对于include 跟 exclude的变化做动态删除
function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const entry: ?CacheEntry = cache[key]
if (entry) {
const name: ?string = entry.name
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
// 清楚对应节点的缓存
function pruneCacheEntry (
cache: CacheEntryMap,
key: string,
keys: Array<string>,
current?: VNode
) {
const entry: ?CacheEntry = cache[key]
if (entry && (!current || entry.tag !== current.tag)) {
entry.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
const patternTypes: Array<Function> = [String, RegExp, Array]
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
methods: {
cacheVNode() {
// 取出我们在this中缓存的Vnode引用
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) {
// 取出Vnode已经生成 tag, componentInstance, componentOptions 内容
const { tag, componentInstance, componentOptions } = vnodeToCache
// 缓存起来
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
// 将key推入key队列
keys.push(keyToCache)
// prune oldest entry
// 如果超出最大限制了
if (this.max && keys.length > parseInt(this.max)) {
// 清理掉keys数组中第一个
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
// 清空临时变量
this.vnodeToCache = null
}
}
},
created () {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
// 循环清空所有缓存实例
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.cacheVNode()
// 监听props的变化
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
updated () {
this.cacheVNode()
},
render () {
// 获取对应传入的Vnode节点
const slot = this.$slots.default
// 取出第一个组件组件节点
const vnode: VNode = getFirstComponentChild(slot)
// 获取组件对应的配置项
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
// 获取组件的名称
const name: ?string = getComponentName(componentOptions)
// 获取当前vm中已经初始化的props参数
const { include, exclude } = this
// 如果没有命中缓存,那就直接渲染当前组件
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
// this.data中定义的数据项
const { cache, keys } = this
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
// 如果已经缓存的当前的Key值,那么直接取出之前缓存的实例
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
// 刷新位置
remove(keys, key)
keys.push(key)
} else {
// delay setting the cache until update
this.vnodeToCache = vnode
this.keyToCache = key
}
// 设置标识符
vnode.data.keepAlive = true
}
// 返回命中的节点
return vnode || (slot && slot[0])
}
}
看完以上代码我们知道,我们<keep-alive>
组件实际上是通过在组件中缓存了组件的componentInstance
也就是源代码中的vm
来对数据进行缓存的,同时因为缓存vm
根据我们上一篇阅读的代码,我们可知在组件实例化过程中_update
函数执行结束后,生成的对应的dom节点会保存在vm.$el
中。通过重新挂在$el
达到保存DOM节点的效果。
vue-router
vue-router
基本属于在我们spa
的项目中一定会使用到的数据仓库,我这边是跟着Vue2.*的脚手架一起拉取的,所以我们这里看到的源码版本是3.*的版本。
入口文件
// src/main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
/* eslint-disable no-new */
const vue = new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
// src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import HelloWorld1 from '@/components/HelloWorld1'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
},
{
path: '/hello',
name: 'HelloWorld1',
component: HelloWorld1
}
]
})
我们可以看到首先执行的是Vue.use(Router)
代码语句我们看一下具体实现
// node_modules/vue-router/src/install.js
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
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
// 找到父节点的Vnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
Vue.mixin({
beforeCreate () {
// 如果当前组件的配置项中有router
if (isDef(this.$options.router)) {
// 那么当前组件为根组件
this._routerRoot = this
// 将_router 设置为配置的router,其实理解为这边转移了一下绑定的this
this._router = this.$options.router
// 调用_router的初始化方法
this._router.init(this)
// 对_route关键字设置可相应
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 设置_routerRoot
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
// 定义vue实例对象的$router数据代理
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
// 定义vue实例对象的$route数据代理
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 注册组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
// 对路由挂钩使用相同的挂钩合并策略
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
接着我们通过Router
构造我们的router
实例,那么让我们来看看VueRouter
的实现
VueRouter构造函数
// node_modules/vue-router/src/index.js
/* @flow */
import { install } from './install'
import { START } from './util/route'
import { assert, warn } from './util/warn'
import { inBrowser } from './util/dom'
import { cleanPath } from './util/path'
import { createMatcher } from './create-matcher'
import { normalizeLocation } from './util/location'
import { supportsPushState } from './util/push-state'
import { handleScroll } from './util/scroll'
import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'
import type { Matcher } from './create-matcher'
import { isNavigationFailure, NavigationFailureType } from './util/errors'
export default class VueRouter {
static install: () => void
static version: string
static isNavigationFailure: Function
static NavigationFailureType: any
static START_LOCATION: Route
app: any
apps: Array<any>
ready: boolean
readyCbs: Array<Function>
options: RouterOptions
mode: string
history: HashHistory | HTML5History | AbstractHistory
matcher: Matcher
// 当浏览器不支持 `history.pushState` 控制路由是否应该回退到 `hash` 模式。默认值为 `true`。
fallback: boolean
beforeHooks: Array<?NavigationGuard>
resolveHooks: Array<?NavigationGuard>
afterHooks: Array<?AfterNavigationHook>
constructor (options: RouterOptions = {}) {
if (process.env.NODE_ENV !== 'production') {
warn(this instanceof VueRouter, `Router must be called with the new operator.`)
}
// 初始化数据
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
/*
* 创建匹配器
*/
this.matcher = createMatcher(options.routes || [], this)
// 默认模式为'hash'模式 总共有三种模式‘hash’, ‘history’以及‘abstract’
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}`)
}
}
}
match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
// 返回当前的路由实例
get currentRoute (): ?Route {
return this.history && this.history.current
}
// 根节点初始化
init (app: any /* Vue component instance */) {
process.env.NODE_ENV !== 'production' &&
assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
// 插入跟节点列表
this.apps.push(app)
// set up app destroyed handler
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () => {
// clean out app from this.apps array once destroyed
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null
if (!this.app) this.history.teardown()
})
// main app previously initialized
// return as we don't need to set up new history listener
// 如果之前已经初始化过了,那么
if (this.app) {
return
}
// 初始化历史监听器
this.app = app
const history = this.history
// 如果是具体实现了的历史监听器
if (history instanceof HTML5History || history instanceof HashHistory) {
const handleInitialScroll = routeOrError => {
const from = history.current
const expectScroll = this.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
// 配置并支持pushState事件
if (supportsScroll && 'fullPath' in routeOrError) {
// 调度配置了的滚动行为
handleScroll(this, routeOrError, from, false)
}
}
const setupListeners = routeOrError => {
// 初始化浏览器的事件监听, 当页面的popstate或者hashchange事件发生时,切换对应的路由
history.setupListeners()
// 处理滚动
handleInitialScroll(routeOrError)
}
// 跳转到当前节点,处理页面的滚动行为
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
// 如果切换了路由节点,就变更根节点的`_route`
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
// 注册beforeEach钩子,并返回一个删除函数
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
// 注册beforeResolve钩子,并返回一个删除函数
beforeResolve (fn: Function): Function {
return registerHook(this.resolveHooks, fn)
}
// 注册afterEach钩子,并返回一个删除函数
afterEach (fn: Function): Function {
return registerHook(this.afterHooks, fn)
}
// 导航结束的钩子
onReady (cb: Function, errorCb?: Function) {
this.history.onReady(cb, errorCb)
}
/*
* 触发onError事件的方法
* 错误在一个路由守卫函数中被同步抛出;
* 错误在一个路由守卫函数中通过调用 `next(err)` 的方式异步捕获并处理;
* 渲染一个路由的过程中,需要尝试解析一个异步组件时发生错误。
*/
onError (errorCb: Function) {
this.history.onError(errorCb)
}
// 一系列的路由操作方法的代理
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.push(location, resolve, reject)
})
} else {
this.history.push(location, onComplete, onAbort)
}
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.replace(location, resolve, reject)
})
} else {
this.history.replace(location, onComplete, onAbort)
}
}
go (n: number) {
this.history.go(n)
}
back () {
this.go(-1)
}
forward () {
this.go(1)
}
// 返回根据to命中的组件
getMatchedComponents (to?: RawLocation | Route): Array<any> {
const route: any = to
? to.matched
? to
: this.resolve(to).route
: this.currentRoute
if (!route) {
return []
}
return [].concat.apply(
[],
route.matched.map(m => {
return Object.keys(m.components).map(key => {
return m.components[key]
})
})
)
}
// 解析目标的位置
resolve (
to: RawLocation,
current?: Route,
append?: boolean
): {
location: Location,
route: Route,
href: string,
// for backwards compat
normalizedTo: Location,
resolved: Route
} {
current = current || this.history.current
const location = normalizeLocation(to, current, append, this)
const route = this.match(location, current)
const fullPath = route.redirectedFrom || route.fullPath
const base = this.history.base
const href = createHref(base, fullPath, this.mode)
return {
location,
route,
href,
// for backwards compat
normalizedTo: location,
resolved: route
}
}
getRoutes () {
return this.matcher.getRoutes()
}
// 动态添加路由方法
addRoute (parentOrRoute: string | RouteConfig, route?: RouteConfig) {
this.matcher.addRoute(parentOrRoute, route)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
addRoutes (routes: Array<RouteConfig>) {
if (process.env.NODE_ENV !== 'production') {
warn(false, 'router.addRoutes() is deprecated and has been removed in Vue Router 4. Use router.addRoute() instead.')
}
this.matcher.addRoutes(routes)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
}
// 钩子注册的Help函数
function registerHook (list: Array<any>, fn: Function): Function {
list.push(fn)
return () => {
const i = list.indexOf(fn)
if (i > -1) list.splice(i, 1)
}
}
function createHref (base: string, fullPath: string, mode) {
var path = mode === 'hash' ? '#' + fullPath : fullPath
return base ? cleanPath(base + '/' + path) : path
}
VueRouter.install = install
VueRouter.version = '__VERSION__'
VueRouter.isNavigationFailure = isNavigationFailure
VueRouter.NavigationFailureType = NavigationFailureType
VueRouter.START_LOCATION = START
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
那么接着我们就以我们路由操作常见的push
方法为例跟进去HashHistory
看一下整个的实例化代码
HashHistory构造函数
// node_modules/vue-router/src/history/hash.js
/* @flow */
import type Router from '../index'
import { History } from './base'
import { cleanPath } from '../util/path'
import { getLocation } from './html5'
import { setupScroll, handleScroll } from '../util/scroll'
import { pushState, replaceState, supportsPushState } from '../util/push-state'
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
return
}
// 确保hash上有斜线,对路径进行转发
ensureSlash()
}
// this is delayed until the app mounts
// to avoid the hashchange listener being fired too early
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
this.listeners.push(setupScroll())
}
const handleRoutingEvent = () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
this.listeners.push(() => {
window.removeEventListener(eventType, handleRoutingEvent)
})
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
// 调用继承History的transitionTo,进行跳转
this.transitionTo(
location,
route => {
// 页面跳转完成后,通过pushState或者windoe.location.replace或者assign来改变hash
pushHash(route.fullPath)
// 处理滚动事件
handleScroll(this.router, route, fromRoute, false)
// 处理用户注册的完成事件
onComplete && onComplete(route)
},
onAbort
)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
// 页面跳转完成后,通过replaceState或者windoe.location.replace或者assign来改变hash
replaceHash(route.fullPath)
// 处理滚动事件
handleScroll(this.router, route, fromRoute, false)
// 处理用户注册的完成事件
onComplete && onComplete(route)
},
onAbort
)
}
go (n: number) {
window.history.go(n)
}
ensureURL (push?: boolean) {
const current = this.current.fullPath
if (getHash() !== current) {
push ? pushHash(current) : replaceHash(current)
}
}
getCurrentLocation () {
return getHash()
}
}
function checkFallback (base) {
const location = getLocation(base)
if (!/^\/#/.test(location)) {
window.location.replace(cleanPath(base + '/#' + location))
return true
}
}
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
export function getHash (): string {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
let href = window.location.href
const index = href.indexOf('#')
// empty path
if (index < 0) return ''
href = href.slice(index + 1)
return href
}
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
了解完HashHistory
以后我们发现,跳转的核心方法主要是transitionTo
,对应的url操作都来自于replaceHash
,那么我们先来了解一下History
对应的实例化过程, 并直接找到transitionTo
方法
History构造函数
// node_modules/vue-router/src/history/base.js
/* @flow */
import { _Vue } from '../install'
import type Router from '../index'
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
import { warn } from '../util/warn'
import { START, isSameRoute, handleRouteEntered } from '../util/route'
import {
flatten,
flatMapComponents,
resolveAsyncComponents
} from '../util/resolve-components'
import {
createNavigationDuplicatedError,
createNavigationCancelledError,
createNavigationRedirectedError,
createNavigationAbortedError,
isError,
isNavigationFailure,
NavigationFailureType
} from '../util/errors'
import { handleScroll } from '../util/scroll'
export class History {
router: Router
base: string
current: Route
pending: ?Route
cb: (r: Route) => void
ready: boolean
readyCbs: Array<Function>
readyErrorCbs: Array<Function>
errorCbs: Array<Function>
listeners: Array<Function>
cleanupListeners: Function
// implemented by sub-classes
+go: (n: number) => void
+push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
+replace: (
loc: RawLocation,
onComplete?: Function,
onAbort?: Function
) => void
+ensureURL: (push?: boolean) => void
+getCurrentLocation: () => string
+setupListeners: Function
constructor (router: Router, base: ?string) {
// 子类实例记录
this.router = router
// 格式化根地址
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
this.listeners = []
}
listen (cb: Function) {
this.cb = cb
}
onReady (cb: Function, errorCb: ?Function) {
if (this.ready) {
cb()
} else {
this.readyCbs.push(cb)
if (errorCb) {
this.readyErrorCbs.push(errorCb)
}
}
}
onError (errorCb: Function) {
this.errorCbs.push(errorCb)
}
transitionTo (
// 路由信息
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
let route
// catch redirect option https://github.com/vuejs/vue-router/issues/3201
try {
// 查找匹配到的路由信息
route = this.router.match(location, this.current)
} catch (e) {
this.errorCbs.forEach(cb => {
cb(e)
})
// Exception should still be thrown
throw e
}
// 将当前的路由信息设置为老的路由信息
const prev = this.current
// 确认开始切换页面
this.confirmTransition(
route,
// 确认完成方法回调
() => {
// app._route = route
// 注意RouterView组件根据当前_route的匹配器进行render操作
this.updateRoute(route)
// 完成回调(可能包括url操作, 路由滚动事件, 用户定义事件)
onComplete && onComplete(route)
// 确认当前的URL是正确的
this.ensureURL()
// 执行afterEach的钩子函数,此时页面,url滚动,用户自定义都已经完成
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
// 看下来ready全局只会触发一次
// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
// 错误回调
err => {
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
// Initial redirection should not mark the history as ready yet
// because it's triggered by the redirection instead
// https://github.com/vuejs/vue-router/issues/3225
// https://github.com/vuejs/vue-router/issues/3331
if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
this.ready = true
this.readyErrorCbs.forEach(cb => {
cb(err)
})
}
}
}
)
}
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
// 当前页面
const current = this.current
// 新页面
this.pending = route
const abort = err => {
// changed after adding errors with
// https://github.com/vuejs/vue-router/pull/3047 before that change,
// redirect and aborted navigation would produce an err == null
if (!isNavigationFailure(err) && isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err)
})
} else {
if (process.env.NODE_ENV !== 'production') {
warn(false, 'uncaught error during route navigation:')
}
console.error(err)
}
}
onAbort && onAbort(err)
}
const lastRouteIndex = route.matched.length - 1
const lastCurrentIndex = current.matched.length - 1
// 如果当前路由跟目标路由一致
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
lastRouteIndex === lastCurrentIndex &&
route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
) {
// 确认当前的URL是正确的
this.ensureURL()
// 如果存在hash,那么执行滚动事件
if (route.hash) {
handleScroll(this.router, current, route, false)
}
// 中断跳转任务
return abort(createNavigationDuplicatedError(current, route))
}
// 返回对应需要更新的路由,需要销毁的路由跟需要转换成活跃状态的路由数组
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
//创建队列
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
// beforeRouteLeave 的组件内联钩子
extractLeaveGuards(deactivated),
// global before hooks
// beforeEach的全局钩子
this.router.beforeHooks,
// in-component update hooks
// beforeRouteUpdate 的组件内联钩子
extractUpdateHooks(updated),
// in-config enter guards
// 执行初始化配置中的beforeEnter的钩子
activated.map(m => m.beforeEnter),
// async components
// 解析异步组件
resolveAsyncComponents(activated)
)
// 执行钩子的迭代器方法
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
try {
hook(
// 钩子中的to
route,
// 钩子中的from
current,
// 钩子中的next
(to: any) => {
// 不跳转
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(createNavigationAbortedError(current, route))
// 错误
} else if (isError(to)) {
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(createNavigationRedirectedError(current, route))
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)
}
}
// 依次对queue进行调用
runQueue(queue, iterator,
// 完成所有迭代后的回调函数
() => {
// wait until async components are resolved before
// extracting in-component enter guards
// 将要进入页面的内连钩子 beforeRouteEnter数组
const enterGuards = extractEnterGuards(activated)
// 加入 beforeResolve 钩子
const queue = enterGuards.concat(this.router.resolveHooks)
// 再次运行队列
runQueue(queue, iterator,
() => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this.pending = null
// 处理beforeRouteEnter获取vm的实例回调
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
})
})
}
updateRoute (route: Route) {
this.current = route
this.cb && this.cb(route)
}
setupListeners () {
// Default implementation is empty
}
teardown () {
// clean up event listeners
// https://github.com/vuejs/vue-router/issues/2341
this.listeners.forEach(cleanupListener => {
cleanupListener()
})
this.listeners = []
// reset current history route
// https://github.com/vuejs/vue-router/issues/3294
this.current = START
this.pending = null
}
}
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(/\/$/, '')
}
function resolveQueue (
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {
let i
const max = Math.max(current.length, next.length)
for (i = 0; i < max; i++) {
if (current[i] !== next[i]) {
break
}
}
return {
updated: next.slice(0, i),
activated: next.slice(i),
deactivated: current.slice(i)
}
}
function extractGuards (
records: Array<RouteRecord>,
name: string,
bind: Function,
reverse?: boolean
): Array<?Function> {
const guards = flatMapComponents(records, (def, instance, match, key) => {
const guard = extractGuard(def, name)
if (guard) {
return Array.isArray(guard)
? guard.map(guard => bind(guard, instance, match, key))
: bind(guard, instance, match, key)
}
})
return flatten(reverse ? guards.reverse() : guards)
}
function extractGuard (
def: Object | Function,
key: string
): NavigationGuard | Array<NavigationGuard> {
if (typeof def !== 'function') {
// extend now so that global mixins are applied.
def = _Vue.extend(def)
}
return def.options[key]
}
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}
function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
if (instance) {
return function boundRouteGuard () {
return guard.apply(instance, arguments)
}
}
}
function extractEnterGuards (
activated: Array<RouteRecord>
): Array<?Function> {
return extractGuards(
activated,
'beforeRouteEnter',
(guard, _, match, key) => {
return bindEnterGuard(guard, match, key)
}
)
}
function bindEnterGuard (
guard: NavigationGuard,
match: RouteRecord,
key: string
): NavigationGuard {
return function routeEnterGuard (to, from, next) {
return guard(to, from, cb => {
if (typeof cb === 'function') {
if (!match.enteredCbs[key]) {
match.enteredCbs[key] = []
}
match.enteredCbs[key].push(cb)
}
next(cb)
})
}
}
OK,那么到这里我们就完成实例化过程的大致学习,但是我相信大部分小伙伴对于我们渲染的完整过程还是有点把握不准,先不急我们先来学习一下<router-view>
组件的内部实现
<router-view>组件
// node_modules/vue-router/src/components/view.js
import { warn } from '../util/warn'
import { extend } from '../util/misc'
import { handleRouteEntered } from '../util/route'
export default {
name: 'RouterView',
// 函数式组件,
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
// used by devtools to display a router-view badge
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
// 从这里收集当前父组件的updateWatch
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.
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) {
depth++
}
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
// render previous view if the tree is inactive and kept-alive
if (inactive) {
const cachedData = cache[name]
const cachedComponent = cachedData && cachedData.component
if (cachedComponent) {
// #2301
// pass props
if (cachedData.configProps) {
fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
}
return h(cachedComponent, data, children)
} else {
// render previous empty view
return h()
}
}
// 取出匹配的路由
const matched = route.matched[depth]
// 获取对应组件
const component = matched && matched.components[name]
// render empty node if no matched route or no config component
if (!matched || !component) {
cache[name] = null
return h()
}
// cache component
cache[name] = { component }
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
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
}
// register instance in init hook
// in case kept-alive component be actived when routes changed
data.hook.init = (vnode) => {
if (vnode.data.keepAlive &&
vnode.componentInstance &&
vnode.componentInstance !== matched.instances[name]
) {
matched.instances[name] = vnode.componentInstance
}
// if the route transition has already been confirmed then we weren't
// able to call the cbs during confirmation as the component was not
// registered yet, so we call it here.
handleRouteEntered(route)
}
const configProps = matched.props && matched.props[name]
// save route and configProps in cache
if (configProps) {
extend(cache[name], {
route,
configProps
})
fillPropsinData(component, data, route, configProps)
}
// 返回对应子组件的Vnode节点
return h(component, data, children)
}
}
function fillPropsinData (component, data, route, configProps) {
// resolve props
let propsToPass = data.props = resolveProps(route, configProps)
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]
}
}
}
}
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.`
)
}
}
}
总结
到这里的话会不会更加清晰一点,实际流程大致如下:
-
插件注册时,在
beforeCreate
生命周期中为跟节点的_route
注册了观察者 -
在
RouterView
组件渲染时获取$route
,$route
为_route
的代理,那么此时_route
的dep
实例收集到RouterView
父组件的updateComponentWatcher
-
在
vue-router
发生页面切换时,处理完各种钩子后将通过history.listen(route => { this.apps.forEach(app => { app._route = route }) })
设置
_route
,触发Watcher
的update
,将任务插入微任务队列 -
到达浏览器事件循环微任务检查点,检查
promise.then()
触发执行updateComponentWatcher
-
updateComponentWatcher
调用组件的_redner
方法触发子节点重新渲染
那么以上就是整个vue-router
的更新路由逻辑。