vue-router原理解析

·  阅读 199
vue-router原理解析

一、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

一般插件在安装时会做哪些事情呢?有以下几点。

  1. 添加全局方法或 property
  2. 添加全局资源
  3. 注入组件选项
  4. 添加实例方法

那么在Vue.use(Router)完成后,

  1. 添加全局方法或property,具体表现为添加了$route$router;
  2. 添加全局资源,具体表现为添加了组件router-viewrouter-link;
  3. 注入组件选项,使用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>
  }
}
复制代码
分类:
前端