首页 > 技术文章 > 浅读Vue-Router源码记录

xuanyuandai 2021-05-19 14:50 原文

vue-router源码拾遗

但凡是使用过Vue-router的前端开发工作者,都知道Vue-router包括两种实现模式:hash和history。为了对这两种模式有更直观的认识,我选择简略阅读源码并在此记录。


vue-router是Vue.js框架的路由插件,下面从源码入手,边看代码边看原理,由浅入深学习vue-router实现两种路由模式的方法。

vue-router的模式参数

模式参数:mode

const router = new VueRouter({
    mode: 'history',
    routes: [...]
})

创建VueRouter实例的时候,直接将mode以构造函数参数的形式传入,在VueRouter类定义(src/index.js)中,使用如下

export default class VueRouter {
    mode: string;//传入的string类型
    history: HashHistory | HTML5History | AbstractHistory; //实际调用的对象属性
    matcher: Matcher; // url正则匹配方法
    fallback: boolean; // 如果浏览器不支持,history需要回滚为hash模式
    ...
    let mode = options.mode || 'hash' //默认是hash模式
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash' // 传入模式为history,但是浏览器不支持,则回滚
    }
    if (!inBrowser) {
      mode = 'abstract' // 不在浏览器环境下,强制设置为‘abstract’
    }
    this.mode = mode

    // 根据mode的值,确定要使用的类,并实例化
    switch(this.mode) {
        case 'history':
            this.history = new HTML5History(this, option.base)
            break;
        case 'hash: 
            this.history = new HashHistory(this, option.base, this.fallback)
            break;
        case 'abstract':
            this.history = new AbstractHistory(this, option.base)
            break;
        default: 
            ...
    }
    // 通过mode确定好history实例后,进行实例的初始化和监听
    init (app: any /* Vue component instance */) {
        const history = this.history

        // 根据history的类别执行相应的初始化操作和监听
        if (history instanceof HTML5History) {
            // 'history'模式初始化
            history.transitionTo(history.getCurrentLocation())
        } else if (history instanceof HashHistory) {
            const setupHashListener = () => {
                history.setupListeners()
            }
            // 'hash'模式初始化
            history.transitionTo(
                history.getCurrentLocation(),
                setupHashListener,
                setupHashListener
            )
        }

        // 添加监听
        history.listen(route => {
            this.apps.forEach((app) => {
                app._route = route
            })
        })
    }
}

自此,基本完成了对mode字段的前期校验和后期使用,history的实例也已经初始化完成。接下来就是路由的一些基本操作,比如push(),replace(),onReady()等。
接着以上源码往下看,以下代码只留存关键行

onReady (cb: Function, errorCb?: Function) {
    this.history.onReady(cb, errorCb)
}

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // this.history会经过初始化操作,这里就会调用原生history的push方法
    this.history.push(location, resolve, reject)
    // this.history.push(location, onComplete, onAbort)
}

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // replace也是原生方法的调用
    this.history.replace(location, resolve, reject)
    // this.history.replace(location, onComplete, onAbort)
}

从以上代码可看出,VueRouter类中的方法可以认为是一个代理,实际是调用的具体history对象的对应方法,在init()方法中初始化时,会根据history对象具体的类别执行不同的操作。

说了这么多,什么时候调以及如何调用HTML5History和HashHistory有了大致的了解,接下来就看看这两个类是怎么实现的


HashHistory

源码文件路径:src/history/hash

原理回顾

hash("#")符号加在URL上只是用于指示网页中的位置,#符号本身及它后边的字符称为hash,可以通过window.location.hash属性读取。

  1. hash虽然加在URL中,但是并不会被包含在http请求中,它对服务器端无用,因此改变hash并不会重载页面。
  2. hash可以添加监听事件(window.addEventListener('hashchange', fn, false))。
  3. hash每次被改动后,都会在浏览器访问历史中增加一个记录。

hash的以上特点,就注定可以用来实现“更新视图但不重新请求页面”

代码解读

构造函数

