Vue-route底层实现原理

  • 先抛出一堆问题:
  1. 如何改变 URL 却不引起页面刷新?
  2. 如何检测 URL 变化了?
  3. inistall方法做了什么?
  4. 如何$router与$route绑定到所有组件上?
  5. 如何确保插件只安装一次?

  • 1、引入vue-router,并使用Vue.use(VueRouter)
  • 2、定义路由数组,并将数组传入VueRouter实例,并将实例暴露出去
  • 3、将VueRouter实例注册到根Vue实例上
//简洁版:仅列出关键信息
import VueRouter from 'vue-router'    //会调用vue-router上的install方法初始化vuerouter这个类
Vue.use(VueRouter) // 第一步 

const routes = [
    {
        path: '/home',
        component: home,
    },   .....

]

export default new VueRouter({
    routes // 第二步    new出router类的实例
})

new Vue({
  router,  // 第三步      注册到根实例上
  render: h => h(App)
}).$mount('#app')


确保插件不会重复安装:

Vue.use = function(plugin){
	const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
	if(installedPlugins.indexOf(plugin)>-1){
		return this;
	}
	<!-- 其他参数 -->
	const args = toArray(arguments,1);
	args.unshift(this);
	if(typeof plugin.install === 'function'){
		plugin.install.apply(plugin,args);
	}else if(typeof plugin === 'function'){
		plugin.apply(null,plugin,args);
	}
	installedPlugins.push(plugin);
	return this;
}

vue.js上新增use方法,判断插件是不是已经注册过,维护一个数组,没注册过的则插入,注册过的return出去。

install方法

Vue.use(XXX),就是执行XXX上的install方法,并将Vue作为参数传入install方法。也就是Vue.use(VueRouter) === VueRouter.install()

调用install方法时,会将Vue作为参数传入

install一般是给每个vue实例加东西,在这里就是给每个组件添加$route$router

$route$router有什么区别?

$router是VueRouter的实例对象,$route是当前路由对象,也就是说$route$router的一个属性 注意每个组件添加的$route是是同一个,$router也是同一个,所有组件共享的。

// src/my-router.js
....省略了部分代码,只列出关键部分

let _Vue
VueRouter.install = (Vue) => {
    _Vue = Vue
    // 使用Vue.mixin混入每一个组件
    Vue.mixin({
        // 在每一个组件的beforeCreate生命周期去执行
        beforeCreate() {
            if (this.$options.router) { // 如果是根组件,即router实例存在,vuerouter实例在第三步就已挂载到根组件上
                // this 是 根组件本身
                this._routerRoot = this

                // this.$options.router就是挂在根组件上的VueRouter实例
                this.$router = this.$options.router

                // 执行VueRouter实例上的init方法,初始化
                this.$router.init(this)
            } else {
                // 非根组件,也要把父组件的_routerRoot保存到自身身上,相当于把根组件传递下去给子组件
                this._routerRoot = this.$parent && this.$parent._routerRoot
                // 子组件也要挂上$router
                this.$router = this._routerRoot.$router
            }
        }
    })
  Object.defineProperty(Vue.prototype, '$router', {//将$router挂载到组件实例上。
    get () { return this._routerRoot._router }
  })

  Vue.component('RouterView', View)    //注册全局组件
  Vue.component('RouterLink', Link)

}

一种代理的思想,我们获取组件的$router,其实返回的是根组件的_root._router

个人总结:

1、use方法中,用installed数组维护,标识只执行一次,不会重复安装,

2、通过递归将所有的vue组件混入beforeCreate和destroyed钩子函数,因为在钩子函数中this指向其本身,在钩子中对每个子组件绑定_route即VueRouter实例. 将VueRouter实例对象注入到所有的vue实例上,

在原型对象上将$router和router实例对象绑定

3、后面通过Vue.component定义全局的<router-link>和<route-view>组件,link相当于a标签,可以改变hash地址

routes转map结构

//router/index.js
import Vue from 'vue'
import VueRouter from './myVueRouter'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
  const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
];
const router = new VueRouter({
  mode:"history",
  routes
})
export default router



//myVueRouter.js
let Vue = null;
class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] //你传递的这个路由是一个数组表
        this.routesMap = this.createMap(this.routes)
        console.log(this.routesMap);
    }
    createMap(routes){
        return routes.reduce((pre,current)=>{
            pre[current.path] = current.component
            return pre;
        },{})
    }
}

router实例添加history.current属性,属性可获取到当前路由路径

//myVueRouter.js
let Vue = null;
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值,没有的话跳转到#/
            location.hash? '':location.hash = "/";
            window.addEventListener("load",()=>{
                this.history.current = location.hash.slice(1)
            })
            window.addEventListener("hashchange",()=>{
                this.history.current = location.hash.slice(1)
            })
        } else{
            location.pathname? '':location.pathname = "/";
            window.addEventListener('load',()=>{
                this.history.current = location.pathname
            })
            window.addEventListener("popstate",()=>{
                this.history.current = location.pathname
            })
        }
    }

    createMap(routes){
        return routes.reduce((pre,current)=>{
            pre[current.path] = current.component
            return pre;
        },{})
    }

}







