文章已同步至掘金:https://juejin.cn/post/6844903942128664584
欢迎访问😃,有任何问题都可留言评论哦~
关于vue-router
实现原理的问题是非常重要的,并且经常会在面试中提问
本章简单讲解一下 vue-router
中 router-link
、router-view
、$router
、$route
的实现原理
里面的注释可能会有点多,但是还是本着走一步测一步的原则,慢慢看,慢慢来
路由模式
说到前端路由,不得不说路由的两种模式:
- Hash 模式
- History 模式
两种路由的具体区别和使用,请参考我的文章:《Vue的mode中 hash 与 history 的区别》
Hash模式
hash模式的特性:
URL
中hash
值只是客户端的一种状态,也就是说当向服务器端发出请求时,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模式的特性:
pushState
和repalceState
的标题(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.js
、routes.js
、vue-router.js
文件。
整体结构如图所示:
在上图中,我们不使用Vue框架中的router
,因为人家的router
里已经给我们封装好了router-view
、router-link
、$router
、$route
等一系列方法,所以我们使用自己写的代码。
其中routes.js
中定义了我们需要用到的路由,而vue-router.js
文件中则相当于vue-router
的源码。
组件Home.vue
和About.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>
源码
如果只看代码,没有自己实践一番,肯定是理解不了的
评论区