Vue之vue-router原理剖析

2,590 阅读4分钟

在Vue中,vue-router占据重要的位置,是vue的核心插件,那么它的实现原理是什么呢? 在剖析原理之前,先来了解几个概念:SAP,路由模式 SPA(single page application):单一页面应用程序,有且只有一个完整的页面,当它在加载页面时,不会加载整个页面,而只更新某个指定的容器中内容(组件)

路由模式:hash模式、history模式,abstract模式

1.hash模式

随着ajax的流行,异步数据请求交互运行在不刷新浏览器的情况下进行。而异步交互体验更高的版本就是SPA--单页应用。单页应用不仅在页面交互时不用刷新,而且在页面跳转时也是不用刷新,于是就有浏览前端路由。实现原理就是根据不同的url进行解析,来匹配不同的组件;但当url发生变化时就会造成页面的刷新。这就出现了hash,使用hash在改变url的情况下,保证页面的不刷新。 http://www.xxx.com/#/login 这种#,后面hash值发生变化,并不会导致浏览器向服务器发出请求,进而不会发生页面刷新。而且每当hash发生变化时,都会触发hashchange事件,通过这个事件可以知道hash值发生了什么变化,然后可以监听hashchange来实现更新页面部分内容大都操作

2.history模式

因在HTML5标准发布后,多了两个API,pushStatereplaceState,通过这两个API可以改变url地址且不会发送请求。同时还有popstate事件,通过这些API就能以另一种方式来实现前端路由,但原理与hash实现相同,但不会有**#**,因此当用户主动刷新页面之类操作时,还是会给服务器发送请求,为避免这种情况,所以这实现需要服务器支持,需把所有路由都重定向到根页面

3.abstract模式

abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。 根据平台差异可以看出,在 Weex 环境中只支持使用 abstract 模式。 不过,vue-router 自身会对环境做校验,如果发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。 (当然,你也可以明确指定在所有情况下都使用 abstract 模式)

vue-router实现原理

原理核心:更新视图而不重新请求页面。 vue-router实现单页面跳转,提供了三种方式:hash模式、history模式 abstract模式,根据mode参决定使用哪种方式。 接下来详细剖析其原理

在这张流程图中分析vue-router实现原理,我们将官网的vue-router挂载到Vue实例上,打印出来增加了什么:

  • options下的router对象很好理解,这个就是我们在实例化Vue的时候挂载的那个vue-router实例;
  • _route是一个响应式的路由route对象,这个对象会存储我们路由信息,它是通过Vue提供的Vue.util.defineReactive来实现响应式的,下面的get和set便是对它进行的数据劫持;
  • _router存储的就是我们从$options中拿到的vue-router对象;
  • _routerRoot指向我们的Vue根节点;
  • _routerViewCache是我们对View的缓存;
  • route和router是定义在Vue.prototype上的两个getter。前者指向_routerRoot下的_route,后者指向_routerRoot下的_router

总结起来,vue-router就是一个类,里面封装了一个mixin,定义了两个‘原型’,注册了两个组件。 在mixin中beforeCreate中定义**_routerRoot、_router、_route**,使其每个组件都有_routerRoot,定义了两个原型是指在Vue.prototype上面定义的两个getter,也就是**router、route**,注册两个组件是指要用到的router-link、router-view

基本原理:

  1. Vue.use()时将vue-router挂载到Vue上时调用install方法,在install方法里调用mixin定义_routerRoot、_router、_route,使其每个组件都有_routerRoot,_router,在Vue原型上挂载router、route。
  2. 在初始化时调用init方法,根据mode参数决定使用哪种路由模式;
    • 如果使用hash模式,则定义load页面加载方法,在页面加载时通过location.hash获取地址栏的url的hash值赋值给history对象的current,定义hashchange事件监听url的hash变化,将变化后的hash值赋值给history对象的current
    • 如果使用的是history模式,则定义load页面加载函数,通过location.pathname,获取地址栏url中对应组件的地址如:http://localhost:8080/about,取得 /about,赋值给history对象的current, 定义popstate方法,当点击前进或后退时改变了当前活动历史项(history),触发此事件,通过location.pathname将历史中的url赋值给history对象的current
  3. 在定义全局组件时,
    • router-link组件,调用render()函数时,通过mode确定使用哪种路由方式,获取传递的to赋值给a标签的href属性进行跳转,使用slot插槽传递中间的值
    • router-view组件时,将之前赋值的组件名:history.current,通过将数据转换为对象方法,传递给render()函数中的h()方法进行渲染对应组件 详细原理已剖析完毕,代码如下: 目录结构如下:

