理解vue-router中(router-link router-view $router $route)实现原理

关于vue-router实现原理的问题是很是重要的,而且常常会在面试中提问
本章简单讲解一下 vue-routerrouter-linkrouter-view$router$route 的实现原理html

里面的注释可能会有点多,可是仍是本着走一步测一步的原则,慢慢看,慢慢来前端

路由模式

说到前端路由,不得不说路由的两种模式:vue

  • Hash 模式
  • History 模式

两种路由的具体区别和使用,请参考个人文章:《Vue的mode中 hash 与 history 的区别》git

Hash模式

hash模式的特性:面试

  • URLhash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送。
  • hash 值的改变,都会在浏览器的访问历史中增长一个记录。所以咱们能经过浏览器的回退、前进按钮控制 hash 的切换。
  • 咱们可使用 hashchange 事件来监听 hash 的变化。

咱们能够经过两种方式触发 hash 变化,一种是经过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 就会发生改变,也就会触发 hashchange 事件
还有一种方式就是直接使用 JS 来对 location.hash 进行赋值,从而改变 URL,触发 hashchange 事件vue-router

Hash实现原理api

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>hash原理</title>
</head>
<body>
    <!-- hash原理:靠的是location.hash  load事件 hashchange事件 -->
    <a href="#/home">首页</a>
    <a href="#/about">关于</a>
    <div id="box"></div>
</body>
<script>
    // 当Html文档加载完毕后,会触发load事件 
    window.addEventListener("load",()=>{
        // 在浏览器中有一个api叫location 
        // console.log(location.hash.slice(1))
        // location是浏览器自带   也就是说是Hash路由的原理是location.hash 
        box.innerHTML = location.hash.slice(1)
    })
    // hashchange  当hash改变时触发这个事件 
    window.addEventListener("hashchange",()=>{
        box.innerHTML = location.hash.slice(1)
    })
</script>
</html>
复制代码

History模式

history模式的特性:数组

  • pushStaterepalceState 的标题(title):通常浏览器会忽略,最好传入 null
  • 咱们可使用 popstate 事件来监听 URL 的变化;
  • history.pushState()history.replaceState() 不会触发 popstate 事件,这时咱们须要手动触发页面渲染;

History实现原理浏览器

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>history原理</title>
</head>
<body>
    <!-- history原理:靠的是h5中的api:history.pushState popstate 等 -->
    <a onclick="go('/home')">首页</a>
    <a onclick="go('/about')">关于</a>
    <div id="box"></div>
</body>
<script>
    function go(pathname){
        //pushState方法,里面有三个参数
        history.pushState({},null,pathname)
        box.innerHTML = pathname
    }
    //popstate方法
    window.addEventListener("popstate",()=>{
        // console.log(".....")
        console.log(location.pathname)
        go(location.pathname)
    })
</script>
</html>
复制代码

原理实现

为了使代码更加的优雅,咱们在 src 目录下新建了一个文件夹 router ,用来代替外面的 router.js 文件,而且在 router 文件夹下新建了index.jsroutes.jsvue-router.js文件。bash

总体结构如图所示:

图片加载失败!

在上图中,咱们不使用Vue框架中的router,由于人家的router里已经给咱们封装好了router-viewrouter-link$router$route等一系列方法,因此咱们使用本身写的代码。
其中routes.js中定义了咱们须要用到的路由,而vue-router.js文件中则至关于vue-router的源码。

组件Home.vueAbout.vue很简单,就打印一个内容,以下:

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>
复制代码

而后在咱们建立的router文件夹下的routes.js文件中引入上面的两个路由:

import Home from '../views/Home.vue'
import About from '../views/About.vue'
export default [
    {path:'/home',component:Home},
    {path:'/about',component:About},
]
复制代码

而后在咱们的index.js文件中引入routes.js文件,而且把咱们将要写Vue源码的文件vue-router.js文件也引入到里面

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

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

export default new VueRouter({
    mode:'history',
    routes
})
复制代码

接下来开始在vue-router中写源码

首先咱们定义一个VueRouter类,并在里面建立一个constructor,并把这个类导出去:

//VueRouter就是一个类
class VueRouter{
    constructor(options){   // constructor指向建立这个对象的函数  下面定义的这些变量都将挂在VueRouter实例上
		
	//打印options以下
	console.log(options);   //{mode: "hash", routes: Array(2)}
	
        // 获得路由模式
        this.mode=options.mode || "hash"
		
        // 实例化HistoryRoute挂在VueRouter组件上,this指向VueRouter这个类
        this.history=new HistoryRoute()
		
        // 获得路由规则
        this.routes=options.routes || []
		
        // 转换后的数据形式  {'/home':Home}
        this.routesMap=this.createMap(this.routes)
		
        this.init()	//刷新页面就会调用init方法
    }
}

// 把类导出去
export default VueRouter
复制代码

咱们须要把路由格式转换成咱们须要的那种格式,就如上面代码所示,获得路由规则后,而后把他传入createMap中,开始转换格式并返回赋值给routesMap