// 继承History基类
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
        }
        ensureSlash()
    }
    function checkFallback (base) {
        // 得到除去base的真正的location值
        const location = getLocation(base)
        if (!/^\/#/.test(location)) {
            // 如果不是以/#开头,就降级处理,降级为hash模式下的/#开头
            window.location.replace(cleanPath(base + '/#' + location))
            return true
        }
    }
    function ensureSlash (): boolean {
        // 获取hash
        const path = getHash()
        if (path.charAt(0) === '/') {
            // 如果是/开头,直接返回
            return true
        }
        // 不是/开头,就手动增加开头/
        replaceHash('/' + path)
        return false
    }
}

这里什么时候算降级呢?就是不支持history api的情况

push()

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // transitionTo():父类中定义用来处理路由变化的基础逻辑的方法
    this.transitionTo(
        location,
        route => {
            pushHash(route.fullPath)
            handleScroll(this.router, route, fromRoute, false)
            onComplete && onComplete(route)
        },
        onAbort
    )
}

function pushHash (path) {
    // supportsPushState:src/util/push-state.js中定义,浏览器环境并且支持pushState()方法
    if (supportsPushState) {
        pushState(getUrl(path))
    } else {
        // 不支持,直接赋值操作
        window.location.hash = path
    }
}

以上代码可看出push()方法只是对url中的hash进行了基本的赋值,那视图如何更新呢?继续看父类中的transitionTo()方法
源码路径:src/history/base.js

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // 路由匹配过程,这个后边详解
    const route = this.router.match(location, this.current)
    this.confirmTransition(
        route,
        () => {
            this.updateRoute(route)
            ...
        }
    )
}
updateRoute (route: Route) {
    ...
    this.cb && this.cb(route)
    ...
}

在路由改变之后,调用了History中的this.cb方法,而this.cb的定义如下:

listen (cb: Function) {
    this.cb = cb
}

listen方法作为基础类的方法,我们不难想到,在主类index.js中history初始化之后,必然有使用的痕迹

init (app: any /* Vue component instance */) {
    this.apps.push(app)
    history.listen(route => {
        this.apps.forEach((app) => {
            app._route = route
        })
    })
}

根据注释可以看出入参的app是Vue组件实例,但是我们知道Vue本身的组件定义中是没有有关路由内置属性_route的,VueRouter.install = install如果组件中要有这个属性,应该是在插件加载的地方,即VueRouter的install()方法里,我们F12看下

export function install (Vue) {
    // 混入
    Vue.mixin({
        beforeCreate () {
            if (isDef(this.$options.router)) {
                // 混入到哪个组件,this就指向哪个组件
                this._routerRoot = this
                // VueRouter实例
                this._router = this.$options.router
                // VueRouter中的init(),由于这是全局混入,所以this===Vue组件实例
                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)
        },
        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.mixin()全局注册了混入,影响注册后创建的每个Vue实例,在beforeCreate钩子函数中,使用Vue.util.defineReactive设置了响应式的_route属性,当路由当_route值变化,就会调用Vue实例的render()方法,更新视图。

replace()

replace()与push()略有不同,它不是将新路由添加到浏览器访问历史记录,而是直接替换当前路由

// index.js
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.replace(location, resolve, reject)
      })
    } else {
      this.history.replace(location, onComplete, onAbort)
    }
}

// history/hash.js
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        replaceHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
}
function replaceHash (path) {
    if (supportsPushState) {
        // 这个方法干啥的呢
        replaceState(getUrl(path))
    } else {
        // 这里调用原生的replace方法
        window.location.replace(getUrl(path))
    }
}

