目 录CONTENT

文章目录

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

俊阳IT知识库
2023-02-17 / 0 评论 / 0 点赞 / 569 阅读 / 2,604 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2023-02-17,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。
广告

文章已同步至掘金:https://juejin.cn/post/6844903942128664584
欢迎访问😃,有任何问题都可留言评论哦~

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

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

路由模式

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

  • Hash 模式
  • History 模式

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

Hash模式

hash模式的特性:

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

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

Hash实现原理

<!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文件。

整体结构如图所示:

vue-router-principle-1

在上图中,我们不使用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>

源码

如果只看代码,没有自己实践一番,肯定是理解不了的

源码地址


^_<

0

评论区