vue-router原理分析

一、剖析VueRouter本质

先抛出个问题,Vue项目中是怎么引入VueRouter。

  1. 安装VueRouter,再通过import VueRouter from 'vue-router'引入
  2.  const router = new VueRouter({...}),再把router作为参数的一个属性值,new Vue({router})
  3. 通过Vue.use(VueRouter) 使得每个组件都可以拥有store实例

从这个引入过程我们可以发现什么?

  1. 我们是通过new VueRouter({...})获得一个router实例,也就是说,我们引入的VueRouter其实是一个类。

所以我们可以初步假设

class VueRouter{
    
}
  1. 我们还使用了Vue.use(),而Vue.use的一个原则就是执行对象的install这个方法

所以,我们可以再一步 假设VueRouter有有install这个方法。

class VueRouter{

}
VueRouter.install = function () {
    
}

到这里,你能大概地将VueRouter写出来吗?

很简单,就是将上面的VueRouter导出,如下就是myVueRouter.js

//myVueRouter.js
class VueRouter{

}
VueRouter.install = function () {
    
}

export default VueRouter

二、分析Vue.use

Vue.use(plugin);

(1)参数

{ Object | Function } plugin

(2)用法

安装Vue.js插件。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。调用install方法时,会将Vue作为参数传入。install方法被同一个插件多次调用时,插件也只会被安装一次。

关于如何上开发Vue插件,请看这篇文章,非常简单,不用两分钟就看完:如何开发 Vue 插件?

(3)作用

注册插件,此时只需要调用install方法并将Vue作为参数传入即可。但在细节上有两部分逻辑要处理:

1、插件的类型,可以是install方法,也可以是一个包含install方法的对象。

2、插件只能被安装一次,保证插件列表中不能有重复的插件。

(4)实现

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;
}

1、在Vue.js上新增了use方法,并接收一个参数plugin。

2、首先判断插件是不是已经别注册过,如果被注册过,则直接终止方法执行,此时只需要使用indexOf方法即可。

3、toArray方法我们在就是将类数组转成真正的数组。使用toArray方法得到arguments。除了第一个参数之外,剩余的所有参数将得到的列表赋值给args,然后将Vue添加到args列表的最前面。这样做的目的是保证install方法被执行时第一个参数是Vue,其余参数是注册插件时传入的参数。

4、由于plugin参数支持对象和函数类型,所以通过判断plugin.install和plugin哪个是函数,即可知用户使用哪种方式祖册的插件,然后执行用户编写的插件并将args作为参数传入。

5、最后,将插件添加到installedPlugins中,保证相同的插件不会反复被注册。(~~让我想起了曾经面试官问我为什么插件不会被重新加载!!!哭唧唧,现在总算明白了)

第三点讲到,我们把Vue作为install的第一个参数,所以我们可以把Vue保存起来

//myVueRouter.js
let Vue = null;
class VueRouter{

}
VueRouter.install = function (v) {
    Vue = v;
};

export default VueRouter

然后再通过传进来的Vue创建两个组件router-link和router-view

//myVueRouter.js
let Vue = null;
class VueRouter{

}
VueRouter.install = function (v) {
    Vue = v;
    console.log(v);

    //新增代码
    Vue.component('router-link',{
        render(h){
            return h('a',{},'首页')
        }
    })
    Vue.component('router-view',{
        render(h){
            return h('h1',{},'首页视图')
        }
    })
};

export default VueRouter

我们执行下项目,如果没报错,说明我们的假设没毛病。

天啊,没报错。没毛病!

三、完善install方法

install 一般是给每个vue实例添加东西的

在这里就是给每个组件添加$route$router

$route$router有什么区别?

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

这是什么意思呢???

来看mian.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  render: function (h) { return h(App) }
}).$mount('#app')

我们可以发现这里只是将router ,也就是./router导出的store实例,作为Vue 参数的一部分。

但是这里就是有一个问题咯,这里的Vue 是根组件啊。也就是说目前只有根组件有这个router值,而其他组件是还没有的,所以我们需要让其他组件也拥有这个router。

因此,install方法我们可以这样完善

//myVueRouter.js
let Vue = null;
class VueRouter{

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