// util/push-state.js
export function replaceState (url?: string) {
    // 这里调的和push()一样,都是走pushState()方法
    pushState(url, true)
}
export function pushState (url?: string, replace?: boolean) {
    const history = window.history
    if (replace) {
        // 如果是replace,进这里
        // 重写history.state进新对象,防止修改造成影响
        const stateCopy = extend({}, history.state)
        stateCopy.key = getStateKey()
        // 调用原生replaceState
        history.replaceState(stateCopy, '', url)
    } else {
        // 不是replace,调用原生的pushState
        history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
}

地址栏监听

以上分析的push和replace方法,是在Vue组件的逻辑代码中调用,但是在实际场景中,还可以直接对地址栏进行修改,达到改变路由的目的,这在HashHistory是如何实现的,继续去HashHistory类代码中寻找答案。可以发现有这样一个方法setupListeners(),里边的window.addEventListener()跟上文提到的HashHistory的特性2不谋而合

setupListeners () {
    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    // 重点是这里面的hashchange监听事件
    window.addEventListener(
        supportsPushState ? 'popstate' : 'hashchange',
        () => {
            const current = this.current
            if (!ensureSlash()) {
                // 如果不符合hash要求
                return
            }
            // 这里是基类中的视图更新
            this.transitionTo(getHash(), route => {
                if (supportsScroll) {
                    handleScroll(this.router, route, current, true)
                }
                if (!supportsPushState) {
                    // 直接修改地址栏,调用replaceHash方法
                    replaceHash(route.fullPath)
                }
            })
        }
    )
}

HTML5History

源码文件路径:src/history/html5

源码解读

History interface是浏览器历史记录栈提供的接口,通过back(),forward().go()等方法,实现读取浏览器历史记录栈信息的功能,进行跳转操作。从HTML5开始,提供两个新的方法:pushState()和replaceState(),这在上文已经接触过,直接打开源码src/html5.js

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
        //这里直接调用基层的pushState
        pushState(cleanPath(this.base + 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
        replaceState(cleanPath(this.base + route.fullPath))
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
    }, onAbort)
}

相较HashHistory方式,HTML5History的push和replace在route的处理上,少了赋值层,

// hash=>pushHash
window.location.hash = path

// hash=>replaceHash
window.location.replace(getUrl(path))

而是直接调用了history中的基层方法

地址栏监听

// 获取路径
const initLocation = getLocation(this.base)
// 添加监听方法
window.addEventListener('popstate', e => {
    const current = this.current

    // Avoiding first `popstate` event dispatched in some browsers but first
    // history route not updated since async guard at the same time.
    const location = getLocation(this.base)
        if (this.current === START && location === initLocation) {
            return
        }

    this.transitionTo(location, route => {
        if (supportsScroll) {
            handleScroll(router, route, current, true)
        }
    })
})

AbstractHistory

源码解读

export class AbstractHistory extends History {
    index: number
    // 用于存放路由记录的数组,仿浏览器历史记录栈
    stack: Array<Route>

    constructor (router: Router, base: ?string) {
        super(router, base)
        this.stack = []
        this.index = -1
    }
    // 以push为例,任何路由操作都是操作stack数组
    push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
        this.transitionTo(
            location,
            route => {
                this.stack = this.stack.slice(0, this.index + 1).concat(route)
                this.index++
                onComplete && onComplete(route)
            },
            onAbort
        )
    }
    ...
}

AbstractHistory的操作方式较为简单,初始化一个仿浏览器历史记录栈的路由数组,push(),replace()等操作,都对该数组进行操作。


路由匹配过程

VueRouter的基本工作流程大致就如上分析,这里有个很重要的点,需要单独分析一下,就是路由的匹配过程。
为了方便代码解读,需要先认清几个基本概念:

  1. Location:对url的结构化描述,例如:{path: '/main', query: {age: 25}, name: 'mainPage'}等
  2. rowLocation:type rowLocation = Location | string
  3. Route:表示一条路由
    export interface Route {
        path: string
        name?: string | null
        hash: string
        query: Dictionary<string | (string | null)[]>
        params: Dictionary<string>
        fullPath: string
        matched: RouteRecord[] //匹配的所有的RouteRecord对象
        redirectedFrom?: string
        meta?: any
    }
  1. RouteRecord:表示路由记录对象
    export interface RouteRecord {
        path: string
        regex: RegExp // 正则规则
        components: Dictionary<Component>
        instances: Dictionary<Vue> // vue实例
        name?: string
        parent?: RouteRecord
        redirect?: RedirectOption
        matchAs?: string
        meta: any
        beforeEnter?: (
            route: Route,
            redirect: (location: RawLocation) => void,
            next: () => void
        ) => any  // 钩子函数
        props:
            | boolean
            | Object
            | RoutePropsFunction
            | Dictionary<boolean | Object | RoutePropsFunction>
    }

对以上类型有基本认识后,来看下matcher实现代码