// 在VueRouter中定义一个方法,把数组形式的路由转变成对象形式,即[{path:'/home',component:'Home'}]转变成{'/home':Home},这样写,下面根据路径渲染组件时较为简洁,方便

createMap(routes){
	// array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
	// total--初始值,或者计算结束后返回的值,当前值,当前元素的索引(可选),当前元素所属数组(可选),initialValue可选,传递给函数的初值

	return routes.reduce((memo,current)=>{
		// console.log(memo)
		memo[current.path]=current.component
		return memo
	},{})
}
复制代码

接下来在VueRouter中定义init()方法,判断当前使用的是什么路由,而且把获得的路径保存到current中 :

先在VueRouter类的外面(和VueRouter同级)写一个HistoryRoute类用来保存获得的路径:

class HistoryRoute{
    constructor(){
        this.current = null
    }
}
复制代码

接下来调用init()方法把得到到的路径保存到current里,以下:

init(){
	// hash的原理是location,location.hash表示访问的路径,还有两个事件,load,hashchange
	//使用的是hash路由
	if(this.mode==="hash"){
		// location是浏览器内部的一个api
		// console.log(location.hash)  //  #/ #/home #/about

		location.hash ? "" : location.hash="/"

		// 当页面加载完毕后触发load事件
		window.addEventListener("load",()=>{
			// console.log(location.hash.slice(1))  //   /  /home  /about
			// current保存了响应的路径
			this.history.current = location.hash.slice(1)   //  去掉#是为了下面匹配{path:'/home'}时能够匹配到
			// console.log(this.history.current)   // /  /home  /about
		})
		// 点击前进后退按钮时才会hashchange事件
		window.addEventListener("hashchange",()=>{
			this.history.current=location.hash.slice(1)
			// console.log(this.history.current)
		})
	}
	// history模式,靠的是popstate,location.pathname表示访问的路径
	else
	{
		//使用的是history
		location.pathname ? "" : location.pathname = "/"
		// console.log(location.pathname)   //  /   /home    /about
		// 页面加载完毕后触发load事件
		window.addEventListener("load",()=>{
			this.history.current=location.pathname
			console.log("----",this.history.current)
		})
		// 当用户点击回退或前进按钮时才会触发popstate事件
		window.addEventListener("popstate",()=>{
			this.history.current=location.pathname
			console.log(this.history.current)
		})
	}
}
复制代码

如今在VueRouter里定义一些方法,以下:

push(){}
go(){}
back(){}
复制代码

重点来了:

VueRouter上面挂载一个install方法,并让所有的组件均可以使用$router$route

//install方法中第一个参数就是Vue构造器
VueRouter.install = function(Vue){

    // console.log(Vue);   //Vue构造器
    //当使用Vue.use(Vue-router)时,调用install方法
    //Vue.component()   //全局组件
	
    Vue.mixin({
	
        //给每一个组件混入一个beforeCreate钩子函数,当访问根组件时触发
        beforeCreate(){
            // console.log(this.$options.name);
            //获取根组件
            if(this.$options && this.$options.router){
			
                //找到根组件
                //把当前的实例挂载到_root上面
                this._root = this   //main根组件
				
                //把router实例挂载到_router上
                this._router = this.$options.router
				
                //监控路径变化,路径变化就刷新视图
                Vue.util.defineReactive(this,'xxx',this._router,history)    //这行代码能够给咱们的history设置get和set,使history变成响应式
            }
	    else
		{
		//全部组件中都是有router
                this._root = this.$parent._root;
                this._router = this.$parent._router;
            }

            // this.$options.name 获取组件的名字
            // console.log(this.$options.name)
	    //让$router在全局中可用
	    // defineProperty(obj,prop,descriptor),
            // obj:要定义的属性的对象;prop:要定义或修改的对象的属性;descriptor:要定义或修改的属性值
            Object.defineProperty(this,"$router",{	// this表示每个组件
                get(){	// 获取$router时,调用get,返回this._root._router,相似的还有一个set方法,设置$router时调用set方法
                    return this._root._router;
                }
            })
	    //让$route在全局中可用
            Object.defineProperty(this,"$route",{
                get(){	// 访问$route时,自动调用get方法
                    // console.log(this._root._router.history);
                    return{
                        current:this._root._router.history.current
                    }
                }
            })
        }
    })
	
    //让router-link在全局中可用
    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>
        }
    })
	
    //让router-view在全局中可用
    Vue.component('router-view',{
        render(h){
            let current = this._self._root._router.history.current;
            let routesMap = this._self._root._router.routesMap;
	    //current是一个变量,因此用[]包起来用
            return h(routesMap[current])
        }
    })
}
复制代码

接下来在App.vue中就可使用了:

<template>
   <div>
      <router-link to="/home">首页</router-link>
      <br>
      <router-link to="/about">关于</router-link>
      <router-view></router-view>
   </div>

</template>

<script>
export default {
   name:'app',
   mounted(){
   	  //下面两个就能够打印出相应的内容了
      // console.log(this.$router);
      // console.log(this.$route);
   }
}
</script>
复制代码

源码

若是只看代码,没有本身实践一番,确定是理解不了的

在此处附上源码--->源码地址


^_<

相关文章
相关标签/搜索