一、vue-router原理解析
整体内容较长,需要一定的耐心,可以先理解大概思想再进行一个内容一个内容的突破。我们先从vue-router的基本使用开始讲起。
1.1 定义一个路由文件
// router.js
import Router from 'vue-router'
Vue.use(Router)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
children:[
{
path: 'a',
name: 'a',
component: ()=>import(/* webpackChunkName: "aboutA" */ '../views/AboutA.vue')
},
{
path: 'b',
name: 'b',
component: ()=>import(/* webpackChunkName: "aboutA" */ '../views/AboutB.vue')
}
]
}
]
const router = new VueRouter({
routes
})
router.beforeEach((to, from, next) => {
console.log('beforeEach')
next()
})
export default router
1.2 在main.js中引入
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router,
name: 'main',
render: h => h(App)
}).$mount('#app')
1.3 在app.vue中添加router-view与router-link组件
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
</div>
<router-view/>
</div>
</template>
<script>
export default {
name: 'app'
}
</script>
<style>
...
</style>
以上就完成了我们的vue-router基本使用,启动服务以后进入home首页,点击router-link可以进行路由跳转,router-view组件将会替换成定义的routes的component内容。官方说法是将组件 (components) 映射到路由 (routes),然后告诉 Vue Router 在哪里渲染它们。说完用法,那我们来思考一下它的实现原理吧。
二、从用法思考原理
1.引入与挂载
从调用顺序上来看,首先是引入vue-router模块,然后使用了Vue.use(Router)进行了插件安装,翻阅vue.js官方文档:cn.vuejs.org/v2/guide/pl…
Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。
因此我们还原出以下代码,新建一个vue-router文件夹,文件夹下新建index.js,声明一个class VueRouter,关于js类的说明参考:developer.mozilla.org/zh-CN/docs/…
// index.js
import install from './install'
export default class VueRouter{}
// 实例的属性必须定义在类的方法里
// install有以下两种定义方法
// 1.类的属性可以使用static在class内进行定义
export default class VueRouter{
static install = install
}
// 2.或者定义在class外
VueRouter.install = install;
那么基础结构我们就已经完成了,在install的时候做了一些什么事情呢?同级目录下新建install.js
一般插件在安装时会做哪些事情呢?有以下几点。
- 添加全局方法或 property
- 添加全局资源
- 注入组件选项
- 添加实例方法
那么在Vue.use(Router)完成后,
- 添加全局方法或property,具体表现为添加了
$route与$router; - 添加全局资源,具体表现为添加了组件
router-view和router-link; - 注入组件选项,使用Vue.mixin混入beforeCreate处理函数,在组件初始化时提供路由相关操作。
// install.js
import View from './components/view'
import Link from './components/link'
export let _Vue;
export function install (Vue) {
// Vue是在use的时候传入,将Vue构造函数存起来,需要的地方进行引用
_Vue = Vue;
// 注入组件选项
Vue.mixin({ // 给所有组件的生命周期都增加beforeCreate方法,挂载在vue原型上,加载每个Vue组件时都会执行到
beforeCreate() {
if (this.$options.router) { // 如果有router属性说明是根实例
this._routerRoot = this; // 将根实例挂载在_routerRoot属性上
this._router = this.$options.router; // 将当前router实例挂载在_router上
this._router.init(this); // 根实例上进行路由初始化,这里的this指向的是根实例router实例
Vue.util.defineReactive(this,'_route',this._router.history.current);
} else { // 父组件渲染后会渲染子组件
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
},
});
// 添加两个属性
// 在use之后还只是单纯的增加了这两个属性,this._routerRoot值是在执行beforeCreate之后才有,也就是new Vue之后才会有真正的值
// 为什么要这么做呢,因为之后我们要使用this.$route获取current路由对象
Object.defineProperty(Vue.prototype,'$route',{
get(){
return this._routerRoot._route;
}
});
// 使用this.$router获取router的实例,也就是之前new Router()得到的实例
Object.defineProperty(Vue.prototype,'$router',{
get(){
return this._routerRoot._router;
}
})
// 增加了两个全局资源组件
Vue.component('RouterView',View);
Vue.component('RouterLink',Link);
}
2.new vueRouter解析用户配置文件
说完install完成的事情之后我们来看看vueRouter实例化的时候干了什么
- (1)建立一个匹配器进行关系匹配,提供match与addroutes方法
- (2)根据mode创建一个history类型,用来执行路径监听与匹配(这里我们用hash做演示)
- (3)注册钩子函数(勾子函数有好几种,拿出一个进行演示)
// index.js
import {install} from './install'
import createMatcher from './create-matcher'
import HashHistory from './history/hash'
export default class VueRouter{
constructor (options) {
// 根据用户传递的routes创建匹配关系,this.matcher需要两个方法
// match: match方法用来匹配规则
// addRoutes:用来动态添加路由
this.matcher = createMatcher(options.routes || []);
// 根据mode创建一个history类型,默认为hash
this.history = new HashHistory(this);
// 注册before勾子函数
this.beforeHooks = [];
}
// 在组件初始化时才调用,这里功能可以先放着
init(app){
...
}
beforeEach(fn){
this.beforeHooks.push(fn);
}
}
VueRouter.install = install;
(1)因为功能比较复杂,将createMatcher分离出来,同级目录新建create-matcher文件夹,文件夹下新建index.js
// create-matcher/index.js
import createRouteMap from './create-route-map'
export default function createMatcher(routes){
// 收集所有的路由路径,收集路径的对应渲染关系
// pathList = ['/','/about','/about/a','/about/b']
// pathMap = ['/':'/的记录','/about':'/about的记录'...]
let {pathList, pathMap} = createRouteMap(routes);
console.log(pathList, pathMap)
// 这个方法就是动态加载路由的方法
function addRoutes(routes){
createRouteMap(routes,pathList,pathMap)
}
function match(location){
...
}
return {
addRoutes,
match
}
}
在目录create-matcher文件夹下新建create-route-map.js
// create-matcher/create-route-map.js
export default function createRouteMap(routes,oldPathList,oldPathMap){
// 当第一次加载时没有pathList 和 pathMap
let pathList = oldPathList || [];
let pathMap = oldPathMap || Object.create(null);
routes.forEach(route => {
addRouteRecord(route,pathList,pathMap);
});
return {
pathList,
pathMap
}
}
// pathList = ['/','/about','/about/a','/about/b']
// pathMap = ['/':'/的记录','/about':'/about的记录'...]
function addRouteRecord(route,pathList,pathMap,parent){
let path = parent?`${parent.path}/${route.path}`:route.path;
let record = {
path,
component:route.component,
parent
}
if(!pathMap[path]){
pathList.push(path);
pathMap[path] = record;
}
// 处理子路由
if(route.children){
route.children.forEach(r=>{
addRouteRecord(r,pathList,pathMap,route);
})
}
}
(2)执行路径监听与匹配,同级目录新建history文件夹,文件夹下新建base.js,因为模式有很多种,history,hash,抽象,所以将基本方法放在基础History类里,那我们思考一下哪些是基础的功能,哪些是模式特有的功能。基础功能有跳转,更新,监听;特有的有路径的监听,hash模式还会在项目启动后将/路径变更为/#/
// history/base.js
import {runQueue} from '../util/async'
export default class History{
constructor(router){
this.router = router;
this.current = createRoute(null,{path:'/'});
this.cb = null;
}
// 跳转功能
transitionTo(location,onComplete){
let route = this.router.match(location);
if(location === route.path && route.matched.length === this.current.matched.length){
return
}
// 预留勾子函数执行逻辑,可以先只看this.updateRoute(route);onComplete && onComplete();
let queue = [].concat(this.router.beforeHooks);
const iterator = (hook,next)=>{
hook(route,this.current,()=>{
next();
})
}
runQueue(queue,iterator,()=>{
this.updateRoute(route);
onComplete && onComplete();
})
}
updateRoute(route){
this.current = route;
this.cb && this.cb(route);
}
listen(cb){
this.cb = cb;
}
}
export function createRoute(record,location){
let res = [];
if(record){
while(record){
res.unshift(record);
record = record.parent
}
}
return {
...location,
matched: res
}
}
新建hash.js,继承Histoty
// history/hash.js
import History from "./base";
export default class HashHistory extends History{
constructor(router){
super(router);
ensureSlash();
}
getCurrentLocation(){
return getHash();
}
setupListener(){
window.addEventListener('hashchange',()=>{
this.transitionTo(getHash());
})
}
push(location){
this.transitionTo(location)
}
}
function ensureSlash(){
if(window.location.hash){
return
}
window.location.hash = '/'
}
function getHash(){
return window.location.hash.slice(1);
}
(3)注册钩子函数
// index.js
export default class VueRouter{
constructor (options) {
...
this.beforeHooks = [];
}
...
init(app){
...
}
beforeEach(fn){
this.beforeHooks.push(fn);
}
...
}
VueRouter.install = install;
新建一个util方法,用来执行勾子函数
// util/async.js
export function runQueue (queue, iterator, cb) {
function step (index) {
if(index >= queue.length){
cb();
} else {
let hook = queue[index];
iterator(hook,()=>{
step(index+1)
})
}
}
step(0)
}
将History中transitionTo方法中的runQueue结合起来看会更清楚。
3.提供init方法,监听变化,跳转路由
// index.js
init(app){
const history = this.history;
// 设置路径变化监听
const setupHashListener = ()=>{
history.setupListener();
}
// 注册路由变化函数
history.listen((route)=>{
app._route = route
})
// 跳转路由
history.transitionTo(
history.getCurrentLocation(),
setupHashListener
)
}
4.然后提供别的push等等方法进行路由操作
// index.js
push(location){
this.history.push(location)
}
5.实现router-view组件
新建components文件夹
// components/view.js
export default {
functional: true,
render(h,{parent,data}){ //动态渲染
let route = parent.$route;
let depth = 0;
data.routerView = true;
while(parent){
if(parent.$vnode && parent.$vnode.data.routerView){
depth++;
}
parent = parent.$parent;
}
let record = route.matched[depth];
console.log('record:',record);
if(!record){
return h();
}
return h(record.component,data);
}
}
实现router-link组件
// components/link.js
export default {
props:{
to:{
type:String,
required:true
},
tag:{
type:String
}
},
render(){
let tag = this.tag || 'a';
let handler = ()=>{
this.$router.push(this.to);
}
return <tag onClick={handler}>{this.$slots.default}</tag>
}
}