export function createMatcher (routes: Array<RouteConfig>, router: VueRouter): Matcher {
    // 这个方法往下看
    const { pathList, pathMap, nameMap } = createRouteMap(routes)
    // 动态添加路由配置
    function addRoutes (routes) {
        createRouteMap(routes, pathList, pathMap, nameMap)
    }
    // match主函数,根据传入的raw和currentRoute 计算出新的路径并返回
    function match (raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location): Route {
        // 获取路由path query hash等
        const location = normalizeLocation(raw, currentRoute, false, router)
        const { name } = location

        if (name) {
            // 如果传入了name参数,找到该name对应的记录
            const record = nameMap[name]
            // 没有匹配记录,则创建
            if (!record) return _createRoute(null, location)
            // 所有参数键值
            const paramNames = record.regex.keys
                .filter(key => !key.optional)
                .map(key => key.name)
            // 检查获取的路由参数类型,保证是object
            if (typeof location.params !== 'object') {
                location.params = {}
            }
            // vueRouter的参数对应赋值给浏览器的路由参数
            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]
                    }
                }
            }

            location.path = fillParams(record.path, location.params, `named route "${name}"`)
            // 创建
            return _createRoute(record, location, redirectedFrom)
        } else if (location.path) {
            // 没有name参数,传了path
            location.params = {}
            // pathList
            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)
    }
}

在源码文件src/create-route-map中找到createRouteMap()方法,入参传了路由配置routes,返回的是三个属性:

  1. pathList:存储所有的path值
  2. pathMap:path到RouteRecord的映射关系
  3. nameMap:name到RouteRecord的映射关系

其中的主要函数分析一下

function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
    const { path, name } = route

    const pathToRegexpOptions: PathToRegexpOptions =
        route.pathToRegexpOptions || {}
    // path格式清理,确保格式正确
    const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)

    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) {
        route.children.forEach(child => {
        const childMatchAs = matchAs
            ? cleanPath(`${matchAs}/${child.path}`)
            : undefined
        addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
        })
    }
    // 记录path并且创建path到RouteRecord的映射
    if (!pathMap[record.path]) {
        pathList.push(record.path)
        pathMap[record.path] = record
    }

    // 如果路由对象存在别名,将别名递归调用,进行映射的创建
    if (route.alias !== undefined) {
        const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
        for (let i = 0; i < aliases.length; ++i) {
            const alias = aliases[i]

            const aliasRoute = {
                path: alias,
                children: route.children
            }
            addRouteRecord(
                pathList,
                pathMap,
                nameMap,
                aliasRoute,
                parent,
                record.path || '/' // matchAs
            )
        }
    }
    // 创建name 到 RouteRecord 的映射
    if (name) {
        if (!nameMap[name]) {
            nameMap[name] = record
        }
    }
}

Vue-router组件分析

routerLink作为点击跳转的组件,要实现的也是类似标签的能力