Vue.mixin({
    beforeCreate(){
        if (this.$options && this.$options.router){ // 如果是根组件
            this._root = this; //把当前实例挂载到_root上
            this._router = this.$options.router;
            新增代码
            Vue.util.defineReactive(this,"xxx",this._router.history)
        }else { //如果是子组件
            this._root= this.$parent && this.$parent._root
        }
        Object.defineProperty(this,'$router',{
            get(){
                return this._root._router
            }
        });
        Object.defineProperty(this,'$route',{
            get(){
                return this._root._router.history.current
            }
        })
    }
})

利用Vue提供的API:defineReactive,使得this._router.history对象得到监听。

第一次渲染router-view这个组件的时候,会获取到this._router.history这个对象,从而就会被监听到获取this._router.history。就会把router-view组件的依赖wacther收集到this._router.history对应的收集器dep中,因此this._router.history每次改变的时候。this._router.history对应的收集器dep就会通知router-view的组件依赖的wacther执行update(),从而使得router-view重新渲染(其实这就是vue响应式的内部原理

下面以hash模式为例子:

hash模式的原理就是,通过window.addeventlistener监听hashchange这个事件(history的话是监听popstate这个事件),可以监听浏览器url中hash值的变化,通过window.location.hash可以获取到组件名,再通过map结构找出对应的组件。

class HashHistory {
    constructor(router) {

        // 将传进来的VueRouter实例保存
        this.router = router

        // 如果url没有 # ,自动填充 /#/ 
        ensureSlash()
        
        // 监听hash变化
        this.setupHashLister()
    }
    // 监听hash的变化
    setupHashLister() {
        window.addEventListener('hashchange', () => {
            // 传入当前url的hash,并触发跳转
            this.transitionTo(window.location.hash.slice(1))
        })
    }

    // 跳转路由时触发的函数
    transitionTo(location) {
        console.log(location) // 每次hash变化都会触发,可以自己在浏览器修改试试
        // 比如 http://localhost:8080/#/home/child1 最新hash就是 /home/child1
       // 找出所有对应组件,router是VueRouter实例,createMathcer在其身上
      let route = this.router.createMathcer(location)
    }
}

// 如果浏览器url上没有#,则自动补充/#/
function ensureSlash() {
    if (window.location.hash) {
        return
    }
    window.location.hash = '/'
}

// 这个先不讲,后面会用到
function createRoute(record, location) {
    const res = []
    if (record) {
        while (record) {
            res.unshift(record)
            record = record.parent
        }
    }
    return {
        ...location,
        matched: res
    }
}
export default HashHistory

注意:手动刷新的话不会出发hashchange,所以在初始化的时候执行一次原地跳转


// src/my-router.js

class VueRouter {

    // ...原先代码
    
    init(app) {
        // 初始化时执行一次,保证刷新能渲染
        this.history.transitionTo(window.location.hash.slice(1))
    }

    // ...原先代码
}

贴一下完整代码:

/src/my-router.js

//myVueRouter.js
let Vue = null;
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值,没有的话跳转到#/
            location.hash? '':location.hash = "/";
            window.addEventListener("load",()=>{
                this.history.current = location.hash.slice(1)
            })
            window.addEventListener("hashchange",()=>{
                this.history.current = location.hash.slice(1)
            })
        } else{
            location.pathname? '':location.pathname = "/";
            window.addEventListener('load',()=>{
                this.history.current = location.pathname
            })
            window.addEventListener("popstate",()=>{
                this.history.current = location.pathname
            })
        }
    }

    createMap(routes){
        return routes.reduce((pre,current)=>{
            pre[current.path] = current.component
            return pre;
        },{})
    }

}
VueRouter.install = function (v) {
    Vue = v;
    Vue.mixin({
        beforeCreate(){
            if (this.$options && this.$options.router){ // 如果是根组件
                this._root = this; //把当前实例挂载到_root上
                this._router = this.$options.router;
                Vue.util.defineReactive(this,"xxx",this._router.history)
            }else { //如果是子组件
                this._root= this.$parent && this.$parent._root
            }
            Object.defineProperty(this,'$router',{
                get(){
                    return this._root._router
                }
            });
            Object.defineProperty(this,'$route',{
                get(){
                    return this._root._router.history.current
                }
            })
        }
    })
    Vue.component('router-link',{
        props:{
            to:String
        },
        render(h){
            let mode = this._self._root._router.mode;
            let to = mode === "hash"?"#"+this.to:this.to
            return h('a',{attrs:{href:to}},this.$slots.default)
        }
    })
    Vue.component('router-view',{
        render(h){
            let current = this._self._root._router.history.current
            let routeMap = this._self._root._router.routesMap;
            return h(routeMap[current])
        }
    })
};

export default VueRouter

src/view.js link.js

Vue.component('router-view',{
    render(h){
        let current = this._self._root._router.history.current
        let routeMap = this._self._root._router.routesMap;
        return h(routeMap[current])
    }
})


Vue.component('router-link',{
    props:{
        to:String
    },
    render(h){
        let mode = this._self._root._router.mode;
        let to = mode === "hash"?"#"+this.to:this.to
        return h('a',{attrs:{href:to}},this.$slots.default)
    }
})

render函数里的this指向的是一个Proxy代理对象,代理Vue组件,而我们前面讲到每个组件都有一个_root属性指向根组件,根组件上有_router这个路由实例。 所以我们可以从router实例上获得路由表,也可以获得当前路径。 然后再把获得的组件放到h()里进行渲染。

参考https://juejin.cn/post/6854573222231605256

阅读剩余
THE END