「进阶篇」Vue Router 核心原理解析
前言
此篇为进阶篇,希望读者有 Vue.js,Vue Router 的使用经验,并对 Vue.js 核心原理有简单了解;
不会大篇幅手撕源码,会贴最核心的源码,对应的官方仓库源码地址会放到超上,可以配合着看;
对应的源码版本是 3.5.3,也就是 Vue.js 2.x 对应的 Vue Router 最新版本;
Vue Router 是标准写法,为了简单,下面会简称 router。
本文将用以下问题为线索展开讲 router 的原理:
- $router 和 $route 哪来的
- router 怎样知道要渲染哪个组件
- this.$router.push 调用了什么原生 API
- router-view 渲染的视图是怎样被更新的
- router 怎样知道要切换视图的
文末有总结大图
以下是本文使用的简单例子:
// main.js
import Vue from 'vue'
import App from './App'
import router from './router'
new Vue({
el: '#app',
// 挂载 Vue Router 实例
router,
components: { App },
template: '<App/>'
})
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import About from '@/components/About'
import Home1 from '@/components/Home1'
// 使用 Vue Router 插件
Vue.use(Router)
// 创建 Vue Router 实例
export default new Router({
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'Home',
component: Home,
children: [
{
path: 'home1',
name: 'Home1',
component: Home1
}
]
},
{
path: '/about',
name: 'About',
component: About
}
]
})
// App.vue
<template>
<div id="app">
<router-link to="/home">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
<router-link to="/home/home1">Go to Home1</router-link>
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
页面表现举例:
$router 和 $route 哪来的
我们在组件里使用 this.$router 去跳转路由、使用 this.$route 获取当前路由信息或监听路由变化,那它们是从哪里来的?答案是路由注册
路由注册
路由注册发生在 Vue.use 时,而 use 的就是 router 在 index.js 暴露的 VueRouter 类:
// demo代码:
import Router from 'vue-router'
// 使用 Vue Router 插件
Vue.use(Router)
// router 的 index.js
import { install } from './install'
// VueRouter 类
export default class VueRouter {
}
VueRouter.install = install
// install.js
export function install (Vue) {
// 全局混入钩子函数
Vue.mixin({
beforeCreate () {
// 有router配置项,代表是根组件,设置根router
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
} else {
// 非根组件,通过其父组件访问,一层层直到根组件
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
},
})
// Vue 原型上增加 $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)
}
所以 this.$router,this.$route 就是在注册路由时混入了全局的 beforeCreate 钩子,钩子里进行了 Vue 原型的拓展。
同时也清楚了 router-view 和 router-link 的来源。
VueRouter 类
我们先看最核心部分
export default class VueRouter {
constructor (options) {
// 确定路由模式,浏览器环境默认是 hash,Node.js环境默认是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
// 根据模式实例化不同的 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}`)
}
}
}
}
constructor 里重要的两个事情:1. 确定路由模式,2. 根据模式创建 History 实例。
如上,history 类有 base 基类,不同模式有对应的 abstract 类、hash 类、html5 类,继承于 base 类,history 实例处理路由切换、路由跳转等等事情。
init
VueRouter 的 init 发生在刚才说的 beforeCreate 钩子里
// beforeCreate 钩子里调用了 init
this._router.init(this)
// VueRouter类的 init 实例方法
init(app) {
// 保存 router 实例
this.app = app
const history = this.history
if (history instanceof HTML5History || history instanceof HashHistory) {
const setupListeners = routeOrError => {
// 待揭秘
history.setupListeners()
}
// 路由切换
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
}
init 里最主要处理了 history.transitionTo,transitionTo 有调用了 setupListeners,先有个印象即可。
router 怎样知道要渲染哪个组件
用户传入路由配置后,router 是怎样知道要渲染哪个组件的,答案是 Matcher
Matcher
Matcher 是匹配器,处理路由匹配,创建 matcher 发生在 VueRouter 类的构造函数里
this.matcher = createMatcher(options.routes || [], this)
// create-matcher.js
export function createMatcher(routes, router){
// 创建映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// 根据我们要跳转的路由匹配到组件,比如 this.$router.push('/about')
function match() {
}
}
createRouteMap
createRouteMap 负责创建路由映射表
export function createRouteMap(routes, oldPathList, oldPathMap, oldNameMap){
const pathList: Array<string> = oldPathList || []
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
...
return {
pathList,
pathMap,
nameMap
}
}
其中的处理细节先不用关注,打印一下例子里的路由映射表就很清楚有什么内容了:
pathList【path 列表】、pathMap【path 到 RouteRecord 的映射】、nameMap【name 到RouteRecord 的映射】,有了路由映射表之后想定位到 RouteRecord 就很容易了
其中 router 一些数据结构如下:源码
match 方法
match 方法就是从刚才生成的路由映射表里面取出 RouterRecord
// create-matcher.js
function match(raw, currentRoute, redirectedFrom){
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
// name 的情况
...
} else if (location.path) {
// path 的情况
...
}
}
this.$router.push 调用了什么原生 API
this.$router.push 用于跳转路由,内部调用的是 transitionTo 做路由切换,
以 hash 模式为例
// history/hash.js
// push 方法
push (location, onComplete, onAbort) {
// transitionTo 做路由切换,在里面调用了刚才的 matcher 的 match 方法匹配路由
// transitionTo 第2个和第3个参数是回调函数
this.transitionTo(
location,
route => {
pushHash(route.fullPath)
onComplete && onComplete(route)
},
onAbort
)
}
// 更新 url,如果支持 h5 的 pushState api,就使用 pushState 的方式,
// 否则设置 window.location.hash
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
history 模式就是调用 pushState 方法
pushState 方法
export function pushState (url, replace) {
// 获取 window.history
const history = window.history
try {
if (replace) {
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
// 调用 replaceState
history.replaceState(stateCopy, '', url)
} else {
// 调用 pushState
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
...
}
}
router-view 渲染的视图是怎样被更新的
router-view 用于渲染传入路由配置对应的组件
export default {
name: 'RouterView',
functional: true,
render(_, { props, children, parent, data }) {
...
// 标识
data.routerView = true
// 通过 depth 由 router-view 组件向上遍历直到根组件,
// 遇到其他的 router-view 组件则路由深度+1
// 用 depth 帮助找到对应的 RouterRecord
let depth = 0
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) {
depth++
}
parent = parent.$parent
}
data.routerViewDepth = depth
// 获取匹配的组件
const route = parent.$route
const matched = route.matched[depth]
const component = matched && matched.components[name]
...
// 渲染对应的组件
const h = parent.$createElement
return h(component, data, children)
}
}
比如例子中的二级路由 home1
因为是二级路由,所以深度 depth 是 1,找到如下图的 home1 组件
更新
那么每次路由切换之后,怎样触发了渲染新视图呢?
每次 transitionTo 完成后会执行添加的回调函数,回调函数里更新了当前路由信息
在 VueRouter 的 init 方法里注册了回调:
history.listen(route => {
this.apps.forEach(app => {
// 更新当前路由信息 _route
app._route = route
})
})
而在组件的 beforeCreate 钩子里把 _route 变成了响应式的,在 router-view 的 render 函数里访问到了 parent.$route,也就是访问到了 _route,
所以一旦 _route 改变了,就触发了 router-view 组件的重新渲染
// 把 _route 变成响应式的
Vue.util.defineReactive(this, '_route', this._router.history.current)
router 怎样知道要切换视图的
到现在我们已经清楚了 router 是怎样切换视图的,那当我们点击浏览器的后退按钮、前进按钮的时候是怎样触发视图切换的呢?
答案是 VueRouter 在 init 的时候做了事件监听 setupListeners
setupListeners
popstate 事件:在做出浏览器动作时,才会触发该事件,调用 window.history.pushState 或 replaceState 不会触发,文档
hashchange 事件:hash 变化时触发
核心原理总结
本文从5个问题出发,解析了 Vue Router 的核心原理,而其它分支比如导航守卫是如何实现的等等可以自己去了解,先了解了核心原理再看其他部分也是水到渠成。
本身前端路由的实现并不复杂,Vue Router 更多的是考虑怎样和 Vue.js 的核心能力结合起来,应用到 Vue.js 生态中去。
对 Vue Router 的原理有哪一部分想和我聊聊的,可以在评论区留言。
「进阶篇」Vue Router 核心原理解析的更多相关文章
- 二十三、详述 IntelliJ IDEA 中恢复代码的方法「进阶篇」
咱们已经了解了如何将代码恢复至某一版本,但是通过Local History恢复代码有的时候并不方便,例如咱们将项目中的代码进行了多处修改,这时通过Local History恢复代码就显得很麻烦,因为它 ...
- PHP丨PHP基础知识之条件语SWITCH判断「理论篇」
Switch在一些计算机语言中是保留字,其作用大多情况下是进行判断选择.以PHP来说,switch(开关语句)常和case break default一起使用 典型结构 switch($control ...
- PHP丨PHP基础知识之PHP基础入门——函数「理论篇」
前两天讲过PHP基础知识的判断条件和流程控制,今天来讲讲PHP基础知识之PHP基础入门--函数! 一.函数的声明与使用 1.函数名是标识符之一,只能有数字字母下划线,开头不能是数字. 函数名的命名,须 ...
- PHP丨PHP基础知识之流程控制WHILE循环「理论篇」
昨天讲完FOR循环今天来讲讲他的兄弟WHILE循环!进入正题: while是计算机的一种基本循环模式.当满足条件时进入循环,进入循环后,当条件不满足时,跳出循环.while语句的一般表达式为:whil ...
- PHP丨PHP基础知识之条件语IF判断「理论篇」
if语句是指编程语言(包括c语言.C#.VB.java.php.汇编语言等)中用来判定所给定的条件是否满足,根据判定的结果(真或假)决定执行给出的两种操作之一. if语句概述 if语句是指编程语言(包 ...
- Android进阶:七、Retrofit2.0原理解析之最简流程【下】
紧接上文Android进阶:七.Retrofit2.0原理解析之最简流程[上] 一.请求参数整理 我们定义的接口已经被实现,但是我们还是不知道我们注解的请求方式,参数类型等是如何发起网络请求的呢? 这 ...
- vue响应式原理解析
# Vue响应式原理解析 首先定义了四个核心的js文件 - 1. observer.js 观察者函数,用来设置data的get和set函数,并且把watcher存放在dep中 - 2. watcher ...
- 【算法】(查找你附近的人) GeoHash核心原理解析及代码实现
本文地址 原文地址 分享提纲: 0. 引子 1. 感性认识GeoHash 2. GeoHash算法的步骤 3. GeoHash Base32编码长度与精度 4. GeoHash算法 5. 使用注意点( ...
- 一篇不一样的docker原理解析
转自:https://zhuanlan.zhihu.com/p/22382728 https://zhuanlan.zhihu.com/p/22403015 在学习docker的过程中,我发现目前do ...
随机推荐
- Linux安装JDK8环境
1.下载JDK包 点击同意下载后,会让你注册oracel账号,登录了才能下载 2.上传到linux服务器,然后解压 解压命令(注意jdk的版本名称不一定相同): tar -zxvf jdk-8u181 ...
- 5. 堪比JMeter的.Net压测工具 - Crank 实战篇 - 接口以及场景压测
目录 堪比JMeter的.Net压测工具 - Crank 入门篇 堪比JMeter的.Net压测工具 - Crank 进阶篇 - 认识yml 堪比JMeter的.Net压测工具 - Crank 进阶篇 ...
- LGP2891题解
题目大意 有 \(n\) 只奶牛,\(q\) 种食物和 \(p\) 种饮料,每只奶牛喜欢一些饮料和食物,但只能那一种,求最小配对数量. 首先来看一下这道题的简化版:没有饮料,该怎么做呢? 我会!裸的二 ...
- ArcGIS热点分析
许多论文中一般会有热点分析图,ArcGIS中提供了热点分析的功能. 先看下描述:给定一组加权要素,使用 Getis-Ord Gi* 统计识别具有统计显著性的热点和冷点. 其实非常简单,今天博主就跟大家 ...
- 前端—我的第一篇博客 梦开始的地方(面向对象版tab栏)
这是我的第一篇博客 博客生涯才开始 但是人生已经过去了二十个年头了 才开始弄这个 也没搞得太懂 我原本的想法是想搞个源代码上来 但是看了半天好像就只能传html源代码 那我还有css js的部分呢 我 ...
- CodeUp Problem D: More is better
根据题目意思,输入的每一对A.B都是直接朋友,并且最后只会得到一个集合,该集合就是Mr Wang选择的男孩. 因此很容易写出代码,甚至不需要自己构建一个并查集,只需要使用C++的set模板,每次读入一 ...
- [转载]nc命令详解
最近在搞反向连接,试来试去发现最好的工具还是nc.正好趁这个机会把nc的用法总结一下: 1.端口扫描: nc -vv ip port 例:nc -vv 192.168.1.1 5000 扫描192.1 ...
- 16经典的SPI Flash的扇区擦除flash_se功能
一设计功能 对SPI_flash进行扇区擦除,分为写指令和扇区擦除两个时序部分. 二设计知识点 我简单理解flash,第一它是掉电不丢失数据的存储器,第二它每次写入新数据前首先得擦除数据,分为扇区擦除 ...
- ::before和:after中的的双冒号和单冒号有什么区别及这两个伪元素的作用
::before和:after中的的双冒号和单冒号有什么区别及这两个伪元素的作用 单冒号(:)用于CSS3伪类,双冒号(::)用于CSS3伪元素(伪元素由双冒号和伪元素名称组成),为了兼容已有的伪元素 ...
- Spring Data Jpa 更新操作
第一步,通过Repository对象把实体根据ID查询出来 第二部,往查出来的实体对象进行set各个字段 第三步,通过Repository接口的save方法进行保存 保存和更新方式(已知两种) 第一种 ...