props: {
    to: {
      type: toTypes,
      required: true
    }, // 跳转路径
    tag: {
      type: String,
      default: 'a'
    }, // 渲染出的标签类型,默认是<a>
    exact: Boolean,
    append: Boolean,
    replace: Boolean, // 调用replace还是push
    activeClass: String, // 激活后的class
    exactActiveClass: String,
    ariaCurrentValue: {
      type: String,
      default: 'page'
    },
    event: {
      type: eventTypes,
      default: 'click'
    } // 可以触发导航的事件,默认是click,还可以是'mouseover' 等
},
// 主要方法,render()
render (h: Function) {
    const router = this.$router
    const current = this.$route
    const { location, route, href } = router.resolve(
      this.to,
      current,
      this.append
    )

    const classes = {}
    // ...这里省去一些赋值初始化语句

    // handler方法用于绑定
    // 绑定后,点击触发
    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
            //replace逻辑,触发VueRouter的replace()更新路由
            router.replace(location, noop)
        } else {
            // 添加逻辑,触发VueRouter的push()更新路由
            router.push(location, noop)
        }
      }
    }
    // 绑定click事件,忽略所有默认的点击事件
    const on = { click: guardEvent }
    if (Array.isArray(this.event)) {
        // 如果传了event为数组类型
        this.event.forEach(e => {
            on[e] = handler
        })
    } else {
        on[this.event] = handler
    }
    // 赋值初始化操作,创建元素需要附加的数据
    const data: any = { class: classes }

    if (this.tag === 'a') {
        // 如果是<a>元素,直接绑定
        data.on = on
        data.attrs = { href, 'aria-current': ariaCurrentValue }
    } else {
      // 找到第一个<a>标签
      const a = findAnchor(this.$slots.default)
      if (a) {
        // 为<a>标签绑定事件
        a.isStatic = false
        const aData = (a.data = extend({}, a.data))
        aData.on = aData.on || {}
        // transform existing events in both objects into arrays so we can push later
        for (const event in aData.on) {
          const handler = aData.on[event]
          if (event in on) {
            aData.on[event] = Array.isArray(handler) ? handler : [handler]
          }
        }
        // append new listeners for router-link
        for (const event in on) {
          if (event in aData.on) {
            // on[event] is always a function
            aData.on[event].push(on[event])
          } else {
            aData.on[event] = handler
          }
        }
        // 标签的所有属性=>attrs
        const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
        // 赋值href属性
        aAttrs.href = href
        aAttrs['aria-current'] = ariaCurrentValue
      } else {
        // 没有<a>标签,就为当前元素绑定
        data.on = on
      }
    }
    // 创建Vnode $createElement
    return h(this.tag, data, this.$slots.default)
  }
}
// click点击事件自定义
function guardEvent (e) {
    // 忽略带有功能键的点击
    if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
    // 忽略点击引起的默认动作
    if (e.defaultPrevented) return
    // 忽略鼠标右键方法
    if (e.button !== undefined && e.button !== 0) return
    // 忽略 `target="_blank"` 属性
    if (e.currentTarget && e.currentTarget.getAttribute) {
        const target = e.currentTarget.getAttribute('target')
        if (/\b_blank\b/i.test(target)) return
    }
    // 阻止默认行为,防止跳转
    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
            }
        }
    }
}

router-view

作为视图显示组件,要根据routerLink的点击结果显示对应的Vue组件视图,还要可实现层级嵌套展示

render (_, { props, children, parent, data }) {
    // props: props
    // children: 所有子节点
    // parent: 父组件的引用
    // data: 要创建的组件的属性
    
    data.routerView = true

    // 父组件的$createElement函数
    const h = parent.$createElement
    const name = props.name
    // 父组件(当前)的路由对象
    const route = parent.$route
    // 父组件(当前)的路由缓存信息
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

    // 嵌套的层级深度
    let depth = 0
    // 是否keepAlive内
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
        // 如果父组件存在,并且父组件还存在父组件,就获取父组件的Vnode的data,没有就{}
        const vnodeData = parent.$vnode ? parent.$vnode.data : {}
        if (vnodeData.routerView) {
            // 父组件routerView存在,层级+1
            depth++
        }
        if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
            // 如果设置了keepAlive
            inactive = true
        }
        // 向上一级
        parent = parent.$parent
    }
    // 赋值嵌套层级
    data.routerViewDepth = depth

    // 设置keepAlive
    if (inactive) {
      // 获取当前缓存
      const cachedData = cache[name]
      // 获取当前缓存的Vue组件
      const cachedComponent = cachedData && cachedData.component
      if (cachedComponent) {
        // 创建缓存的组件
        return h(cachedComponent, data, children)
      } else {
        // 没找到组件就创建空组件
        return h()
      }
    }
    // 根据嵌套层级获取当前要显示的路由对象
    const matched = route.matched[depth]
    // 要显示的组件
    const component = matched && matched.components[name]

    // 为空 将当前cache设为null,请创建空组件
    if (!matched || !component) {
      cache[name] = null
      return h()
    }

    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
      }
    }

    const configProps = matched.props && matched.props[name]
    // save route and configProps in cachce
    if (configProps) {
      extend(cache[name], {
        route,
        configProps
      })
      fillPropsinData(component, data, route, configProps)
    }
    // 最后创建组件
    return h(component, data, children)
  }

总结

vue-router源码大致先记录在这里,仅作为学习记录,供以后回头查看。
如有不妥之处,还请指正。

推荐阅读