写在前面
最近有时间,准备重新学习一下 vue,工作之余整理了一下对原理及源码的收获,在此记录一下,方便以后回来复习
ps:本篇文章以vue2.6.x、vue3.2.x为例
基本使用
- 使用VueRouter插件
import Vue from 'vue';
import VueRouter from 'vue-router';
// 1.使用VueRouter插件
Vue.use(VueRouter);
- 创建实例
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
},
];
// 2.创建实例
const router = new VueRouter({
routes,
});
- 配置到Vue的
options中
// 3.配置到选项中
new Vue({
// ...其他options
router,
render: h => h(App),
}).$mount('#app');
// 4.在组件中使用
<router-link to="/home">home</router-link>
<router-view></router-view>
问题
- 为什么要先注册插件,注册插件做了哪些事情:
Vue.use(VueRouter) new VueRouter(...)做了哪些事情- 全局的
<router-link />组件和<router-view />组件是哪里来的
原理探究
1.为什么要先注册插件,注册插件做了哪些事情
-
Vue.use接收的参数是一个带有函数 install的对象,install包含参数Vue,将此参数保存,用于后续使用VueRouter.install = function(_Vue) { Vue = _Vue; }; -
通过
mixins延迟执行,取出根实例中的router,挂载到Vue的原型链上Vue.mixin({ beforeCreate() { // 这个钩子函数在每个组件创建实例时都会调用,但只有根组件被加入了router if (this.$options.router) { Vue.prototype.$router = this.$options.router; } }, }); -
全局声明
router-link组件和router-view组件- 3.1.
router-link组件:只做展示,点击时更新 urlthis.$slots.default是一个虚拟dom构成的数组Vue.component('router-link', { props: { to: { type: String, required: true, }, }, render(h) { return h( 'a', { attrs: { href: '#' + this.to, }, }, this.$slots.default // 虚拟dom构成的数组 ); }, }); - 3.2.
router-view组件:url 变化时,展示的组件对应改变,Vue 最大的特点是响应式,而组件每次渲染都是调用了render函数,因此我们希望将来某个数据变化时能够触发render执行,这部分内容在new VueRouter(...)时操作
- 3.1.
2.new VueRouter(...) 做了哪些事情
new VueRouter(...)返回的实例将来要挂载到 Vue 的原型链上,即组件中用的this.$router
-
定义一个响应式的属性
current,改变时触发<router-view />更新 -
Vue 提供了定义响应式数据的方法:
Vue.util.defineReactive这个方法会给我们的属性添加
observer, 然后通过getter函数 触发watcher的收集机制,将<router-view />的 render 方法收集到依赖中, 这样每次更新时就会执行render函数,从而更新视图。核心代码如下:
// 定义响应式数据 Vue.util.defineReactive(this, 'current', initial); Vue.component('router-view', { render(h) { let component = null; // 调用一次getter,观察者会将render函数收集 const route = this.$router.$options.routes[this.$router.current]; if (route) { component = route.component; } return h(component); }, }); -
监听路由改变
window.addEventListener('hashchange', () => { this.current = window.location.hash.slice(1); });因为此时
current已经是响应式数据,所以改变时会自动触发watcher更新:即<router-view />更新到这里,VueRouter的基本原理就结束了。
思考与总结
1. 定义响应式数据的方式除了Vue.util.defineReactive还有哪些,有何区别?
-
1.1. 创建一个 Vue 对象
const state = new Vue({ data: { count: 0 }, });state.count就是响应式对象,可以直接在视图中使用 -
1.2. Vue.observable
const state = Vue.observable({ count: 0 });同样
state.count是一个响应式对象,直接在视图中使用 -
1.3. 区别:
defineReactive是Vue源码中的内容,并没有在官方文档中出现过,而observable是2.6版本新出现的一个API,在低于 2.6 版本的 Vue 项目 中会报is not a function的错误。所以保险起见,使用defineReactive或new Vue(...)比较好
2. mixins的妙用:延迟执行
-
2.1. 这里是给全局的
Vue混入生命周期,因此每个组件都享用该mixins,需要在执行函数中加以判断,只在根组件中挂载$routerVue.mixin({ beforeCreate() { // 根实例才有该选项 if (this.$options.router) { Vue.prototype.$router = this.$options.router; } }, }); -
2.2. 由于混入组件的内容与当前组件容易出现命名冲突、以及方法变量来源不明,一直以来都对这个 api 绕着走。今天才发现了原来还有这么妙的使用方式,最开始以为是借助事件总线实现延迟执行,直到看了源码的这部分,才有了恍然大悟的感觉
3. 嵌套<router-view/>如何解决
源码是这样处理的:
在 VueRouter 中新增一个响应式属性matched,每次路由变化时执行一个叫做match的函数用来匹配当前路由。
在<router-view/>组件中,给虚拟 dom 的data中添加一个属性routerView,表示当前组件是<router-view />
然后在 render 函数中,创建一个变量depth表示当前路由的深度,其计算方式是通过while循环向上遍历,遇到<router-view />就+1
最后,<router-view/>组件渲染时从$router中取出matched,根据matched[depth]就可以获取到当前 url 下对应深度的路由组件
核心代码:
VueRouter.js
constructor(options) {
this.$options = options;
// 表示当前url
this.current = window.location.hash.slice(1) || '/';
// 定义一个响应式数组 matched,用来存储当前匹配的路由
Vue.util.defineReactive(this, 'matched', []);
this.match();
// 每次hash改变都重置matched,然后重新匹配
window.addEventListener('hashchange', () => {
this.current = window.location.hash.slice(1);
this.matched = [];
this.match();
});
}
match(routes) {
routes = routes || this.$options.routes;
for (const route of routes) {
if (route.path === '/' && this.current === '/') {
this.matched.push(route.component);
} else if (route.path !== '/' && this.current.includes(route.path)) {
this.matched.push(route.component);
}
if (route.children) {
this.match(route.children);
}
}
}
router-view (ps:源码中是函数式组件,这里为了简化就直接用非函数组件了)
Vue.component('router-view', {
render(h) {
// <router-view /> 的标志
this.$vnode.data.routerView = true;
// 嵌套路由
let depth = 0;
let parent = this.$parent;
while (parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {};
if (vnodeData.routerView) {
depth++;
}
parent = parent.$parent;
}
const component = this.$router.matched[depth];
return component ? h(component) : null;
},
});
4. 当前webpack环境在Vue.component中为什么不能用template?
在写router-view组件时,发现template无法使用,虽然不是VueRouter的内容,不过遇到了就总结下吧
在webpack环境中,默认情况下打包的 vue 是 runtime 版本,借助loader就完成了编译工作,并且runtime识别的是render 函数,因此没有 compiler模块,也就不能解析template