app.vue

<template>
    <div id="app">
      <router-link to='/home'>首页</router-link>
      <br>
      <router-link to="/about">关于</router-link>
      <router-view></router-view>
    </div>
</template>
<script>
export default {
  name:'app'
};
</script>

Home.vue

<template>
    <div>
        home
    </div>
</template>
<script>
export default {
    name:'home'
}
</script>

About.vue

<template>
    <div>
        about
    </div>
</template>
<script>
export default {
    name:'about'
}
</script>

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './routers';
import store from './store'

Vue.config.productionTip = false

new Vue({
  name:'main',
  router,
  store,
  render: h => h(App)
}).$mount('#app')

index.js

import Vue from 'vue';
import routes from './routes';
import VueRouter from './vue-router';

// 使用Vue.use就会调用install方法
Vue.use(VueRouter)

export default new VueRouter({
    mode:"history",//hash,history
    routes
})

routes.js

import Home from '../views/Home.vue';
import About from '../views/About.vue';
export default [
    {path:'/home',name:'home',component:Home},
    {path:'/about',name:'about',component:About}

]

vue-router.js


class HistoryRoute{
    constructor(){
        this.current=null
    }
}

class VueRouter{
    constructor(options){
        this.mode=options.mode||"hash";
        this.routes=options.routes||[];
        this.routesMap=this.createMap(this.routes)
        this.history=new HistoryRoute();
        this.init();

    }
    init(){
        if(this.mode==="hash"){
            //使用的是hash路由
            // console.log('使用的是hash模式')
            // console.log(location.hash)
            location.hash?"":location.hash="/"
            window.addEventListener("load",()=>{
                this.history.current=location.hash.slice(1);
                // console.log('load==>',this.history.current)
            })
            window.addEventListener("hashchange",()=>{
                this.history.current=location.hash.slice(1)
                // console.log('hashchange==>',this.history.current)
            })

        }else{
            //使用的history
            location.pathname?"":location.pathname='/'
            window.addEventListener("load",()=>{
                this.history.current=location.pathname;
                // console.log('load==>',this.history.current)
            })
            window.addEventListener("popstate",()=>{
                this.history.current=location.pathname
                // console.log('popstate==>',this.history.current)
            })

        }
    }
    push(){}
    go(){}
    back(){}
    // createMap将数组转换为对象结构
    createMap(routes){
        return routes.reduce((memo,current)=>{
            // memo刚开始是个空对象
            memo[current.path]=current.component
            return memo;
        },{})
    }

}

VueRouter.install=function(Vue){
    Vue.mixin({
        // 将每个组件都混入一个 beforeCreate
        beforeCreate() {
            // 获取根组件
            if(this.$options&&this.$options.router){
                // 找到根组件
                // 把当前实例挂载到_router上面
                this._router = this.$options.router
                this._root=this;
              Vue.util.defineReactive(this,"xxx",this._router,history)
            }else{
                // main---->app----->home/about  
                this._root = this.$parent._root; //所有组件都有了router
            }
            Object.defineProperty(this,"$router",{
                get(){
                    // console.log(this._root)
                    return this._root._router
                }
            })
            Object.defineProperty(this,"$route",{
                get(){
                    // console.log(this._root._router.history.current)
                    return {
                        current:this._root._router.history.current
                    }
                }
            })

        },
    }),
    Vue.component("router-link",{
        props: {
            to:String
        },
        render(h){
            let mode = this._self._root._router.mode;
            return <a href={mode==='hash'?`#${this.to}`:this.to}>{this.$slots.default}</a>
        }
    }),
    Vue.component("router-view",{
        render(h) {
            let current=this._self._root._router.history.current;
            // console.log(this)
            let routesMap=this._self._root._router.routesMap
            return h(routesMap[current])
        },
    })
}

export default VueRouter