Vue-Router核心原理实现(文末附手写版源码)

VueRouter的核心原理

一、VueRouter的核心组成部分

  • 主要实现以下几部分:
    • mode
    • this.$router/this.$route
    • router-link/router-view
    • Vue.use注册插件

1、mode

在vueRouter中,主要分为两种模式,一种是hash,一种是history。

  • hash模式是通过监听hashchange事件,然后根据hash值去加载对应的内容的。
  • history模式是通过history.pushState来添加浏览器历史记录,然后通过监听popState事件,也就是监听历史记录的改变,来加载相应的内容。
    • 当然,这种模式会有问题,可能我刷新了下浏览器,这个路由是不存在的,就会发生404的情况。
    • 举例:如下代码,点击关于两个字,内容显示/about,地址栏路径变成了/about,这时候去刷新页面,发现找不到/about路径对应的内容,就出现了404,Not Found
	<a onclick="go('/about')">关于a>
	<div id="html">
	div>
	function go(pathname){
		// 通过history模式,页面不会刷新,只会往历史记录中增加历史记录
		history.pushState({}, null, pathname);//第一个参数是传入的数据。第二个是名字,无实际意义。第三个是路径
		html.innerHTML = pathname;
	}
	// 监听popstate事件,也就是监听历史记录改变,比如前进或后退,就会触发该事件
	window.addEventListener('popstate', () => {
		html.innerHTML = location.pathname;
	})

2、this.$router/this.$route

  • this.$router其实就是vueRouter这个实例,也就是我们挂载在根实例options上的router,我们常常会去使用this.$router上的一些方法。
	new Vue({
		el:'#app',
		router,
		render:(h) => h(App)
	})
  • this.$route其实保存了一些属性,比如当前路径current等。
  • 当然,除了这些,我们也会遇到一丁点问题,比如如何让所有组件都能访问到根组件上挂在的router实例?——Vue.mixin+beforeCreate+递归(类似)来解决。

3、router-link/router-view

我们在vue中,经常用标签的模式去使用router-link/router-view,可见,router-link/router-view其实是两个组件。

  • router-link其实是一个a标签,当然,也可以通过tag属性传入的方式自定义标签。另外,还有to属性,代表路由跳转的去向。
  • router-view,渲染当前路径所对应的组件内容。那么如何渲染,只要找到路径对应的组件,然后render函数来渲染即可。当然,在我们真正写原理的时候,会发现,除了这些,还有bug让我们去解决——先注册vue组件,再去加载页面,执行router-view的render函数时,根本获取不到当前路径,怎么办?深度劫持帮大忙。

4、Vue.use注册插件

  • 当我们在使用VueRouter的时候,常常是通过Vue.use()方法来注册插件,其实内部是调用了VueRouter上的install方法。

二、源码实现

  • 直接上源码,主要看注释
class HistoryRoute{
    constructor(){
        this.current = null;
    }
}


class VueRouter{
    constructor(options){
        this.mode = options.mode || 'hash';
        this.routes = options.routes || [];
        // 你传递的路由表是一个数组,改造成一个对象:key是路径,value是路径对应的组件{'/home':Home,'/about':About}
        this.routesMap = this.createMap(this.routes);
        // 路由中需要存放当前的路径,当前路径变化的时候,应该渲染对应的组建
        // this.history = {current:null};// 默认是null  当然,我们最好用一个类来管理当前路径,这样方便拓展
        this.history = new HistoryRoute();
        this.init(); // 开始初始化操作
    }
    init(){
        // 判断是什么模式,如果是hash应该怎么样,如果是history应该怎么样
        // 如果是hash
        if(this.mode === 'hash'){
            // 先判断用户打开时有没有hash,没有就跳转到#/这样的路径
            location.hash ? '' : location.hash = '/';
            // 读取当前的路径,比如是/home,存到this.history中
            //刚加载页面的时候,把路径放到this.history中
            window.addEventListener('load', () => {
                this.history.current = location.hash.slice(1);
            });
            //当路由改变的时候,也要将路径存放到this.history中
            window.addEventListener('hashchange', () => {
                this.history.current = location.hash.slice(1);
            })

        }else{// 如果是history
            // 先判断用户打开的时候有没有路径,没有的恶化就跳转到#/这样的路径
            location.pathname ? '' : location.path = '/';
            // 页面一加载就把路径放到this.histroy中,这个跟hash是一样的,唯一区别的是用pathname,不是hash而已
            window.addEventListener('load', () => {
                this.history.current = location.pathname;
            });
            //当浏览器前进后退的时候,要将对应的路径放到this.history中
            window.addEventListener('popstate', () => {
                this.history.current = location.pathname;
            })
        }
        
    }
    createMap(routes){
        return routes.reduce((prev, next, index, arr) => {
            prev[next.path] = next.component;
            return prev;
        },{})
    }
}

// 使用Vue.use,会自动调用插件这个类的install方法
VueRouter.install = function(Vue, options){
    console.log(Vue, 'install');
    // 每个组件都有this.$router / this.$route
    // 如何使得每个组建都有this.$router/this.$route,那就要用到Vue.minxin
    Vue.mixin({
        beforeCreate(){
            // 获取组件的属性名称

            if(this.$options && this.$options.router){// 说明这是根组件
                this._root = this;//把当前实例挂在在_root上
                this._router = this.$options.router;// 把router实例挂在在_router上
                //observer方法
				//如果history中的current属性变化,也会刷新视图
				//this.xxx = this._router.histoury
				Vue.util.defineReactive(this,'xxx',this._router.history)//深度劫持,去看MVVM的原理
            }else{
                // vue组件的渲染顺序 父->子->孙
                this._root = this.$parent._root;
            }


            Object.defineProperty(this, '$router', {
                get(){
                    return this._root._router;
                }
            });
            Object.defineProperty(this, '$route', {
                get(){
                    return {
                        //当前路由所在的值
                        current:this._root._router.history.current
                    };
                }
            });
        }
    });
    Vue.component('router-link',{
        props:{
			to:String,
			tag:String
		},
		methods:{
			handleClick(){
				let mode = this._self._root._router.mode;
				if(mode === 'hash'){
					location.hash = this.to;
				}else{
					location.pathname = this.to;
				}
			}
		},
		render(h){//这个h是createElement
			//这里的this是什么?在render方法中,this是Proxy,如果你要拿到Vue实例(组件),那么就要用this._self

			//要先取到模式,然后根据模式来对应返回
			let mode = this._self._root._router.mode;
			let tag = this.tag ? this.tag : 'a';
			return <tag on-click={this.handleClick} href={mode === 'hash' ? `#${this.to}` : this.to}>{this.$slots.default}</tag>;
		}
    })
    Vue.component('router-view',{// 根据当前的状态 current 路由表{'/about':About}
        render(h){
            //这里的this是什么?在render方法中,this是Proxy,如果你要拿到Vue实例(根组件),那么就要用this._self
            //如何将current 做成动态的,current变化,对应的组件也应该跟着变化
			//vue实现双向绑定 ,主要靠Object.defineProperty的get和set
            let current = this._self._root._router.history.current;
            let routeMap = this._self._root._router.routesMap;
			return h(routeMap[current]);
        }
    })

}
export default VueRouter;

你可能感兴趣的:(vue,前端)