    Vue.component('router-link',{
        render(h){
            return h('a',{},'首页')
        }
    })
    Vue.component('router-view',{
        render(h){
            return h('h1',{},'首页视图')
        }
    })
};

export default VueRouter

解释下代码:

  1. 参数Vue,我们在第四小节分析Vue.use的时候,再执行install的时候,将Vue作为参数传进去。
  2. mixin的作用是将mixin的内容混合到Vue的初始参数options中。相信使用vue的同学应该使用过mixin了。
  3. 为什么是beforeCreate而不是created呢?因为如果是在created操作的话,$options已经初始化好了。
  4. 如果判断当前组件是根组件的话,就将我们传入的router和_root挂在到根组件实例上。
  5. 如果判断当前组件是子组件的话,就将我们_root根组件挂载到子组件。注意是引用的复制,因此每个组件都拥有了同一个_root根组件挂载在它身上。

这里有个问题,为什么判断当前组件是子组件,就可以直接从父组件拿到_root根组件呢?这让我想起了曾经一个面试官问我的问题:父组件和子组件的执行顺序

A:父beforeCreate-> 父created -> 父beforeMounte -> 子beforeCreate ->子create ->子beforeMount ->子 mounted -> 父mounted

可以得到,在执行子组件的beforeCreate的时候,父组件已经执行完beforeCreate了,那理所当然父组件已经有_root了。

然后我们通过

Object.defineProperty(this,'$router',{
  get(){
      return this._root._router
  }
})

$router挂载到组件实例上。

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

到这里还install还没写完,可能你也发现了,$route还没实现,现在还实现不了,没有完善VueRouter的话,没办法获得当前路径

四、完善VueRouter类

我们先看看我们new VueRouter类时传进了什么东东

//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

可见,传入了一个为数组的路由表routes,还有一个代表 当前是什么模式的mode。因此我们可以先这样实现VueRouter

class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] //你传递的这个路由是一个数组表
    }
}

先接收了这两个参数。

但是我们直接处理routes是十分不方便的,所以我们先要转换成key:value的格式

//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;
        },{})
    }
}

通过createMap我们将

const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }

转换成

路由中需要存放当前的路径,来表示当前的路径状态 为了方便管理,可以用一个对象来表示

//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();
        
    }

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

}

但是我们现在发现这个current也就是 当前路径还是null,所以我们需要进行初始化。

初始化的时候判断是是hash模式还是 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;
        },{})
    }

}

监听事件跟上面原生js实现的时候一致。

五、完善$route

前面那我们讲到,要先实现VueRouter的history.current的时候,才能获得当前的路径,而现在已经实现了,那么就可以着手实现$route了。

很简单,跟实现$router一样

VueRouter.install = function (v) {
    Vue = v;
    Vue.mixin({
        beforeCreate(){
            if (this.$options && this.$options.router){ // 如果是根组件
                this._root = this; //把当前实例挂载到_root上
                this._router = this.$options.router;
            }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',{
        render(h){
            return h('a',{},'首页')
        }
    })
    Vue.component('router-view',{
        render(h){
            return h('h1',{},'首页视图')
        }
    })
};

六、完善router-view组件

现在我们已经保存了当前路径,也就是说现在我们可以获得当前路径,然后再根据当前路径从路由表中获取对应的组件进行渲染

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])
    }
})

解释一下:

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

现在已经实现了router-view组件的渲染,但是有一个问题,就是你改变路径,视图是没有重新渲染的,所以需要将_router.history进行响应式化。

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响应式的内部原理

好了,现在我们来测试一下,通过改变url上的值,能不能触发router-view的重新渲染

path改成home

可见成功实现了当前路径的监听。。

七、完善router-link组件

我们先看下router-link是怎么使用的。

<router-link to="/home">Home</router-link> 
<router-link to="/about">About</router-link>

也就是说父组件间to这个路径传进去,子组件接收就好 因此我们可以这样实现

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)
    }
})

我们把router-link渲染成a标签,当然这时最简单的做法。 通过点击a标签就可以实现url上路径的切换。从而实现视图的重新渲染

ok,到这里完成此次的项目了。

看下VueRouter的完整代码吧

//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

版权声明:
作者:wuhou123
链接:https://wuhou.fun/53.html
来源:前端网
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>
文章目录
关闭
目 录