本质就是借助响应式和组件化来实行内容的替换
路由注册
- 使用 Vue.use() 进行注册并获取当前 Vue 实例(为了使用 Vue 上的辅助函数),且会进行保存为后续去读响应式对象用
路由安装
- 通过 Vue.mixin() 定义了 beforeCreate/destroyed 两个钩子函数(由于是 mixin 之后每个组件对象都会合并这两个函数并执行)
- beforeCreate 内定义了 this._routerRoot/._router。并通过
defineReactive(this, '_route', this._router.history.current)往根实例上设置响应式- this._router.init(this) 来初始化 router**(重要,只在 new Vue() 阶段执行)**
- this._routerRoot 是通过 this.$parent 从父组件获取的,所以它是继承过来的
- beforeCreate 内定义了 this._routerRoot/._router。并通过
- 并在 Vue.prototype 上设置了 route 属性。实际返回的是 this._routerRoot._router/._route 内容
- 注册了
<router-link>/<router-view>两个组件 - 最后把整个 install() 函数挂到 VueRouter.install 上
Vue.mixin({
beforeCreate() {
/**
* this.$options 是当前组件初始化时的选项
* 读取的是 main.js 里的 new Vue({ router })
* 其他 x.vue 页面是没有配置 router 选项的
*/
if (this.$options.router) {
// !只有根组件才触发
/**
* 这么操作后 this.__routerRoot._router 就可以访问了
* 这样后面 $router 就可以获取了
*/
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this);
/**
* 使用 Vue.util.defineReactive 创建一个 响应式对象
* 对 app._route => this._route 进行劫持并添加响应式
* 注意 _route、route/current、$route 三个的关系
* - _route 是用于 响应式 绑定的对象
* - current/route ?这个是不是可以不用
* - $route 是组件内使用时的引用对象
*/
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {
// !非根组件
/**
* 组件树是树状结构的,按照组件嵌套初始化的规则
* 这里始终从它的父节点获取 _routerRoot 对象
*/
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
}
});
为什么使用 mixin 方式,猜测是为了获取响应式对象。因为该对象只设置在了根this上,所以通过执行beforeCreate时从父组件继承
VueRouter 对象
- 在
new VueRouter()对象创建时,内部创建了this.matcher = createMatcher(options.routes)路由匹配器。并按 mode 创建 this.history 对象(history/hash 方式) - 之后在
new Vue()执行 beforeCreate 钩子时,会执行 router.init() 函数 -> 进而执行history.transitionTo()/history.listen() 函数- 这时的 history.transitionTo() 会触发第一次的页面匹配渲染,从而展示内容
- history.listen() 用来添加路由匹配完成后的回调函数。实际是替换响应式的
_route从而让<routerView>能进行渲染更新
class VueRouter {
constructor(options) {
this.matcher = createMatcher(options.routes || []);
this.mode = options.mode || 'hash';
}
init() {
const history = this.history;
if (history instanceof HTML5History || history instanceof HashHistory) {
/**
* 初始化时执行一次,确保能渲染对应组件
* hashchange 只有在 hash 改变时生效
*/
history.transitionTo(history.getCurrentLocation(), () => {
history.setupListeners();
});
}
/**
* 绑定回调事件,确保每次 transitionTo 后在当前实例下能拿到数据
* app 指的是根节点。所以在根节点上新增 _route 属性
* 把回调传进去,确保每次 current 更改都能顺便更改 _route 触发响应式
*/
history.listen((route) => {
// this._route 已经是响应式的
app._route = route;
});
}
}
matcher
- createMatcher -> 通过 createRouteMap() 对传入的
routes进行处理生成一个路由表对象- createRouteMap() 内维护了
pathList/pathMap/nameMap3个和 RouterRecord 相关的映射关系变量- pathMap/nameMap 所有的记录都是平级的,父子关系通过
record.parent字段维护
- pathMap/nameMap 所有的记录都是平级的,父子关系通过
- createRouteMap() 内维护了
const pathList = ["/home", "/home/child1", "/home/child2", "/hello", "/hello/child1"]
// 这里的 component 就是后<router-view>组件用来替换的 VueComponent 实例
const pathMap = {
"/hello": {path: xxx, component: xxx, parent: xxx },
"/hello/child1": {path: xxx, component: xxx, parent: xxx },
}
- addRoutes() 本质就是往 pathList/pathMap 添加新的数据 -> createRouteMap() 维护新的关系
- match() 函数 -> 通过 path 匹配到的 record。然后再在 createRoute() 内通过该 record 去循环向上找(通过 child.parent 获取父)最终得到完整的路径
matched = [ {}, {}, {} ]是父->子这样的顺序。它们也正好来对应 组件的层级,这样就确保了 view 渲染对应的 component
function createRoute(record, path) {
const route = {
path,
matched: []
};
if (record) {
// 递归遍历
// 从 子.parent 获取 父,最终得到 父->子 这样的顺序
while (record) {
route.matched.unshift(record);
record = record.parent;
}
}
return route;
}
路径切换
- history.transitionTo(path, onComplete):是切换路由时都会触发的函数。
$router.push/replace()API也都是通过它的实现路由切换的- 通过拿到 this.matcher.match() 匹配的
record路由对象 -> 触发 this.confirmTransition() -> 触发 this.updateRoute() 最终会更新app._route = route(history.listen() 定义的)这样就会触发响应式配合<routeView>来更新界面 - 之后的 onComplete 分两种情况
- 在初始
init()时执行this.setupListeners()。是添加hashchange/popstate事件监听,这样我们修改URL时也能触发视图的更新 - 后续执行
$router.push/replace()等。是先去修改响应式对象触发视图更新,后再去修改实际的URL地址信息。
- 在初始
- 相同判断:由于 API 会触发
transitionTo()和改变URL,但改变URL又出发了监听的事件从而会再次执行transitionTo(),所以这里有个相同route判断来禁止循环触发
- 通过拿到 this.matcher.match() 匹配的
- URL 地址:正常我们通过
<routerLink>或api去更改路由实际执行的是history.transitionTo()而它只是按 path 匹配 record 并不修改 url 地址,所有在 transitionTo 执行完后还需要触发pushHash/replaceHash/pushState()去更改实际的URL地址/添加记录栈信息- 它们分别是封装了
location.hash/.replace及history.pushState/.replaceState
- 它们分别是封装了
- 组件:
<routerView>通过parent.$router = this._routerRoot._route来获取匹配到的 matched 内容- 由于 matched 是安装父->子顺序组合,实际就对应着 组件的嵌套层级。但是view组件本身不知道自己是第几个,所以需要有一个按照 parent.$vnode 的层级去获取当前的层级数
depth过程**。**最后通过 matched[depth] 获取对应的组件进行渲染展示
- 由于 matched 是安装父->子顺序组合,实际就对应着 组件的嵌套层级。但是view组件本身不知道自己是第几个,所以需要有一个按照 parent.$vnode 的层级去获取当前的层级数
// base.js/html5.js 合并
class History {
/**
* path 是要匹配的路径
*/
transitionTo(path, onComplete) {
// this.router = new VueRouter 实例 -> vueRouter.match -> this.matcher.match
let route = this.router.match(path);
const current = this.current;
/**
* 不管是 hash/history 添加相同的记录,都是会触发的
* 并且 router-link 也是调用 api 触发的
* 所以这里要对重复的内容进行拦截,防止重复渲染
*/
if (isSameRoute(route, current)) {
return;
}
this.updateRoute(route);
onComplete && onComplete(route);
}
/**
* 更新内部记录
* 更新外部的 app._route 用于触发试图刷新
*/
updateRoute(route) {
this.current = route;
this.cb && this.cb(route);
}
setupListeners() {
if (this.listeners.length > 0) return;
const handleRoutingEvent = () => {
const location = getLocation();
this.transitionTo(location);
};
// 注意 popstate 的触发条件
window.addEventListener('popstate', handleRoutingEvent);
this.listeners.push(() => {
window.removeEventListener('popstate', handleRoutingEvent);
});
}
push(path) {
this.transitionTo(path, (route) => {
pushState(route.path);
});
}
replace(path) {
this.transitionTo(path, (route) => {
pushState(route.path, true);
});
}
}
// <routerView> 组件
const RouterView = {
function: true,
render(h, {parent, children, data}) {
const { matched } = parent.$route;
// 标识此组件为 router-view
data.routerView = true;
/**
* 因为 matched 该路径下全量数据
* 所有按照 路径 层级找到对应要展示的 component
* 是由外往内分层展示。一个 view 对应一层,按层级对应
*/
let depth = 0;
while (parent) {
// 如果有 父组件 且 父组件为 <router-view> 说明索引需要加 1
if (parent.$vnode && parent.$vnode.data.routerView) {
depth += 1;
}
parent = parent.$parent;
}
// 这里的 depth 是对应层级要展示的路径
const record = matched[depth];
/**
* 把 routerView 替换为对应要展示的 component
*/
const component = record.component;
return h(component, data, children);
}
}